Yesterday I closed a case about a migration issue from ASP.NET 1.1 to 2.0. The customer built this application based on ASP.NET 1.1 to generate some PDF documents on the fly on the web server, and stream the content to the client for reading; the application also served as a sort of archive browser, where online users are able to browse a list of archived PDF files. The customer thought carefully to his error handling (I think he did a very good job), and also decided to customize the standard 404 error page to something more friendly and informative for application’s users, so he configured the <customErrors> section in his web.config to point to a specific page to handle the 404 return codes. But he wanted this custom page to be displayed also if the user requested a non existing PDF file, so he had to add a new mapping in IIS console, to have requests for .pdf files go through the ASP.NET execution channel (aspnet_isapi.dll etc…) and benefit of the advanced features (including security and error handling) granted by the .NET Framework.
This worked pretty well fos some time, until the customer decided to migrate his web applications to ASP.NET 2.0. He recompiled the application with Visual Studio 2005, updated the IIS mapping to reference the aspnet_isapi.dll which comes with Framework 2.0 and then at the first run of the upgraded site got this weird behavior: if he tried to browse a non existing .aspx page he got his custom 404 page, but if he tried to browse a non existing .pdf file IIS prompted him to enter authentication credentials; but no matter which ones he inserted, after three attempts (all failing) he was redirected to the 401 error page.
They sent me a sample application, and reproducing the problem on my machine was quite straightforward. To be honest in my experience this is not a common scenario (maybe you can correct me?) so I didn’t remeber exactly all the details about how to configure it, but I had some echoes of an ASP.NET training I attended a few years ago (which was part of my MCSD certification, before joining Microsoft) and I thought we had to tell somehow the runtime how to manage this new extension we wanted to add… My mind went to HttpHandlers, but I didn’t find any new HttpHander in customer’s web.config. So I added it to my sample:
<httpHandlers> <add path="*.pdf" verb="*" type="System.Web.StaticFileHandler" validate="true"/> </httpHandlers>
I tested again, and I magically gor the custom 404 page also requesting a non existing .pdf file.
Next question is: if the application worked fine for some time with ASP.NET 1.1 without adding that HttpHandler, where is the difference? Well… thinking about how the configuration file mechanism works in ASP.NET, I opened “C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\CONFIG\machine.config” for 1.1 and “C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG\web.config” for 2.0, and started looking at the differences in the <httpHandlers> section. ASP.NET 2.0 has a longer list of extensions and HttpHandlers perconfigured, but I found what I was looking for at the end of the section:
machine.config for ASP.NET 1.1
[…] <add verb=”*” path=”*.asp” type=”System.Web.HttpForbiddenHandler” /> <add verb=”*” path=”*.licx” type=”System.Web.HttpForbiddenHandler” /> <add verb=”*” path=”*.resx” type=”System.Web.HttpForbiddenHandler” /> <add verb=”*” path=”*.resources” type=”System.Web.HttpForbiddenHandler” /> <add verb=”GET,HEAD” path=”*” type=”System.Web.StaticFileHandler” /> <add verb=”*” path=”*” type=”System.Web.HttpMethodNotAllowedHandler” /> </httpHandlers> […]root web.config for ASP.NET 2.0
[…] <add path=”*.refresh” verb=”*” type=”System.Web.HttpForbiddenHandler” validate=”true” /> <add path=”*.svc” verb=”*” type=”System.ServiceModel.Activation.HttpHandler, System.ServiceModel,Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089″ validate=”false” /> <add path=”*” verb=”GET,HEAD,POST” type=”System.Web.DefaultHttpHandler”
validate=”true” /> <add path=”*” verb=”*” type=”System.Web.HttpMethodNotAllowedHandler” validate=”true” /> </httpHandlers> […]
That’s the difference: in ASP.NET 1.1, if a request has an extension which does not match one of the predefined extensions (and has a GET or HEAD verb) will match the “*” path and will go through the StaticFileHandler module, while in ASP.NET 2.0 if the same happens the request will go through the DefaultHttpHandler module, which has a different behavior. So to be 100% sure I removed the custom HttpHander from application web.config for the repro, and added it in the root web.config, and of course the problem was again resolved.
Another step forward: is it conceivable that we make such a change without documenting it? Well… I learnt to not be surpresed of anything in life, but before becoming too philosophic I started searching our docs and I found a clear reference to this behavior in the ASP.NET Run-Time Breaking Changes page:
Short Description
When mapping custom extensions to an existing ASP.NET builtin handler, it is now necessary to configure a build provider for that extension.
Affected APIs
Configuration
Severity
Low
Compat Switch Available
No
Description
When mapping custom extensions to an existing ASP.NET builtin handler, it is now necessary to configure a build provider for that extension. In version 2.0, the ASP.NET build system requires a build provider to handle compilation. The builtin build providers can be reused but they need to be specified.
User Scenario
If an application maps a private file extensions (say .misc or .foo or whatever) to an existing ASP.NET handler type (say, the page handler), then it is now necessary to provide configuration that tells ASP.NET how to compile that file type. e.g.
<system.web> <httpHandlers> <add verb="*" path="*.misc" type="System.Web.UI.PageHandlerFactory" /> </httpHandlers> </system.web>In v1, this would work as is. In version 2.0, they also need to register a BuildProvider. e.g.
<compilation> <buildProviders> <add extension=".misc" appliesTo="Web" type="System.Web.Compilation.PageBuildProvider" /> </buildProviders> </compilation>Types mapped to the static file handler or star mapped types are not impacted by this, only compiled types.
Work Around
Add a configuration directive to tell ASP.NET how to compile that file type.
I sent everything to the customer and he’s now happiling displying his custom 404 page also for non existing PDF files .
Lesson learnt with this one: if I had the breaking change page at hand, I very likely saved the time spent in troubleshooting… but hey, at least we solved the problem!
Carlo