HTTP semantics might not be vital to us humans, but they are the essential glue that holds the internet together. HTTP status codes allow clients to interpret the response they are about to receive and make processing accommodations. We may all be familiar with 200 OK, but there is a stable of other status codes that can indicate redirects, missing content, and catastrophic events.

In this post, we’ll see how we can use a middleware to process status codes respectfully so that both humans and our web clients get the necessary information necessary to take the next step.

Error Page

When we create a new Razor Pages project, we immediately have an Error page created. Let’s take a look at the implementation.

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class ErrorModel : PageModel
{
    public string RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    private readonly ILogger<ErrorModel> _logger;

    public ErrorModel(ILogger<ErrorModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
    }
}

When we run into the error, we see the following page.

error page in razor pages

Let’s say we go to edit a resource, only to find it does not exist. Most applications will return a 404 (Not Found) result. Let’s see how to do that in our Razor Pages OnGet method.

// the route is /{id:int}
public IActionResult OnGet(int id)
{
    var result = GetResource(id);
    if (result == null)
    {
        return NotFound();
    }

    return Page();
}

There is a problem. When we run our application, we get the default browser error page.

empty 404 page

What’s the deal?

StatusCodePagesMiddleware

Well, it turns out we need an additional middleware known as the StatusCodePagesMiddleware. This middleware gives us the ability to either redirect or execute a route in our web application. Let’s take a look at the internal implementation of this middleware.

var statusCodeFeature = new StatusCodePagesFeature();
context.Features.Set<IStatusCodePagesFeature>(statusCodeFeature);

await _next(context);

if (!statusCodeFeature.Enabled)
{
    // Check if the feature is still available because other middleware (such as a web API written in MVC) could
    // have disabled the feature to prevent HTML status code responses from showing up to an API client.
    return;
}

// Do nothing if a response body has already been provided.
if (context.Response.HasStarted
    || context.Response.StatusCode < 400
    || context.Response.StatusCode >= 600
    || context.Response.ContentLength.HasValue
    || !string.IsNullOrEmpty(context.Response.ContentType))
{
    return;
}

var statusCodeContext = new StatusCodeContext(context, _options, _next);
await _options.HandleAsync(statusCodeContext);

We can see that any status code above 400 and below 600 will cause the middleware to jump into action. Additionally, we can turn the feature off for any reason. In the code comments, its mentioned that it might be essential to turn this feature off for API based responses.

To add this middleware, we need to call UseStatusCodePagesWithReExecutein our Startup.Configure method.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    // *** Here is our middleware registration ***
    app.UseStatusCodePagesWithReExecute("/Error");

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); });
}

Now, when we run our previous NotFound request, we’ll see our error page.

not found page reexecuted

Take note, to preserve the URI but still return a response to the client, we want to use ReExecute and not Redirect.

Conclusion

While Razor Pages allows our OnGet and OnPost methods to be void, we should consider HTTP semantics and always return an IActionResult. The different return type allows us to handle error modes more gracefully, and leverage our pre-packaged Error page. The attention to detail should keep both humans and our robotic overlords happy.

I hope you found this post helpful, and please leave a comment.