Create Configurable Error Pages in ASP.NET Core

Create Configurable Error Pages in ASP.NET Core

Jun 27, 2016     Viewed 7083 times 0 Comments
Posted in #Configuration  #Diagnostics 

The ASP.NET Core came up with many middlewares that handle the exception nicely instead of YSOD, almost of them are available in Diagnostics Repository.

If we are looking to the previous versions of ASP.NET, we will notice that ASP.NET has a good way to create a configurable and customizable error pages via Web.Config as the following:

<configuration>
  <system.web>
    <compilation debug="true" />
    <customErrors mode="On" defaultRedirect="DefaultErrorPage.aspx">
      <error statusCode="401" redirect="Http401ErrorPage.aspx"/>
      <error statusCode="403" redirect="Http403ErrorPage.aspx"/>
      <error statusCode="404" redirect="Http404ErrorPage.aspx"/>
      <error statusCode="500" redirect="Http500ErrorPage.aspx"/>
    </customErrors>
  </system.web>
</configuration>

With that I decide to write a small blog post describing how can we achieve that in ASP.NET Core.

First of all we need to define the error pages in a file (JSON in our case) as the following:

"ErrorPages": {
  "401": "/Error/Http401ErrorPage",
  "403": "/Error/Http403ErrorPage",
  "404": "/Error/Http404ErrorPage",
  "500": "/Error/Http500ErrorPage"
}

after that we can fetch the values of status codes & paths easily with the help of Configuration APIs that provided by ASP.NET Core itself.

public IConfigurationRoot Configuration { get; }

internal static IDictionary ErrorPages { get; } = new Dictionary();

The Configuration property will hold the configuration data and the ErrorPages will contains the details of the error pages.

Then we need to read the data from the JSON file and write them into the declared properties.

var builder = new ConfigurationBuilder()
    .SetBasePath(env.ContentRootPath)
    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
    .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
    .AddEnvironmentVariables();

Configuration = builder.Build();

foreach (var c in Configuration.GetSection("ErrorPages").GetChildren())
{
    var key = Convert.ToInt32(c.Key);
    if (!ErrorPages.Keys.Contains(key))
    {
        ErrorPages.Add(key, c.Value);
    }
}

Now it's time to create a piece of middleware that handle the HTTP errors and map them into the predefined paths.

public class CustomErrorPagesMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    public CustomErrorPagesMiddleware(ILoggerFactory loggerFactory, RequestDelegate next)
    {
        _next = next;
        _logger = loggerFactory.CreateLogger();
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(0, ex, "An unhandled exception has occurred while executing the request");

            if (context.Response.HasStarted)
            {
                _logger.LogWarning("The response has already started, the error page middleware will not be executed.");
                throw;
            }
            try
            {
                context.Response.Clear();
                context.Response.StatusCode = 500;
                return;
            }
            catch (Exception ex2)
            {
                _logger.LogError(0, ex2, "An exception was thrown attempting to display the error page.");
            }
            throw;
        }
        finally
        {
            var statusCode = context.Response.StatusCode;

            if (Startup.ErrorPages.Keys.Contains(statusCode))
            {
                context.Request.Path = Startup.ErrorPages[statusCode];
                await _next(context);
            }
        }
    }
}

The code is straightforward it reads the StatusCode from the Response object and looking for it in the predefined codes and if there's a match it changes the Path of the Request object to the one in the JSON file. 

Last but not least we need write an extension to the IApplicationBuilder interface

public static class BuilderExtensions { public static IApplicationBuilder UseCustomErrorPages(this IApplicationBuilder app) { return app.UseMiddleware<CustomErrorPagesMiddleware>(); } }

Finally use it in the Configure method in the Startup class like this

app.UseCustomErrorPages();

You can download the source code for this post from my CustomErrorPages repository on GitHub.


Leave a Comment