How to Redirect Folder Requests with ASP.NET

When you give a Web site URL to a potential customer, you want that URL to be as simple as possible. Normally, customers can just enter your domain name and start navigating your site from your home page.

However, sometimes you need to give customers a direct link to a specific page on your site. Giving them a full URL that includes a page name and maybe even some query string parameters is a good way to make sure they never see that page. What you really want to do is give them a simple mnemonic name that can be appended to your base URL.

Key Technologies and Concepts

Internet Information Services (IIS)

Dynamic Virtual Directories

Configuring Custom 404 Errors

Coding ASP.NET HttpModules

Redirecting Folder URLs

Data-Driven URL Redirection

Loading XML into a DataSet

For example, if you want to make it easy for customers to find your support page, you could make it so the URL http://www.MyDomain.com/Support goes directly to that page on your site. You do this easily in IIS by setting up a virtual directory called Support that is a URL redirect to your CustomerSupport.aspx page. You could also create a Support subfolder with the support page as the default document.

But what if you don’t want to go into IIS and set up a virtual directory every time you need one of these redirections, and you don’t want to litter your site with subfolders? If your site includes data-driven elements and you have an administration interface for it, wouldn’t it be better if you could just set up the redirections within that interface?

You might want to configure your redirections with a configuration file or database table for many reasons. For instance, if you want every product category in your shopping cart to have a corresponding friendly URL (e.g. www.MyStore.com/ebooks), it would be nice if you could just specify that URL when you set up the category in the shopping cart admin interface.

This article explains how to set up data-driven folder redirections so you can solve this kind of problem. My example uses an XML configuration file, but there’s no reason why you couldn’t apply the same techniques using a database.

Dealing with IIS 404 Errors

Download the Code

To download the sample code for this article, right-click over the link below and select the appropriate "Save Target As" option on the popup menu. The exact wording of this option differs a little from browser to browser.

The zip file contains a fully-functional ASP.NET 2.0 project. To set it up on your own computer, read the instructions in the Readme.pdf file.

Download the Demo Project

When I initially attacked the problem of folder redirections, it seemed pretty straightforward. I figured I’d just create an HttpModule that would intercept all requests and redirect the folder requests as they came in. In fact, that worked out just fine until I moved the project from Visual Studio’s debugging environment to a virtual directory under IIS.

As soon as I moved the project to IIS, I started getting 404 errors. It turns out that IIS was intercepting my requests and determining that the folder references were invalid before it ever passed those requests on to my HttpModule. Cassini, Visual Studio’s development server, passes ALL requests down to your application, so you never see this problem.

In researching the issue, I found that there are a couple of solutions. One solution is to create a "wildcard application map" that maps all file extensions to the .NET Framework DLL. The other solution is to redirect 404 errors to your ASP.NET application.

With wildcard application maps, you bypass the normal file handling performed by IIS for you. Normally, only .NET-related file extensions would be passed to the .NET Framework for processing. Other file extensions are handled by other ISAPI filters (e.g. asp.dll, ssinc.dll, etc), or the files are simply piped down to the browser (e.g. .htm, .css, etc). Granted, ASP.NET is smart about dealing with those non-.NET extensions when it receives them, but I could not see how adding .NET to the mix would do anything good for performance.

Also, passing all files through .NET means you will apply .NET authentication to all files. For example, if you use forms authentication, the style sheet you use for your login page will be one of the things that is blocked unless you take specific steps to avoid that problem, such as turning off authentication on the folder that holds the style sheet. Having to track down and correct these side-effects was another reason why wildcarding seemed like a bad idea to me.

I obviously chose the second alternative, which is to redirect 404 errors back to my application. It is easy to set up and has no side-effects that I’ve found. Additionally, when IIS passes the 404 errors to you, it clearly identifies the URL as a 404. This feature automatically gives your HttpModule a way to filter out only the requests it needs to process.

Setting Up 404 Redirection in IIS

