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

Spread the love

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.

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

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.

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.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *