To read the article online, visit http://www.4GuysFromRolla.com/articles/013008-1.aspx

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
Now imagine that your boss wants to utlize URL rewriting so that the above URLs are available at the virtual URLs ~/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 Web.config. On each request, the requested URL is checked against the table of virtual URLs. If there is a match, the request is rewritten to the corresponding real URL. Another tool for URL rewriting is UrlRewriter.net, an open-source URL rewriter for ASP.NET.

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).

The hyperlink image renders as a red X.

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 sample project's Solution Explorer

The following URL mapping rules defined in Web.config map the virtual URLs to the real ones:

<urlMappings enabled="true">
   <clear />
   <add url="~/photos" mappedUrl="~/GalleryPages/photos.aspx" />
   <add url="~/gallery/welcome" mappedUrl="~/welcome.aspx" />
   <add url="~/gallery/welcome2" mappedUrl="~/welcome2.aspx" />
</urlMappings>

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:

  1. The problem had nothing to do with UrlRewriter.net, and
  2. 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 Cannot use a leading '..' to exit above the top directory exception was a starting point for looking into the bug further as it contained the call stack at the point of the exception.

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:

Armed with the information gleaned from these references and the stack trace, I was ready to get to the bottom of this bug.

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.)

The code for the HyperLink control's RenderContents method.

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.

The code for the Image control's AddAttributesToRender method.

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
{
   protected override void Render(HtmlTextWriter writer)
   {
      HyperLink hl = this.Control as HyperLink;
      if (hl == null)
      {
         base.Render(writer);
         return;
      }

      // This code is copied from HyperLink.RenderContents (using
      // Reflector). References to "this" have been changed to
      // "hl", and we have to render the begin and end tags.
      string imageUrl = hl.ImageUrl;
      if (imageUrl.Length > 0)
      {
         // Let the HyperLink render its begin tag
         hl.RenderBeginTag(writer);

         Image image = new Image();

         // I think the next line is the bug. The URL gets
         // resolved here, but the Image.UrlResolved property
         // doesn't get set. So another attempt to resolve the
         // URL is made in Image.AddAttributesToRender. It's in
         // the callstack above that method that the exception
         // or improperly resolved URL happens.
         //image.ImageUrl = base.ResolveClientUrl(imageUrl);
         image.ImageUrl = imageUrl;

         imageUrl = hl.ToolTip;
         if (imageUrl.Length != 0)
         {
            image.ToolTip = imageUrl;
         }

         imageUrl = hl.Text;
         if (imageUrl.Length != 0)
         {
            image.AlternateText = imageUrl;
         }

         image.RenderControl(writer);

         // Wrap up by letting the HyperLink render its end tag
         hl.RenderEndTag(writer);
      }
      else
      {
         // HyperLink.RenderContents handles a couple of other
         // cases if its ImageUrl property hasn't been set. We
         // delegate to that behavior here.
         base.Render(writer);
      }
   }
}

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>
  <browser refID="Default">
    <controlAdapters>
      <adapter controlType="System.Web.UI.WebControls.HyperLink"
               adapterType="HyperLinkTest.HyperLinkControlAdapter" />
    </controlAdapters>
  </browser>
</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!

  • By Donnie Hale


    Attachments


  • Download the Complete Source Code (in ZIP format)
  • Further Readings:

  • URL Rewriting in ASP.NET
  • A Look at ASP.NET 2.0's URL Mapping
  • UrlRewriter.NET: An Open-Source URL Rewriting Component for ASP.NET
  • Lutz Roeder's Reflector
  • Fiddler HTTP Debugger
  • ASP.NET 2.0 Control Adapter Architecture
  • Article Information
    Article Title: ASP.NET.Working Around ASP.NET's HyperLink ImageUrl Bug
    Article Author: Donnie Hale
    Published Date: January 30, 2008
    Article URL: http://www.4GuysFromRolla.com/articles/013008-1.aspx


    Copyright 2014 QuinStreet Inc. All Rights Reserved.
    Legal Notices, Licensing, Permissions, Privacy Policy.
    Advertise | Newsletters | E-mail Offers