To set up 404 redirection in IIS, modify the site or virtual directory properties for your application as follows:

  • Open the Properties dialog for the site or virtual directory and click the Custom Errors tab.
  • Select the 404 HTTP Error and click the Edit Properties button.
  • In the Error Mapping Properties dialog (see image below), change the Message Type to URL and set the URL to your application site’s default document (e.g. "/Default.aspx") or a custom error page (e.g. "/PageNotFound.aspx"). If your application is in a virtual directory, include the virtual path (e.g. "/URLRedirect/Default.aspx").
  • Save your changes.


Redirecting IIS 404 errors to a URL.

That’s it! Now, all 404 requests will be routed to your application for processing.

The URL that IIS hands off to you will look something like this:

http://localhost/urlredirect/PageNotFound.aspx?404;http://localhost/urlredirect/config

The key is that "?404;" you see in the middle of the URL. The 404 parameter includes the original path that was requested by the browser. You can use it to parse out and process the original path. Your HttpModule can also use it as a quick way to identify the 404 requests it needs to process, and allow all other requests to just fall through.

Something to consider is that, after the redirect, users see the true URL of the destination page on their address bar. There are ways around that issue, but given the simple requirements of my demo application, I’m willing to live with it. Once the visitor has successfully landed on the site, the simplified URL has done its job.

Peeking at HTTP Headers

One of the tools I’ve found very useful in debugging HTTP request/response information in Internet Explorer was developed by a fellow named Jonas Blunk. His tool, named ieHttpHeaders, puts a view window at the bottom of your Internet Explorer window and shows you all of the HTTP headers that the browser sends and receives as it navigates a Web site.

The information it provides can be eye-opening. For example, when I was testing the URLRedirector demo project, ieHttpHeaders exposed the double-302 response problem I describe in the article. That first redirect is generated by IIS before you even see the request in your application, so you would only notice it if you are watching client browser activity.

You can get ieHttpHeaders from Jonas’ site:

www.blunck.se

As a side note, I’d like to point out that I recommend you put an actual page reference in the 404 custom error, rather than using an unqualified folder reference like "/" or "/URLRedirect." If you don’t provide a page name, the visitor’s browser will receive two 302 responses for every request instead of just one. The first 302 is generated by IIS in response to the unqualified redirect to your application, and the second redirect is generated by your module. (See the side bar "Peeking at HTTP Headers" for information about how you can detect that first redirect.) If you provide a page name, it appears that IIS can transfer the initial request directly to the page you specify, so the browser only sees the 302 generated by your module.

Hooking into the HTTP Pipeline

One of the most powerful aspects of ASP.NET is the way Microsoft provided easy-to-use hooks into the processing cycle of HTTP requests and responses, which is referred to as the HTTP Pipeline. In the old days, hooking into the pipeline meant writing an ISAPI filter, which was not an exercise for the timid.

One of the pipeline hook points gives you full access to the HTTP request before it hits your Web page, which lets you inspect, change, or redirect the incoming request. This is the hook you’ll use to perform URL redirection.

To tap into the pipeline, you create an HttpModule. An HttpModule is essentially a filter that you can apply at the Web application level. In fact, the module has application scope and lifetime, which can come in handy as I explain shortly. Your module gets first shot at incoming requests (and last shot at outgoing responses), so it is an excellent location to handle URL rewriting of any kind.

Attaching an HttpModule to your application is surprisingly easy. All you have to do is write the module class and add a configuration setting to your Web.config. You can create a separate assembly for the module, but you don’t have to. You can just place the module class in the App_Code folder of your project and test it from there. For simplicity, that is the approach I chose in the demo project.

The Web.config settings go in configuration/system.web/httpModules. The setting in the demo project looks like this:

<httpModules>
   <!-- Custom HttpModule for path redirection -->
   <add name="URLRedirector" type="URLRedirector"/>
</httpModules>

Writing an HttpModule

The code inside the HttpModule is also surprisingly simple. You write a class that implements the IHttpModule interface. The interface includes only two methods: Init and Dispose. Here’s the entire source code for the module from the demo project. I explain each section in detail below:

Imports System
Imports System.Web
Imports System.Data
 
Public Class URLRedirector
   Implements IHttpModule
 
   Private mdsRedirect As DataSet
 
   Public Sub Init(ByVal TheApp As HttpApplication) Implements IHttpModule.Init
      AddHandler TheApp.BeginRequest, AddressOf Me.Application_BeginRequest
      ' Cache the redirection file in a dataset.
      mdsRedirect = New DataSet()
      mdsRedirect.ReadXml(HttpContext.Current.Server.MapPath("~/App_Data/Redirection.xml"))
   End Sub
 
   Public Sub Dispose() Implements IHttpModule.Dispose
      mdsRedirect = Nothing
   End Sub
 
   Private Sub Application_BeginRequest(ByVal Source As Object, ByVal e As EventArgs)
      Dim oApp As HttpApplication = CType(Source, HttpApplication)
      Dim strRequestUri As String
 
      strRequestUri = oApp.Request.Url.AbsoluteUri.ToLower()
 
      ' OPTION 1: Process all requests (use when running in Visual Studio)...
      ' Redirect known paths and just let the others fall through.
      Call RedirectKnownPath(oApp, strRequestUri)
 
      ' OPTION 2: Process only the 404 requests (use when running under IIS)...
      ' For this module to work under IIS, you must configure the web site to redirect
      ' all 404 requests back to the application.
      'If strRequestUri.Contains("?404;") Then
      '   If Not RedirectKnownPath(oApp, strRequestUri) Then
      '      ' Send all 404 requests for unknown paths to the default page.
      '      oApp.Response.Redirect("~/Default.aspx")
      '   End If
      'End If
   End Sub
 
   Private Function RedirectKnownPath(ByVal oApp As HttpApplication, _
         ByVal strRequestUri As String) As Boolean
      Dim strOriginalUri As String = strRequestUri
      Dim intPos As Integer
      Dim boolPathRedirected As Boolean = False
      Dim oRow As DataRow
      Dim strRequestPath As String
      Dim strDestinationUrl As String
 
      ' Extract the original URL if you received a 404 URL.
      intPos = strRequestUri.IndexOf("?404;")
      If intPos > 0 And strRequestUri.Length > (intPos + 5) Then
         strOriginalUri = strRequestUri.Substring(intPos + 5)
      End If
 
      ' Redirect the request if you find a matching request path.
      For Each oRow In mdsRedirect.Tables(0).Rows
         strRequestPath = Convert.ToString(oRow("RequestPath")).ToLower()
         If strOriginalUri.EndsWith(strRequestPath) Then
            strDestinationUrl = Convert.ToString(oRow("Target"))
            Call oApp.Response.Redirect(strDestinationUrl, False)
            boolPathRedirected = True
            Exit For
         End If
      Next
 
      Return boolPathRedirected
   End Function
End Class

The Init method bootstraps your module and gives you a way to hook a method call from your module into the HTTP pipeline. ASP.NET calls the Init method of all modules specified in Web.config when your Web application initializes.

When ASP.NET calls the Init method, it passes a reference to the current HttpApplication object. Using this reference, you can hook your own handlers onto any HttpApplication event. In my case, I want to hook into the BeginRequest event, which is accomplished with the following line of code:

AddHandler TheApp.BeginRequest, AddressOf Me.Application_BeginRequest

You can call the event handler anything you want. I just followed the VB.NET convention of Class_EventName. The signature of the event handler must match the signature of the BeginRequest event, of course.

If you need to do anything else to initialize your HttpModule, put it in the Init method after your AddHandler statement. In my case, I need to grab the desired redirections from a configuration file and load them into a DataSet for easy processing. I cover that operation in more detail shortly.

In the Dispose method, you can put any shutdown code related to the actions you performed in the Init method. For demonstration purposes, I set the DataSet I loaded to Nothing, even though the reference would be dropped automatically when the module instance is dropped.

Coding a BeginRequest Handler

