Strategies for resource-based 404 errors in ASP.NET MVC

There are already a couple of patterns for handling 404 errors in ASP.NET MVC.

  • For invalid routes, you can add a catch-all {*url} route to match “anything else” that couldn’t be handled by any other route.
  • For invalid controllers and controller actions, you can implement your own IControllerFactory with inbuilt error handling. Nicholas Smit describes one here.

But what happens when a user invokes a valid action on a valid controller, and the requested resource — product, article, sub-category etc — doesn’t exist? Let’s investigate some of the options available.

Classic ASP.NET custom errors: easy, but not that great

The easiest option is to use ASP.NET’s custom errors feature, redirecting users to special pages according to the status code of the exception that was caught.

All you need to do is to uncomment the following section from your web.config, and create a new file called FileNotFound.htm:

<customErrors mode="On">
  <error statusCode="404" redirect="FileNotFound.htm" />
</customErrors>

Then you can start throwing 404 HttpExceptions from your action method.

// Get information about a product.
public ActionResult Detail(int? id)
{
    // Look up the product by ID.
    Product product = products.FirstOrDefault(p => p.Id == id);

    // If it wasn't found, throw a 404 HttpException.
    if (product == null)
        throw new HttpException(404, "Product not found.");

    return View(product);
}

When a user requests the detail for a non-existent product, here’s what they’ll see with a static FileNotFound.htm 404 page (style copied from the application’s MasterPage):

Custom Error redirection page

This method is very easy to implement but has a few disadvantages — particularly from the user’s point of view.

  • The redirection and error page return HTTP status codes 302 Found and 200 OK respectively. While most humans might not notice the difference between 302 Found and 404 Not Found, search engines certainly appreciate the distinction when indexing content. Additionally, under REST principles, redirecting to a different path is not an appropriate response when an particular resource cannot be found. The server should simply return a 404 not found status code.
  • The path to the requested page is passed externally, and the user is redirected to a traditional .html or .aspx page. This looks noisy and unprofessional when used alongside ASP.NET MVC’s elegant paths. The path redirection also makes it difficult for users to retype and correct badly-entered URLs.
  • These error pages are handled outside the ASP.NET MVC framework. ViewData, Models and Filters are not available — data access, logging and master pages will be implemented differently to the rest of your web site.

While they do arguably work, ASP.NET custom errors are clearly not the best tool for the job.

ASP.NET MVC HandleErrorAttribute: getting warmer

The next option is the ASP.NET MVC framework’s built in HandleErrorAttribute, which renders a special error view when uncaught exceptions are thrown from a controller action.

// My custom exception type for 404 errors. 
public class ResourceNotFoundException : Exception {}
// Render the ResourceNotFound view when a ResourceNotFoundException is thrown.
[HandleError(ExceptionType = typeof(ResourceNotFoundException),
    View = "ResourceNotFound")] 
public class ProductsController : Controller
{
    // Get information about a product.
    public ActionResult Detail(int? id)
    {
        // Look up the product by ID.
        Product product = products.FirstOrDefault(p => p.Id == id);
            
        // If it wasn't found, throw a ResourceNotFoundException.
        if (product == null)
            throw new ResourceNotFoundException();

        return View(product);
    }
}

To achieve this, I defined my own ResourceNotFoundException class, and decorated my controller class with a HandleErrorAttribute. Actually you would probably decorate it twice — one for all exceptions, and one specifically for the ResourceNotFoundException type.

Anyway, in this example, when an action throws a ResourceNotFoundException, the HandleErrorAttribute filter will catch it and render a view called ResourceNotFound. If you don’t specify a view, HandleErrorAttribute will look for a default view called Error. I have chosen to use a different one, because I don’t want 404 errors reported in the same way as general application errors.

This renders better results — no URL redirection, and the code is contained within the ASP.NET MVC framework. However, there’s still a problem.

ASP.NET MVC HandleErrorAttribute view

