So far in this series I have been exploring the error handling features in an ASP.NET MVC Web application from the point of view of handling internal server errors. Today I will be turning my attention to handling 404 Not Found errors.
This is Part 4 of a series of posts on the subject of adding error handling to an ASP.NET MVC 5 Web application. The index to this series can be found
here
.
Types of 404 Not Found Errors
There are a number of scenarios that should result in a 404 Not Found response being generated.
Explicitly Throwing a Not Found Exception
Sometimes a request matches a route in a Web site, but other information in that request identifies a non-existent resource. An example is a request for a user’s details, where the supplied user ID is not found in the site’s database. You could handle this by throwing an HttpException with an HTTP status code of 404 Not Found:
throw new HttpException(
(int)HttpStatusCode.NotFound,
"A message");
This exception could then be handled at some point in the request-processing pipeline and a 404 error page response generated.
The demo application contains two 404 links for triggering this scenario:
Action method 404 exception
JSON action method 404 exception
Both links make a request to an action method that throws a 404 HttpException, but the JSON version does so via an AJAX request.
Experiment: Throwing a Not Found Exception
All settings
Default values
Error link to click
Action method 404 exception
You should get the following detailed Custom Errors 404 error page:
The page has a 404 status code and there is no redirect. If you try changing the Custom Error Mode setting to On, you should see that the following simple Custom Errors 404 error page gets served instead:
This page is the same except that it does not include the Version Information section at the bottom.
Experiment: Throwing a Not Found Exception on an AJAX Request
All settings
Default values
Error link to click
JSON action method 404 exception
You should get message boxes showing that the response has the correct status code and it is the detailed Custom Errors 404 error page that has been returned.
As an alternative to throwing a 404 Not Found HttpException, you could instead return an instance of HttpNotFoundResult:
return new HttpNotFoundResult();
This class was new to ASP.NET MVC 3. When the returned instance is invoked, it creates a response that has an HTTP status code of 404 and an empty body. Because the response has an empty body, this approach is not useful if the 404 response will be seen by a user. Nevertheless, you may find it useful when returning a 404 response for a failed AJAX request.
Unknown Routes
As covered in the first post in this series, the routes for a site are registered with the UrlRoutingModule. When a given request matches one of those routes, the handler associated with the matching route is used to generate the response. In an MVC Web site, that handler is normally an instance of MvcHandler, and it handles the request in the following way:
An instance of the controller class that will service the request is created using the registered controller factory. That factory will normally be an instance of DefaultControllerFactory.
If the controller class cannot be located in the site’s code, a 404 Not Found HttpException is thrown.
The controller’s Execute method is invoked. This method calls the InvokeAction method on the controller’s action invoker. By default that action invoker is an instance of the ActionInvoker class in the MVC framework.
The action invoker calls the correct action method on the controller. If it cannot find the action method on the controller, the controller’s HandleUnknownAction method gets called. That method throws a 404 Not Found HttpException.
The action method returns an action result, which the ActionInvoker then invokes to generate the response.
While the ActionInvoker is executing, it also invokes any action filters that are associated with the action method. If an unhandled exception gets thrown during this process, any exception filters associated with the action method get invoked.
This process thus includes the following failure points:
The request could be for a known controller but could be for an action method that does not exist on that controller. The result by default is a 404 Not Found HttpException.
The request could be for an unknown controller. Again, the result is a 404 Not Found HttpException.
The request could fail to match any route. In this case, rather than the UrlRoutingModule handling the request, it is treated like a request for a static file and gets handled natively by IIS. IIS will generate a 404 Not Found response because there is no file on disk that matches the request URL.
The demo application contains the following links for triggering these failure points:
GET for unknown action
POST for unknown action
Unknown controller
Does not match any route
Experiment: GET Link to Unknown Action
All settings
Default values
Error link to click
GET for unknown action
You should get the detailed Custom Errors 404 error page. The page has a 404 status code and there is no redirect.
You should get the same page with the POST for unknown action link.
Experiment: Link to Unknown Controller
All settings
Default values
Error link to click
Unknown controller
You should get the same Custom Errors 404 error page as in the previous experiment.
Experiment: Link Does Not Match Any Route
All settings
Default values
Error link to click
Does not match any route
You should get the following detailed HTTP Errors 404 error page:
The page has a 404 status code and there is no redirect. This page is the HTTP Errors 404 page rather than the Custom Errors 404 page because it is native IIS code that ends up handling the request rather than a managed module.
If you try changing the HTTP Errors Mode to Custom, you should get the following simple HTTP Errors 404 error page:
Zombie DOS
The Windows OS includes
a number of reserved file names
whose roots go back to before the days of MS-DOS. These file names are COM1-9, LPT1-9, CON, AUX, PRN, and NUL, in any case and with any extension. You cannot create files or directories with these names, so names like com1, Com1, com., and COM1.txt are all forbidden. This restriction extends to ASP.NET because pages in ASP.NET Web applications correspond to files in the file system. If a request is made for a reserved file, ASP.NET steps in and returns a 404 Not Found response.
The MVC framework introduced a new paradigm where requests map to handler objects rather than files, but it nevertheless runs within ASP.NET and ASP.NET still first checks for requests that match reserved file names. Consequently, routes like /aux/whatever and /aux.a are not possible. However, a new property was added to ASP.NET in .NET Version 4 that makes it very easy to remove this reserved file names restriction completely. This property is called
RelaxedUrlToFileSystemMapping
and it can be set in a site’s Web.config file:
The demo application includes an example Zombie DOS link.
Experiment: Zombie DOS
All settings
Default values
Error link to click
Zombie DOS
You should get the detailed Custom Errors 404 error page. The page has a 404 status code and there is no redirect.
Restricted Files and Directories
There are a number of files and directories in an ASP.NET MVC Web application that should have access to them restricted in some way.
The following ASP.NET files should not be served:
Any file with the extension .config, including Web.config
global.asax
The following ASP.NET directories should not be browsable and their contents should not be served:
app_code
app_globalresources
app_localresources
app_webreferences
app_data
app_browsers
The following MVC directories should have access restrictions applied:
Controllers
Views
Content
The Controllers and Views directories should not be browsable and their contents, such as the Razor view files in the Views directory and the C# files in the Controllers directory, should not be served. The Content directory should not be browsable but its contents should still be served (since this is where the site’s static content is normally located).
There are two modules in an ASP.NET Web site’s request-processing pipeline that provide most of the necessary restrictions: DirectoryListingModule and RequestFilteringModule.
The DirectoryListingModule is configured by default to prevent the site’s directories from being browsable. (A directory is browsable if a client can make a request for it and get back a listing of the directory’s contents.)
The RequestFilteringModule is configured by default to prevent the ASP.NET directories and any content in them from being served. It also prevents the Web.config and global.asax files from being served, along with any files with various extensions, including .cs and .config.
The Views MVC directory is handled differently. If you look at any ASP.NET MVC Web site, you will see that there is a Web.config file in its Views directory. This is in addition to the Web.config that is located in the site’s root directory. The Views Web.config file is there to block access to all the files in the Views directory, which it does by using the System.Web.HttpNotFoundHandler class.
The Views Web.config file in the demo application actually uses a class that mimics the behaviour of System.Web.HttpNotFoundHandler in order to block access to the view files. I did this as I needed to be able to make its effect configurable, but I could not just derive a class from it as it is marked as internal. The following is the relevant detail of demo application’s Views Web.config file:
<system.web>
<!-- For IIS 5 and 6, and the Visual Studio Development Server -->
<httpHandlers>
<add path="*" verb="*"
type="MvcErrorHandling.MvcHelpers.CustomHttpNotFoundHandler"/>
</httpHandlers>
</system.web>
<system.webServer>
<!-- For IIS 7+ -->
<handlers>
<remove name="BlockViewHandler"/>
<add name="BlockViewHandler"
path="*" verb="*"
preCondition="integratedMode"
type="MvcErrorHandling.MvcHelpers.CustomHttpNotFoundHandler"/>
</handlers>
</system.webServer>
This post by Phil Haack
explains how the handler blocks access to the views. One detail that has changed since he wrote it is that the configuration now excludes all files in the Views directory, not just .aspx files.
Restricted ASP.NET Files and Directories
The demo application includes the following example links for accessing the restricted ASP.NET files and directories:
app_code directory
bin directory
web.config
global.asax
Experiment: Requesting the app_code Directory
All settings
Default values
Error link to click
app_code directory
You should get a detailed HTTP Errors 404.8 error page, with a 404 status code and no redirect:
You should get the same response with the bin directory and Web.config links. It is the RequestFilteringModule that steps in to deny access to these ASP.NET files and directories.
The ‘8’ in the 404.8 status code is a sub-status code, a feature that IIS includes so that it can more precisely indicate the cause of an error.
This Microsoft Support article
lists the status and sub-status codes in IIS 7. Note that the sub-status codes are only available on the server; they are not transmitted back to the client. You can make use of sub-status codes in HTTP Errors when creating a custom error page mapping:
All settings
Default values
Error link to click
global.asax
You should get a detailed HTTP Errors 404.7 error page, with a 404 status code and no redirect:
While it is also the RequestFilteringModule that steps in to deny access to this file, a different sub-status code is returned.
Restricted MVC Files and Directories
The demo application includes the following example links for accessing the restricted MVC files and directories:
Controllers directory
Views directory
Razor view file
Razor layout file
Content directory
Experiment: Requesting the Controllers Directory
All settings
Default values
Error link to click
Controllers directory
You should get the following detailed HTTP Errors 403.14 error page, with a 403 status code:
The 403.14 status code means “Directory listing denied”.
If you use your browser’s developer tools to view the result of the request, you will see that a 301 redirect is issued for the requested URL, with the redirect being to the Controllers directory but with a slash appended, and then a 403 response is issued for the redirect URL. It is the DirectoryListingModule that steps in to prevent the contents of the Controllers directory from being displayed, and I suspect that it is also issuing the initial redirect.
You should get the same response, including the redirect, if you click on the Content directory link.
Interestingly, it is allowed in the HTTP/1.1 specification to return a 404 status code in place of a 403 status code:
10.4.4 403 Forbidden:
The server understood the request, but is refusing to fulfill it. Authorization will not help and the request SHOULD NOT be repeated. If the request method was not HEAD and the server wishes to make public why the request has not been fulfilled, it SHOULD describe the reason for the refusal in the entity. If the server does not wish to make this information available to the client, the status code 404 (Not Found) can be used instead.
This possibility exists because a 403 Forbidden error has a different meaning to a 401 Not Authorized error. The 401 error indicates that if you can gain authorization somehow then you will be able to request the resource, while the 403 error indicates that the request can never be fulfilled, regardless of what actions you take to try to change this.
Experiment: Requesting the Views Directory
All settings
Default values
Error link to click
Views directory
You should get the detailed Custom Errors 404 error page, with a 404 status code and no redirect. It is the HttpNotFoundHandler configured in the Views directory’s Web.config file that prevents access to the Views directory.
Experiment: Requesting a Razor View File
All settings
Default values
Error link to click
Razor view file
You should get the detailed Custom Errors 404 error page, with a 404 status code and no redirect. Again, it is the HttpNotFoundHandler that rejects all requests for files in the Views directory.
Experiment: Requesting a Razor Layout File
All settings
Default values
Error link to click
Razor layout file
You should get the detailed Custom Errors 500 error page, with a 500 status code and no redirect. The error message should be “Files with leading underscores (“_”) cannot be served.” The layout file in the link is _layout.cshtml, which has a leading underscore. ASP.NET includes a restriction where
it cannot serve a file that begins with an underscore
, and the response is 500 Internal Server Error rather than 404 Not Found Error.
Unknown Static Files
A request could be made for a static resource, such as an HTML file, that does not exist. The link in the demo application for triggering this scenario is Unknown image.
Experiment: Requesting an Unknown Static File
All settings
Default values
Error link to click
Unknown image
You should get the detailed Custom Errors 404 error page, with a 404 status code and no redirect.
As described in the first post in this series, MVC routes get registered with the UrlRoutingModule. By default, if the path of the requested URL matches that of a file on the computer then the UrlRoutingModule is skipped. In the experiment, the requested path ‘/unknown.jpg’ does not exist and so the UrlRoutingModule gets to try to match the path to a registered route. It successfully matches the following Default route, with a controller of unknown.jpg and an action of Index:
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new {controller = "Home", action = "Index", id = UrlParameter.Optional});
Since there is no controller called ‘unknown.jpg’, an HttpException gets thrown that has the message “The controller for path ‘/unknown.jpg’ was not found or does not implement IController.” Custom Errors handles this exception by returning a 404 response.
Handling 404 Not Found Errors
There are two basic approaches to handling 404 Not Found errors: deal with them as soon as they arise by hooking into the various ASP.NET MVC framework features, or catch and handle them in an HttpApplication.Error event handler. However, both of these approaches will only deal with some of the 404 Not Found errors because of the myriad ways 404 errors can be produced. For the other types of 404 error, the handling is the same regardless of the basic approach taken.
Handling 404 Not Found Errors Using ASP.NET MVC
There is a
NuGet
package called
NotFound Mvc
that you can use to automatically add 404 Not Found error handling to an ASP.NET MVC Web application (specifically, an MVC Version 3 Web application). It serves as a good example of how to handle 404 errors using the error handling features of MVC, plus it also includes handling of 404 errors generated from outside that framework. I could not use NotFound Mvc with the demo application since I have to be able to make all the error handling in that application configurable, so I have instead recreated its features. I used Version 1.0.0 of the package to do this.
NotFound Mvc Implementation
The NotFound Mvc package automatically makes a series of changes to any ASP.NET MVC 3 Web application that you add it to. If you look at the
source for the package
, you will see that it has the following effects:
It adds a NotFoundController class and a NotFoundViewResult class to the site. The NotFoundController is a special controller that always returns an instance of NotFoundViewResult when it is executed. The NotFoundViewResult class outputs the site’s 404 view to the response, setting the response status code to 404 and TrySkipIisCustomErrors to true in the process.
It decorates the site’s controller factory with an instance of ControllerFactoryWrapper. When the controller factory is called to create a controller instance, the wrapper uses the wrapped controller factory instance to create the controller instance and it then decorates that controller’s ActionInvoker with an instance of ActionInvokerWrapper. If the specified controller cannot be found, the decorator catches the resulting exception and returns an instance of the NotFoundController class.
When the created controller get executed, the ActionInvokerWrapper decorator uses the wrapped action invoker to handle calling the correct action method on the controller. If that action method cannot be found on the controller, the wrapper creates and executes an instance of NotFoundController.
The package adds a ‘/notfound’ route to the site’s routes collection:
// To allow IIS to execute "/notfound" when requesting
// something which is disallowed, such as /bin or /app_data.
RouteTable.Routes.MapRoute(
"NotFound",
"notfound",
new { controller = "NotFound", action = "NotFound" });
The package adds a catch-all route as the last route in the site’s routes collection:
It adds a view called NotFound to the Shared views directory. You would then customize this view to match the site’s look.
The only manual change that you have to make is to change the type of the blocking handler class used in the Views Web.config file in order to block access to the Views directory and its contents:
The NotFoundMvc.NotFoundHandler class is included in the DLL that the package adds to the site.
Recreating NotFound Mvc in the Demo Application
The following experiment mimics the result of adding the NotFound Mvc package to an ASP.NET MVC 5 Web site.
Experiment: NotFound Mvc-style 404 Handling
Manual Error Handling – 404 error handling
Within MVC Framework
HTTP Errors – Mode
Custom
HTTP Errors – 404 Not Found redirect
Mvc Error Page
Other Settings – Relaxed URL mappings
Relaxed
All other settings
Default values
If you click on the 404 error links in turn, you will find that the following links are handled correctly:
the links to action methods that throw 404 Not Found HttpExceptions;
the links for the unknown action, controller, and route;
the Zombie DOS link;
the unknown image link;
the Views directory and Razor view file links;
the links for the restricted ASP.NET directories and files.
These links all return the following custom 404 error page, with the correct 404 status code and no redirect:
There are three links that the demo application does not handle correctly:
Controllers directory;
Content directory;
Razor layout file.
The Controllers and Content directory links still return the 403 HTTP Errors error page, but you could deal with these by configuring HTTP Errors to also show a custom 403 or 404 error page when a 403.14 error occurs.
The Razor layout file link returns a Custom Errors 500 error page, but this would get replaced with a friendly 500 page if you also added some form of 500 Internal Server Error handling.
NotFound Mvc In Summary
In theory NotFound Mvc is a great tool because it almost automates the process of adding 404 Not Found error handling to an ASP.NET MVC Version 5 Web site. My only concern is that some of the changes it makes, such as wrapping the site’s controller factory and adding the extra routes, are not obvious from looking at the site’s code. You could add comments to the site’s code to help deal with this issue.
Handling 404 Not Found Errors In an HttpApplication.Error Event Handler
An alternative approach to that exemplified by NotFound Mvc is to handle the 404 errors that get generated by the MVC framework not within the framework but instead within an HttpApplication.Error event handler. You have previously seen how such a handler can be used to deal with 500 Internal Server Error situations, but it can also be extended to deal with 404 errors. Note that you still need to deal with other types of 404 errors using the same approaches used by the NotFound Mvc package:
a catch-all route needs to be added to the site’s routes collection, as the last route in the collection;
the relaxedUrlToFileSystemMapping flag needs to be set;
custom error mappings need to be added to HTTP Errors for 404.8 and 404.7 status codes;
a custom error mapping needs to be added to HTTP Errors for the 403.14 status code;
custom 500 error handling is needed to deal with the error created by the Razor layout file link.
What is not required is to replace the two usages of the System.Web.HttpNotFoundHandler class in the Web.config file in the Views directory with a custom blocking handler. This is because the default blocking handler throws a 404 Not Found HttpException that gets caught and dealt with in the HttpApplication.Error event handler.
The next experiment demonstrates the HttpApplication.Error event handler approach.
Experiment: Handling 404 errors Using an HttpApplication.Error Event Handler
Manual Error Handling – 404 error handling
Application Error Handler
Manual Error Handling – App error handler response
Transfer Request
HTTP Errors – Mode
Custom
HTTP Errors – 403 Not Found redirect
Mvc Error Page
HTTP Errors – 404 Forbidden redirect
Mvc Error Page
Other Settings – Relaxed URL mappings
Relaxed
All other settings
Default values
You should find that you get friendly error pages for all of the 404 links except for the Razor layout file link. You can get that link to return a friendly error page if you also turn on manual error handling of 500 errors using the HttpApplication.Error event approach.
The Magical Unicorn Mvc Error Toolkit
While searching for error handling frameworks in ASP.NET MVC, I found the
Magical Unicorn Mvc Error Toolkit
NuGet package. This is a package that adds both 404 error and 500 error handling to an ASP.NET MVC Web site, and it does so only using Custom Errors and HTTP Errors. The author described how it works in
a post on Stack Overflow
. When added to an MVC Web site, it makes the following changes:
it adds an error controller along with a view for the 500 error page and a viiew for the 404 error page;
it sets the Custom Errors Mode to On, and adds a default redirect to the 500 error page and a mapping for the 404 error page for 404 errors;
it sets the HTTP Errors Mode to Custom, and adds a mapping to the 500 error page for 500 errors and a mapping to the 404 error page for 404 errors;
it registers two routes, one for the 500 error page and one for the 404 error page.
The only manual change you need to make is to remove the registration of the HandleErrorAttribute as a global action filter in the site’s Global.asax file.
The following experiment simulates how Magical Unicorn works.
Experiment: Simulating the Magical Unicorn Error-Handling Approach
Custom Errors – Mode
Custom Errors – 403 Not Found redirect
Mvc Error Page
Custom Errors – 404 Forbidden redirect
Mvc Error Page
Custom Errors – 500 Server Error redirect
Mvc Error Page
HTTP Errors – Mode
Custom
HTTP Errors – 404 Not Found redirect
Mvc Error Page
HTTP Errors – 500 Server Error redirect
Mvc Error Page
All other settings
Default values
If you try clicking on the various 500 and 404 error links, you will find a series of issues.
A redirect occurs for all the 500 error links. This is because the error response in each case is generated by Custom Errors, and Custom Errors has to be configured to redirect since the error page to show is an MVC error page. (Remember, MVC error pages cannot be used when using the Response Rewrite mode in Custom Errors.)
The Action method 404 exception, JSON action method 404 exception, GET for unknown action, POST for unknown action, Unknown controller, Views directory, Razor view file, Razor layout file, and Unknown image 404 links all redirect to the error page. Again, this is because the response is being generated by Custom Errors and an MVC error page needs to be shown.
The Zombie DOS link is not handled correctly. This can be easily dealt with by setting the relaxed URL mappings property, although a redirect still occurs if you do so.
The Controllers directory and Content directory links result in the basic HTTP Errors 403 error page, although this can be easily remedied by adding a mapping for the 403.14 status code.
The main problem here is the redirects, but there is a way around that: abandon Custom Errors and use HTTP Errors in Replace mode.
Experiment: Using Only HTTP Errors
HTTP Errors – Mode
Custom
HTTP Errors – Existing response
Replace
HTTP Errors – 404 Not Found redirect
Mvc Error Page
HTTP Errors – 403 Forbidden redirect
Mvc Error Page
HTTP Errors – 500 Server Error redirect
Mvc Error Page
Other Settings – Relaxed URL mappings
Relaxed
All other settings
Default values
You should find that all 500 and 404 error links are handled correctly with no redirect.
An important issue is how errors would get logged if you adopt the Magical Unicorn approach or the above amended approach. One thought is to log the errors in the error page’s action method, but this is not possible if you are using HTTP Errors to show your error pages since by that point you can no longer use Server.GetLastError to get hold of the exception that caused the error. There are two simple ways around this: log but do not handle the exception in a handler for the HttpApplication.Error event, or use
ELMAH
to log exceptions in the site.
The approach of using HTTP Errors in Replace mode and adding ELMAH for logging is interesting because of how simple it is to add to a site. What you lose is flexibility and support for versions of IIS besides IIS 7+:
This approach will not work with IIS versions 5 and 6, or with the Visual Studio Development Server (a.k.a. Cassini). This is not an issue if the site will be deployed to IIS 7+, and if you run the site locally using the Development Server during development then you simply get the useful YSODs.
This approach does not allow you to tailor a response to the request. For example, you may want to return JSON error responses for AJAX requests.
If the MVC error page to be shown throws an exception then a Custom Errors YSOD gets displayed; there is no way to fall back to using a custom static error page. This could be remedied by adding an HttpApplication.Error event handler that detects this situation and returns a static error page instead.
You lose the ability on production to hit the Web site locally and get YSODs for errors while still showing the custom error pages for failed remote requests. This issue unfortunately cannot be solved by setting the HTTP Error Mode to DetailedLocalOnly.
Finally
So far in this series I have covered the main error handling features available in an ASP.NET MVC 5 Web application and how they can be used to handle 500 errors and 404 errors. In the next and final post I will consider how these features can be combined to create robust error handling strategies.
i have played with your code today and correct me if i’m wrong but we cannot only use the HttpApplication.Error event handler alone. We must also use the httpErrors right to catch everything?
And about the App error handler response setting, in your MVC3 post, you say that you don’t really know which one is the best to use. What about MVC5? have you made your favorite chose? 🙂