The next step is to add code to your BeginRequest event handler. The BeginRequest event happens before the request is passed to the appropriate page handler, so you get to mess with the request before the page even sees it. It is a perfect opportunity to tweak the headers or content in the request, or even redirect it.

ASP.NET passes a reference to the current HttpApplication object as the first parameter, and event arguments (which I won’t need for this exercise) in the second. Since I do need to use the application reference, I cast the reference to an appropriately defined variable right up front.

Dim oApp As HttpApplication = CType(Source, HttpApplication)

The application reference gives me access to the current HttpRequest object, which in turn gives me access to the requested URL.

strRequestUri = oApp.Request.Url.AbsoluteUri.ToLower()

Now, what I do with that URL depends upon whether I’m running under IIS or Visual Studio. The "Option 1" code passes all requests through the redirection filter (RedirectKnownPath) because I don’t get a 404 error redirect from the Visual Studio Web server.

' OPTION 1: Process all requests (use when running in Visual Studio)...
' Redirect known paths and just let the others fall through.
Call RedirectKnownPath(oApp, strRequestUri)

The "Option 2" code, which you should use when running under IIS, passes only 404 errors through the redirection filter. Of course, for that code to work properly, you have to redirect 404 errors back to the application, as I described earlier.

' OPTION 2: Process only the 404 requests (use when running under IIS)...
' For this module to work under IIS, you must configure the web site to redirect
' all 404 requests back to the application.
If strRequestUri.Contains("?404;") Then
   If Not RedirectKnownPath(oApp, strRequestUri) Then
      ' Send all 404 requests for unknown paths to the default page.
      oApp.Response.Redirect("~/Default.aspx")
   End If
End If

The Option 1 code also works just fine under IIS, but it is wasteful. Unless you use the 404 filtering logic or some kind of file extension filtering, every document request that ASP.NET processes loops uselessly through the redirection filter.

How you handle true 404 errors is up to you. By "true 404," I mean that IIS could not find the requested path, and the RedirectKnownPath method did not redirect the path either. In the demo project, I redirect all true 404 errors to the home page. I describe a couple of other alternatives later in the article.

Configuring Paths for Redirection

Before I dig into the guts of the redirection logic, it is important to understand how I configured the redirection paths. I had several choices. I could have gotten them from a database, from a Web service, or from some kind of configuration file.

To keep the demo project simple, I used an XML configuration file (Redirection.xml) to define what paths to look for and where to send the browser if a configured path is found. I refer to these configured paths as "known paths."

The configuration file looks like this:

<?xml version="1.0" encoding="utf-8" ?>
<Redirections>
   <Redirection RequestPath="/Home" Target="~/Default.aspx"/>
   <Redirection RequestPath="/Config" Target="~/RedirectionFile.aspx"/>
   <Redirection RequestPath="/Setup" Target="~/IISSetup.aspx"/>
</Redirections>

You can put as many Redirection elements as you need within the Redirections document node. Redirection elements have a RequestPath attribute that defines what path to look for and a Target attribute that defines where to redirect the response when the RequestPath is found. The redirection module compares the value in RequestPath to the end of the original request using the EndsWith string method. The attribute value is not case sensitive.

You could obviously get a lot fancier here. For example, you could use regular expressions in the RequestPath with corresponding code in your HttpModule to process them. Again, given my simple requirements, matching the end of the URL works out just fine.

The cool thing about using an XML file is that you can easily load it into a DataSet and process it just as if the data did come from a database. That is what the additional code in the module’s Init method does.

' Cache the redirection file in a dataset.
mdsRedirect = New DataSet()
mdsRedirect.ReadXml(HttpContext.Current.Server.MapPath("~/App_Data/Redirection.xml"))

Keep in mind that an HttpModule has application lifetime. When I load the configuration file into a module instance variable, that means I am effectively caching the data for the lifetime of the application. If I make a change to my configuration file, those changes won’t do anything until I restart the application. If you need a more dynamic solution than that, use a different approach, such as the ASP.NET Cache (with a timeout) or a file watcher.