As you can see, the invalid resource request is coming back as a 500 Server Error. This isn’t by any flaw of the HandleErrorAttribute class; it’s simply designed to handle application errors, not 404s. In fact, it will explicitly ignore any exceptions that have an HTTP status code other than 500.

While it works pretty well, ASP.NET MVC’s HandleErrorAttribute isn’t really suited as a 404 handler.

Custom IExceptionAttribute

HandleErrorAttribute is a filter — a feature of the ASP.NET MVC framework that allows you to execute before or after a controller action is called, or when an exception is thrown. They’re great for checking authentication, logging page requests, exception handling and other cross-cutting concerns.

Here’s a filter I’ve created — it’s effectively a clone of the built in HandleErrorAttribute, except that it ignores everything except ResourceNotFoundExceptions, and it sets the HTTP status code to 404 instead of 500.

public class HandleResourceNotFoundAttribute : FilterAttribute, IExceptionFilter
{
    public void OnException(ExceptionContext filterContext)
    {
        Controller controller = filterContext.Controller as Controller;
        if (controller == null || filterContext.ExceptionHandled)
            return;

        Exception exception = filterContext.Exception;
        if (exception == null)
            return;

        // Action method exceptions will be wrapped in a
        // TargetInvocationException since they're invoked using 
        // reflection, so we have to unwrap it.
        if (exception is TargetInvocationException)
            exception = exception.InnerException;

        // If this is not a ResourceNotFoundException error, ignore it.
        if (!(exception is ResourceNotFoundException))
            return;

        filterContext.Result = new ViewResult()
        {
            TempData = controller.TempData,
            ViewName = View
        };

        filterContext.ExceptionHandled = true;
        filterContext.HttpContext.Response.Clear();
        filterContext.HttpContext.Response.StatusCode = 404;
    }
}

As with HandleErrorAttribute, you simply decorate the controller class with a HandleResourceNotFound attribute.

// Render the ResourceNotFound view when a ResourceNotFoundException is thrown.
[HandleResourceNotFound] 
public class ProductsController : Controller
{
    // Get information about a product.
    public ActionResult Detail(int? id)
    {
        // Look up the product by ID.
        Product product = products.FirstOrDefault(p => p.Id == id);
            
        // If it wasn't found, throw a ResourceNotFoundException.
        if (product == null)
            throw new ResourceNotFoundException();

        return View(product);
    }
}

This filter produces very similar output to that of the HandleErrorAttribute, but with the correct 404 HTTP status code. This is exactly what we want — an easy-to-generate 404 status code, raised and presented within the ASP.NET MVC framework.

ASP.NET MVC HandleErrorAttribute view

In part 2 of this article, I’ll discuss methods for improving the quality of these error pages, and why a single generic 404 error page isn’t sufficient for a modern, rich web application.

Download the code here: 404TestApplication-beta1.zip (ASP.NET MVC Framework Beta 1)

Updated:

  • October 8 2008 – updated for ASP.NET MVC Preview 5.
  • November 23 2008 – updated for ASP.NET MVC Beta 1.
August 17, 2008

20 Comments

Andy on August 18, 2008 at 10:03 am.

How about an extension method, that can be applied to any returned object via LINQ. Similar to the GetOr404() type ORMapper APIs that are used.

public static T IsResourceFound(this T o)
{
if (o == null)
throw new HttpException(404, “Resource not found.”);

return (T)o;
}

Richard on August 18, 2008 at 1:17 pm.

Andy: that’s certainly a possibility (I didn’t really explore that side of things in my article). Perhaps you could create a FirstOrThrow<TException>() extension method?

takepara on September 16, 2008 at 9:26 pm.

Great solution!!

liggett78 on October 10, 2008 at 8:21 pm.

Why not just set the response code and render a special “NotFound” view?
Like

Response.StatusCode = 404;
return View(“NotFound”);

Richard on October 10, 2008 at 11:29 pm.

liggett78: you certainly could do that, and it might satisfy your requirements. However, separating the 404 handler out lets you do other cool AOP stuff like logging 404 errors for particular missing products for example.

PK on November 22, 2008 at 1:07 pm.

