By default, the ASP.NET Core templates include either the ExceptionHandlerMiddleware or the DeveloperExceptionPage. Both of these catch exceptions thrown by the middleware pipeline, but they don't handle error status codes that are returned by the pipeline (without throwing an exception). For that, there is the StatusCodePagesMiddleware.

There are a number of ways to use the StatusCodePagesMiddleware but in this post I will be focusing on the version that re-executes the pipeline.

Default Status Code Pages

I'll start with the default MVC template, but I'll add a helper method for returning a 500 error:

public class HomeController : Controller  
{
    public IActionResult Problem()
    {
        return StatusCode(500);
    }  
}

To start with, I'll just add the default StatusCodePagesMiddleware implementation:

public void Configure(IApplicationBuilder app)  
{
    app.UseDeveloperExceptionPage();

    app.UseStatusCodePages();

    app.UseStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

With this in place, making a request to an unknown URL gives the following response:

404 status code pages

The default StatusCodePagesMiddleware implementation will return the simple text response when it detects a status code between 400 and 599. Similarly, if you make a request to /Home/Problem, invoking the helper action method, then the 500 status code text is returned.

500 status code pages

Re-execute vs Redirect

In reality, it's unlikely you'll want to use status code pages with this default setting in anything but a development environment. If you want to intercept status codes in production and return custom error pages, you'll want to use one of the alternative extension methods that use redirects or pipeline re-execution to return a user-friendly page:

  • UseStatusCodePagesWithRedirects
  • UseStatusCodePagesWithReExecute

These two methods have a similar outcome, in that they allow you to generate user-friendly custom error pages when an error occurs on the server. Personally, I would suggest always using the re-execute extension method rather than redirects.

The problem with redirects for error pages is that they somewhat abuse the return codes of HTTP, even though the end result for a user is essentially the same. With the redirect method, when an error occurs the pipeline will return a 302 response to the user, with a redirect to a provided error path. This will cause a second response to be made to the the URL that is used to generate the custom error page, which would then return a 200 OK code for the second request:

Re directing the pipeline

Semantically this isn't really correct, as you're triggering a second response, and ultimately returning a success code when an error actually occurred. This could also cause issues for SEO. By re-executing the pipeline you keep the correct (error) status code, you just return user-friendly HTML with it.

Re-executing a pipeline

You are still in the context of the initial response, but the whole pipeline after the StatusCodePagesMiddleware is executed for a second time. The content generated by this second response is combined with the original Status Code to generate the final response that gets sent to the user. This provides a workflow that is overall more semantically correct, and means you don't completely lose the context of the original request.

Adding re-execute to your pipeline

Hopefully you're swayed by the re-execte approach; luckily it's easy to add this capability to your middleware pipeline. I'll start by updating the Startup class to use the re-execute extension instead of the basic one.

public void Configure(IApplicationBuilder app)  
{
    app.UseDeveloperExceptionPage();

    app.UseStatusCodePagesWithReExecute("/Home/Error", "?statusCode={0}");

    app.UseStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

Note, the order of middleware in the pipeline is important. The StatusCodePagesMiddleware should be one of the earliest middleware in the pipeline, as it can only modify the response of middleware that comes after it in the pipeline

There are two arguments to the UseStatusCodePagesWithReExecute method. The first is a path that will be used to re-execute the request in the pipeline and the second is a querystring that will be used.

Both of these paths can include a placeholder {0} which will be replaced with the status code integer (e.g. 404, 500 etc) when the pipeline is re-executed. This allows you to either execute different action methods depending on the error that occurred, or to have a single method that can handle multiple errors.

The following example takes the latter approach, using a single action method to handle all the error status codes, but with special cases for 404 and 500 errors provided in the querystring:

public class HomeController : Controller  
{
    public IActionResult Error(int? statusCode = null)
    {
        if (statusCode.HasValue &&
        {
            if (statusCode == 404 || statusCode == 500)
            {
                var viewName = statusCode.ToString();
                return View(viewName);
            }
        }
        return View();
    }
}

When a 404 is generated (by an unknown path for example) the status code middleware catches it, and re-executes the pipeline using /Home/Error?StatusCode=404. The Error action is invoked, and executes the 404.cshtml template:

404 error

Similarly, a 500 error is special cased:

500 error

Any other error executes the default Error.cshtml template:

Standard error

Summary

Congratulations, you now have custom error pages in your ASP.NET Core application. This post shows how simple it is to achieve by re-executing the pipeline. I strongly recommend you use this approach instead of trying to use the redirects overload. In the next post, I'll show how you can obtain the original URL that triggered the error code during the second pipeline execution.