Redirecting Known Paths

The RedirectKnownPaths method is responsible for performing the following tasks:

  • Parse the originally requested path out of the URL.
  • Compare the trailing characters of the original path to the configured paths.
  • If a match is found, redirect the browser to the destination URL associated with the configured path.

The first thing the code does is look in the URL for a 404 parameter. If it finds the 404 parameter, it parses the 404 URL to extract the path originally requested by the browser.

Being able to do "404 filtering" is one of the advantages of using the IIS 404 redirect approach: you automatically get a way to filter out just the requests that might require redirection. This capability is helpful from an efficiency standpoint because your module has to process every single request that gets serviced by ASP.NET, even the ones in which you have no interest. In contrast, using wildcard mapping not only eliminates the ability to do 404 filtering, but it causes all document requests (not just ASP.NET document types) to go through your module.

' Extract the original URL if you received a 404 URL.
intPos = strRequestUri.IndexOf("?404;")
If intPos > 0 And strRequestUri.Length > (intPos + 5) Then
   strOriginalUri = strRequestUri.Substring(intPos + 5)
End If

The rest of the method loops through the rows in the cached DataSet of redirections, seeking a match on RequestPath. If the original URL ends with the value configured in RequestPath, the method redirects the browser to the corresponding value in Target.

' Redirect the request if you find a matching request path.
For Each oRow In mdsRedirect.Tables(0).Rows
   strRequestPath = Convert.ToString(oRow("RequestPath")).ToLower()
   If strOriginalUri.EndsWith(strRequestPath) Then
      strDestinationUrl = Convert.ToString(oRow("Target"))
      Call oApp.Response.Redirect(strDestinationUrl, False)
      boolPathRedirected = True
      Exit For
   End If
Next

If you don’t find a matching "known path," the request just continues through its normal path through the ASP.NET pipeline.

Resolving True 404 Errors

I’ve shown you how to deal with 404 errors that are known paths and need to be redirected. But what if you get a 404 error that is not a known path? In other words, it is a genuine 404 error.

If you don’t actively redirect unknown path requests to a specific page, the request falls through to ASP.NET. Where it goes from there depends upon what path you put into the custom 404 error in IIS.

If you use Default.aspx (or the name of any existing page in your project) in the redirect path of your custom error, you have an interesting situation. ASP.NET displays the Default.aspx page, but the browser address line will show the path that generated the 404 error. Hmm. It seems that all unknown paths lead home!

On the other hand, you can create a custom 404 error page and use that page as the custom 404 error path in IIS. Once again, the browser will show the bad path in the address line, but your error page will let visitors know that the path is invalid.

I tend to think that using a custom 404 page is the most user-friendly solution. The visitor knows what happened, and you can also give them links to the home page and your contact page. The demo project includes PageNotFound.aspx, which does just that. If you go back to the IIS properties dialog image, you’ll see that I used PageNotFound.aspx in my error redirect.

However, the demo project forcefully redirects all true 404 errors to Default.aspx, so you never actually see PageNotFound.aspx. That’s easy to correct: just comment-out the redirect to Default.aspx. The 404 error will fall through to PageNotFound.aspx instead, and the browser address bar will show the URL that failed.

Conclusion

I hope this article gave you some ideas on how to build your own folder redirection module. As simple as the demo project is, it demonstrates several useful skills:

  • Creating an HttpModule and hooking it into the HTTP Pipeline.
  • Loading XML files into a DataSet.
  • Configuring custom 404 errors in IIS.
  • Processing IIS 404 errors.

If you expand the demo project with a few extra features like getting redirections from a database, caching redirections with a timeout, and using regular expressions to match requested paths, you could put the module into its own assembly and have a robust tool that you could use in all of your ASP.NET projects.

Of course, you can also do an Internet search and find third-party URL redirection and rewriting tools. Some are even free. But if your needs are simple, building your own redirection tool gives you valuable experience and full control over how the tool works.

Please see the Download the Code side bar for instructions if you’d like to download the demo project presented in this article.