Richard, what is ‘View’ variable in this:

filterContext.Result = new ViewResult()
{
TempData = controller.TempData,
ViewName = View
};

i don’t get it *blush*

Richard on November 22, 2008 at 2:51 pm.

PK: My HandleResourceNotFoundAttribute class has a string property called View where you can specify which view to be displayed e.g. [HandleResourceNotFound(View = "My404Page")]. Sorry that was indeed confusing – I missed it from my code example!

Tomorrow I will update the code for beta 1 and upload it so you can see the whole thing in action yourself.

PK on November 22, 2008 at 3:56 pm.

Awesome. Before i posted my comment, i just hard coded in my “ResourceNotFound” view name, but i prefer your way because it’s not tighly coupling it.

Also looking forward to the beta 1 refresh. So far, i’m LOVING this MVC stuff :) (being using it since Preview 2 :)

Richard on November 22, 2008 at 6:18 pm.

PK: File uploaded, see the bottom of the article :)

PK on November 23, 2008 at 1:23 am.

Cheers :)

mctip on February 14, 2009 at 11:12 am.

Richard, is there a way to pass the invalid URL to the ResourceNotFound viewdata?

Richard on February 14, 2009 at 4:20 pm.

@mctip sure: in the HandleResourceNotFoundAttribute.OnException() method, where it sets up filterContext.Result. Something like:

        ViewDataDictionary viewData = controller.ViewData;
        viewData.Add("Url", filterContext.HttpContext.Request.Url);

        filterContext.Result = new ViewResult()
        {
            TempData = controller.TempData,
            ViewName = View,
            ViewData = viewData
        };

AndrewO on April 5, 2009 at 10:39 pm.

Hello Richard!
Goods solution from one point, but don’t you think that throwing Exception increase server workload?
Just think, what if someone calls your not found url 100 000 times…

So, I think liggett78 right and his solution is best.

Richard on April 6, 2009 at 8:38 am.

@AndrewO: I think the benefit exceptions bring to your code are more important than the performance hit of using them (see http://www.yoda.arachsys.com/csharp/exceptions2.html). If it is a big problem though (e.g. someone is DDoS’ing your site), you might try something like limiting requests if someone tries to access a non-existent resource 100 times or more.

AndrewO on April 12, 2009 at 10:29 pm.

Richard, thanks for your answer. I have some more questions.
In my webapplication I need:
1) as less code to perform errors as possible
2) write error description to log
3) return different result views for different errors:
3.1) error 404
– show 404 page for page request
– show just 404 status for images, css and js-files with no content
3.2) error 401
3.3) error 500
3.4) etc…

So, what do you think, if I:
1) set in web.config (otherwise Application_Error doesn’t fire in Global.asax)
2) put this code in Application_Error

protected void Application_Error(object sender, EventArgs e)
{
Exception exception = Server.GetLastError();
HttpException httpException = exception as HttpException;
if (httpException != null)
{
RouteData routeData = new RouteData();
routeData.Values.Add(“controller”, “Error”);
routeData.Values.Add(“action”, “HttpError500″);
if (httpException != null)
{
if (httpException.GetHttpCode() == 404)
{
routeData.Values["action"] = “HttpError404″;
}
}
Server.ClearError();
Response.Clear();
IController errorController = new ErrorController();
errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
}

}
3) create ErrorController
public ActionResult HttpError500()
{
return View(“Error”);
}
public ActionResult HttpError404()
{
Response.StatusCode = 404;
return View(“Error404″);
}

As you can see, I:
1) doesn’t need to set any [HandleError] attributes
2) have one point to route exceptions
3) doesn’t have problems with rule
routes.MapRoute(“Default”,
“{controller}/{action}/{id}”,
new { controller = “Home”, action = “Index”, id = “” });
4) can write to log when error occurred

Richard on April 16, 2009 at 11:27 am.

@AndrewO: there is one problem – Application_Error has no access to the current controller context – you cannot easily tell which Controller/Action is being executed, which is desired for nice ASP.NET MVC error logging :)

Leave a Reply