Working Around ASP.NET's HyperLink ImageUrl Bug
By Donnie Hale
Introduction
If you develop software long enough, you'll inevitably run into bugs in your platform or framework of choice. It's important on those occasions that you know how to apply the tools at your disposal to clarify the source of the bug; reproduce it in a deterministic fashion; and either fix the bug, avoid it altogether, or find the best possible workaround.
This article shines the light on a bug I came across in the ASP.NET HyperLink control implementation.
In particular, when using URL rewriting the HyperLink control's ImageUrl
property can be,
in certain circumstances, incorrectly rewritten. The good news is that there is a simple workaround
that's made possible by the flexible architecture of ASP.NET. We'll look at this workaround.
Regardless of whether you need a fix for this bug, I invite you to read on as this article examines the tools and techniques I used to research the bug and devise a workaround. These techniques can help diagnose and rectify other framework-level problems. Read on to learn more!
Background
As with most ASP.NET applications, the system I currently develop and maintain (along with a team of other developers) makes extensive use of the HyperLink control. Many of HyperLinks in our pages use the
ImageUrl
property, which specifies the path to an image to display for the link. Being good ASP.NET
developers, we try to consistently use the application-relative syntax for
those images, e.g. <asp:HyperLink ... ImageUrl="~/images/aspImage.png" />
.
We recently started using UrlRewriter.net to allow access to portions of our application using search engine-friendly extension-less "virtual URLs". While we've been overwhelmingly pleased with the results, we did see the occasional anomaly during development, and our application error logs started showing crpytic and unreproducible exceptions. During work on an upcoming release, we started seeing what I guessed to be related anomalies in the development environment. At the time, I had to move on quickly; so I used what I knew was a safe workaround. But I'd seen enough to know I was going to have to go back and figure out what was going on.
Let's say you're developing a photo gallery application that has two pages:
~/GalleryPages/photos.aspx
- displays a complete listing of the pictures in the photo gallery, and~/Welcome.aspx
- the site's main landing page
~/photos
and ~/gallery/welcome
. That is, if a user enters www.yourserver.com/photos
into their browser's Address
bar, the browser will show that the requested URL as www.yourserver.com/photos
, but behind the scenes the request will be rewritten by
ASP.NET to be handled by the ~/GalleryPages/photos.aspx
page. (Note that the example URLs used in
this explanation match those in the sample project available for download at the end of this
article.)
Performing URL Rewriting |
---|
When an incoming request is handled by the ASP.NET engine, it progresses through the ASP.NET pipeline. During this lifecycle, the request can be
rewritten to an alternate URL. This is accomplished using the HttpContext.RewritePath
method.
ASP.NET 2.0 includes a feature referred to as URL mapping,
which is a simple form of URL rewriting. With URL mapping, the page developer specifies what virtual URLs map to what real URLs in
For a more thorough discussion on URL rewriting, including common uses of URL rewriting and what's happening behind the scenes, refer to Scott Mitchell's article, URL Rewriting in ASP.NET. |
In your web site, you would make sure that the links use the virtual URLs rather than the real ones.
When a user clicks such a link, the browser sends a request for that
virtual URL to the web server. Before the page-handling mechanism
of ASP.NET begins the page lifecycle, URLRewriter.net sees that it has a
mapping from the virtual URL to the real page and modifies the request context
accordingly. When ASP.NET begins processing the page request, it now knows the
real .aspx
page that should be executed.
The HyperLink Control Bug
Note that the "depth" of the real pages doesn't match the depth of their corresponding virtual URLs, and that's where the problem arises. Specifically, if you place a HyperLink control in one of the pages that is mapped to a page at a higher or lower directory, and set the HyperLink control's
ImageUrl
property, the result is either a broken image or an exception.
Let's look at these two cases.
The ~/GalleryFiles/photos.aspx
page (shown below) has a HyperLink control with its ImageUrl
property set to
~/images/aspHyperLink.png
. This works fine if the user visits ~/GalleryFiles/photos.aspx
, but if he visits
~/photos
, the rendered page shows the "red X" missing image icon for the HyperLink image. In general, this problem arises anytime
the virtual URL being requested has its real URL at a "higher" level. In this case, the virtual URL is at the root directory (~/photos
), but
the real URL is one level higher (~/GalleryFiles/photos.aspx
).

When faced with a bad request from the browser, I often turn to Fiddler, a free tool that details the
HTTP requests and responses sent and received by the browser. Fiddler confirmed that the browser was
getting an HTTP "404 Not Found" error. Looking at the resulting page's HTML
source, I could see that the image URL was rendered as
GalleryPages/images/aspHyperLink.png
. The images
folder was resolved as
though it was below the real URL's folder rather than immediately below the
root of the site, e.g. /images/aspHyperLink.png
.
In the second case, where the real page is at a level higher than
the virtual URL, an obscure exception is thrown: Cannot use a leading
'..' to exit above the top directory. For example, when visiting ~/gallery/welcome
, which maps to the URL ~/welcome.aspx
, having
a HyperLink control in the page with its ImageUrl
set produced the following exception:

Note that this exception doesn't appear in the user's browser: they still see a "red X" in place of the image. But the exception is recorded by the ASP.NET runtime. If you have a system that automatically logs unhandled exceptions (such as Health Monitoring or ELMAH), you will see these exceptions there.
I was able to work around both of these issues by ensuring that the depth of a virtual URL always matched the depth of the real page to which it referred. That's what we'd been doing so far, but it wasn't satisfying. I knew that eventually our requirements would force a virtual URL and its target page to be at different depths; and I didn't like seeing the error log entries.
Reproducing the Bug
Since we'd not seen anything like these problems prior to introducing UrlRewriter.net, I attributed the issue to that library (erroneously, as it turned out). In no time I was able to create a very simple test application that reproduced the error. Further, I could reproduce the problem without UrlRewriter.net being referenced at all, simply by using the native URL mapping capability of ASP.NET 2.0. The download accompanying this article includes the entire test application.
The sample application has two pages of interest: ~/GalleryPages/photos.aspx
and ~/welcome.aspx
. The images displayed by
the HyperLink controls in the two pages reside in the ~/images/
folder.

The following URL mapping rules defined in Web.config
map the virtual URLs to the real ones:
<urlMappings enabled="true">
|
The master page has an Image Web control that renders the correct URL regardless of what virtual URL is visiting. The master page also includes the offending HyperLink control.
Having gotten this far, I was sure of two things:
- The problem had nothing to do with UrlRewriter.net, and
- There had to be a bug somewhere in ASP.NET since I was only using its native feature set, and only the simplest of its features at that.

The call stack reveals that the last time the HyperLink control was involved before the exception came was in the HyperLink control's
RenderContents
method.
I then spent some time researching this problem online, and came across two more helpful references:
- Scott Mitchell's treatment of the bug on his blog: URL Rewriting, the Image Web Control, and ~, and
- A report on this bug on Microsoft's Connect website: When using
RewritePath(path, false)
, the HyperLink Control Breaks If UsingImageUrl
. Reading the bug report was a little discouraging as Microsoft had been notified of this error in November 2006, yet opted not to fix to maintain "a high degree of backward compatibility."
Determining the Source of the Bug
If you haven't used Reflector, stop reading right now and start downloading this indispensible tool. Microsoft has just released the source code of most of the .NET base class libraries (for debugging purposes) as part of its Visual Studio 2008 release (see .NET Framework Library Source Code Now Available). However, since nearly the time of .NET's release, Reflector has been serving a similar purpose. It was the perfect tool to see if I could understand what was causing this behavior and if I had any hope of correcting it or finding an acceptable workaround.
I brought up the source code for HyperLink control's RenderContents
in Reflector.
RenderContents
is the method an ASP.NET web control uses to render what's
essentially the inner HTML of the control. An HTML link with an image takes
the form <a href="..."><img src="/images/myimage.jpg"></a>
. As the highlighted
area in code snippet below shows, if the HyperLink control's ImageUrl
property is set,
it creates an Image control, sets the appropriate properties on that control
based on its own properties, and allows the Image to render itself at the
current location in the HtmlTextWriter
stream. (Since it's rendering the inner
HTML at this point, the HyperLink's begin tag and attributes (<a href="...">
)
will have already been rendered.)

Now that I had an understanding of this method, I started working my way
toward the top of the call stack, starting with the call to
Control.RenderControl
. The methods between HyperLink.RenderContents
and
Image.AddAttributesToRender
are basically just setting up to allow the Image
to render itself. The AddAttributesToRender
method allows a control to add any
attributes and associated values that need to be rendered as part of the begin
tag of an HTML element.
The first thing I noticed about Image.AddAttributesToRender
(shown below) is
that it checks to see if the ImageUrl has already been resolved (converting it
from an application-relative URL, if that's the form in which it's specified,
to an appropriate URL for rendering into HTML for a browser). I recalled that
HyperLink.RenderContents
also resolves the ImageUrl
prior to setting that
property on the Image control it creates. I checked to see if it was setting
the UrlResolved
property, which it wasn't. At this point, I was very
suspicious of this double-resolving of the HyperLink's ImageUrl
property.

Verification and Workaround
I needed a way to determine if the double-resolving of the HyperLink's
ImageUrl
property was the culprit, but I obviously couldn't
recompile the System.Web
assembly. One approach might have been to create a custom server control
derived from HyperLink and override its RenderContents
method so that it
didn't resolve the ImageUrl
of the Image control it creates. The challenge
with this approach is that you have to change your use of HyperLink in
page markup to instead use the new custom control. That's feasible for a test
application but not as easy for an existing production application.
Instead, I decided to see if I could modify HyperLink's rendering behavior
using a control adapter.
A control adapter allows you to hook into the rendering
process of a control without having to change the control's implementation,
properties, etc. Using Reflector, I copied the HyperLink.RenderContents
code that
handles the case of an ImageUrl
being specified. The HyperLink code uses
this
to refer to the control instance, so I had to change that code to refer
to the control instance that's passed to a control adapter. Then I changed the
line that specifies a resolved URL for the Image control to just use the value
of ImageUrl
as-is.
public class HyperLinkControlAdapter : ControlAdapter
|
Note that because the Render
method on a control adapter has full
responsibility for rendering the control, we have to call RenderBeginTag
and
RenderEndTag
methods in the ImageUrl
handling code. However, for the else
cases in
HyperLink.RenderContents
(when ImageUrl
hasn't been specified), we delegate
the rendering to the base class implementation. In that case, we don't have to
worry about rendering the begin and end tags.
To test whether this works, I had to enable the control adapter. This is done
by specifying the target control type (HyperLink) and control adapter class in
a .browser
file in the special ASP.NET App_Browsers
folder.
<browsers>
|
With the control adapter enabled, both of the links on the master page work as expected. If you
comment out the <adapter>
element in the markup above to disable the control
adapter, the aberrant behavior returns. It seems conclusive that this
double-resolving of the HyperLink's ImageUrl is at the root of the problem.
Conclusion
Working software is what counts, and it's not always an easy path to get there. In this article we dissected a bug in the ASP.NET framework libraries by first identifying it; then analyzing it; and, finally, providing a fix. There are a number of tools that aid in performing these steps, such as Fiddler and Reflector. Thanks to the flexibility of ASP.NET's architecture, we were able to develop and verify an elegant workaround to the problem.
Happy Programming!
Attachments
Further Readings: