Working Around ASP.NET's HyperLink ImageUrl BugBy Donnie Hale
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!
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
ImageUrlproperty, 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
~/gallery/welcome. That is, if a user enters
www.yourserver.com/photosinto 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.aspxpage. (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 |
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
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
.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
ImageUrlproperty, the result is either a broken image or an exception. Let's look at these two cases.
~/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 (
the real URL is one level higher (
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
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.
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
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:
~/welcome.aspx. The images displayed by
the HyperLink controls in the two pages reside in the
The following URL mapping rules defined in
Web.config map the virtual URLs to the real ones:
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
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 Using
ImageUrl. 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
<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 (
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
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
UrlResolved property, which it wasn't. At this point, I was very
suspicious of this double-resolving of the HyperLink's
Verification and Workaround
I needed a way to determine if the double-resolving of the HyperLink's
ImageUrlproperty was the culprit, but I obviously couldn't recompile the
System.Webassembly. One approach might have been to create a custom server control derived from HyperLink and override its
RenderContentsmethod so that it didn't resolve the
ImageUrlof 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
Note that because the
Render method on a control adapter has full
responsibility for rendering the control, we have to call
RenderEndTag methods in the
ImageUrl handling code. However, for the
else cases in
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
.browser file in the special ASP.NET
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.
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.