Handling 404 Not Found in Asp.Net Core

You might be surprised to find that the default asp.net core mvc templates do not handle 404 errors gracefully resulting in the standard browser error screen when a page is not found. This posts looks at the various methods for handling 404 not found errors in asp.net core.

The Problem

Without additional configuration, this is what a (chrome) user will see if they visit a URL that does not exist:

Chrome Default 404 Screen

Fortunately, it is very simple to handle error status codes. We'll cover three techniques below.

The Solution

In previous versions of Asp.Net MVC, the primary place for handling 404 errors was in the web.config.

You probably remember the <customErrors> section which handled 404's from the ASP.NET pipeline as well as <httpErrors> which was lower level and handled IIS 404's. It was all a little confusing.

In .Net core, things are different and there is no need to play around with XML config (though you can still use httpErrors in web.config if you are proxying via IIS and you really want to :-)).

There are really two different situations that we need to handle when dealing with not-found errors.

There is the case where the URL doesn't match any route. In this situation, if we cannot ascertain what the user was looking for, we need to return a generic not found page. There are two common techniques for handling this but first we'll talk about the second situation. This is where the URL matches a route but one or more parameter is invalid. We can address this with a custom view.

Custom Views

An example for this case would be a product page with an invalid or expired id. Here, we know the user was looking for a product and instead of returning a generic error, we can be a bit more helpful and return a custom not found page for products. This still needs to return a 404 status code but we can make the page less generic, perhaps pointing the user at similar or popular products.

Handling these cases is trivial. All we need to do is set the status code before returning our custom view:

public async Task<IActionResult> GetProduct(int id)
{
    var viewModel = await _db.Get<Product,GetProductViewModel>(id);

    if (viewModel == null)
    {
        Response.StatusCode = 404;
        return View("ProductNotFound");
    }

    return View(viewModel);
}

Of course, you might prefer to wrap this up into a custom action result:

public class NotFoundViewResult : ViewResult
{
    public NotFoundViewResult(string viewName)            
    {
        ViewName = viewName;
        StatusCode = (int)HttpStatusCode.NotFound;
    }
}

This simplifies our action slightly:

public async Task<IActionResult> GetProduct(int id)
{
    var viewModel = await _db.Get<Product,GetProductViewModel>(id);

    if (viewModel == null)
    {
        return new NotFoundViewResult("ProductNotFound");
    }

    return View(viewModel);
}

This easy technique covers specific 404 pages. Let's now look at generic 404 errors where we cannot work out what the user was intending to view.

Catch-all route

Creating a catch-all route was possible in previous version of MVC and in .Net Core it works in exactly the same way. The idea is that you have a wildcard route that will pick up any URL that has not been handled by any other route. Using attribute routing, this is written as:

[Route("{*url}", Order = 999)]
public IActionResult CatchAll()
{
    Response.StatusCode = 404;
    return View();
}

It is important to specify the Order to ensure that the other routes take priority.

A catch-all route works reasonably well but it is not the preferred option in .Net Core. While a catch-all route will handle 404's, the next technique will handle any non-success status code so you can do the following (probably in an actionfilter in production):

public async Task<IActionResult> GetProduct(int id)
{
    ...

    if (RequiresThrottling())
    {
        return new StatusCodeResult(429)
    }

    if (!HasPermission(id))
    {
        return Forbid();
    }

    ...
}

Status Code Pages With Re Execute

StatusCodePagesWithReExecute is a clever piece of Middleware that handles non-success status codes where the response has not already started. This means that if you use the custom view technique detailed above then the 404 status code will not be handled by the middleware (which is exactly what we want).

When an error code such as a 404 is returned from an inner middleware component, StatusCodePagesWithReExecute allows you to execute a second controller action to handle the status code.

You add it to the pipeline with a single command in startup.cs:

app.UseStatusCodePagesWithReExecute("/error/{0}");
...
app.UseMvc();

The order of middleware definition is important and you need to ensure that StatusCodeWithReExecute is registered before any middleware that could return an error code (such as the MVC middleware).

You can specify a fixed path to execute or use a placeholder for the status code value as we have done above.

You can also point to both static pages (assuming that you have the StaticFiles middleware in place) and controller actions.

In this example, we have a separate action for 404. Any other non-success status code hits, the Error action.

[Route("error/404")]
public IActionResult Error404()
{
    return View();
}

[Route("error/{code:int}")]
public IActionResult Error(int code)
{
    // handle different codes or just return the default error view
    return View();
}

Obviously, you can tailor this to your needs. For example, if you are using request throttling as we showed in the previous section then you can return a 429 specific page explaining why the request failed.

Conclusion

Handling specific cases of page not found is best addressed with a custom view and setting the status code (either directly or via a custom action result).

Handling more generic 404 errors (or in fact any non-success status code) can be achieved very easily by using the StatusCodeWithReExecute middleware. Together, these two techniques are the preferred methods for handling non-success HTTP status codes in Asp.Net Core.

By adding StatusCodeWithReExecute to the pipeline as we have done above, it will run for all requests but this may not be what we want all of the time. In the next post we will look at how to handle projects containing both MVC and API actions where we want to respond differently to 404's for each type.

Useful or Interesting?

If you liked the article, I would really appreciate it if you could share it with your Twitter followers.

Share on Twitter

Comments

Be the first person to comment on this article.