It may surprise you to learn that the built-in asp.net core mvc templates do not gracefully handle 404 errors, which causes the typical browser error screen to appear when a page cannot be found. This article examines the different approaches to 404 not found errors in ASP.NET Core.
Issue
If a (chrome) user visits a URL that is not present, they will see this without any additional configuration:
How to Fix
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.
When it comes to not-found errors, there are essentially two scenarios that we must manage.
Sometimes the URL doesn’t correspond with any route. In this case, we must return a generic not found page if we are unable to determine what the user was trying to find. There are two standard methods for dealing with this, but let’s start with the second scenario. In this case, a route is matched by the URL, but one or more parameters are missing. We can use a custom view to solve this.
Custom Views
A product page with an invalid or expired ID would be an example in this situation. Here, we can be a little more helpful and return a custom not found page for products since we know the user was looking for a product, rather than just a generic error. Although a 404 status code must still be returned, we can make the page less generic by maybe directing the user to related or well-liked products.
These cases are easy to handle. Setting the status code is all that is required before we can return our customized 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);
}
Naturally, you may want to combine this into a result of a custom action:
public class NotFoundViewResult : ViewResult
{
public NotFoundViewResult(string viewName)
{
ViewName = viewName;
StatusCode = (int)HttpStatusCode.NotFound;
}
}
This makes our action a little simpler:
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 simple method addresses certain 404 pages. Now let’s examine generic 404 errors, which occur when we are unable to determine what the user intended to view.
Catch-all route
In earlier versions of MVC, it was possible to create a catch-all route, and in.Net Core, this function is identical. The concept is that any URL that hasn’t been handled by another route will be picked up by your wildcard route. This is expressed using attribute routing as follows:
[Route("{*url}", Order = 999)]
public IActionResult CatchAll()
{
Response.StatusCode = 404;
return View();
}
To guarantee that the other routes take precedence, it is crucial to specify the Order.
Although a catch-all route functions fairly well, it is not the recommended choice in.Net Core. The following method, which is likely to be used in a production actionfilter, will handle any non-success status code while a catch-all route will handle 404s:
public async Task<IActionResult> GetProduct(int id)
{
...
if (RequiresThrottling())
{
return new StatusCodeResult(429)
}
if (!HasPermission(id))
{
return Forbid();
}
...
}
Status Code Pages With Re Execute
A smart bit of middleware called StatusCodePagesWithReExecute deals with non-success status codes in cases where the response hasn’t begun yet. This means that the middleware won’t handle the 404 status code if you use the custom view method described above—exactly what we want.
StatusCodePagesWithReExecute enables you to run a second controller action in response to an error code (like a404) returned by an inner middleware component.
At startup, you use a single command to add it to the pipeline.cs:
app.UseStatusCodePagesWithReExecute("/error/{0}");
...
app.UseMvc();
The registration order of middleware is crucial, and you should make sure that StatusCodeWithReExecute is registered before any middleware (like the MVC middleware) that might return an error code.
As we did above, you can use a placeholder for the status code value or specify a fixed path to execute.
Additionally, you can point to controller actions as well as static pages (provided that the StaticFiles middleware is installed).
For 404, we have a different action in this example. If another non-success status code is encountered, the Error action is triggered.
[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();
}
You can obviously modify this to suit your needs. For instance, you can return a 429 specific page indicating the reason behind the request failure if you are utilizing request throttling, as we demonstrated in the preceding section.
Conclusion
It is best to use a custom view and set the status code (either directly or through a custom action result) when handling specific cases of page not found.
Using the StatusCodeWithReExecute middleware, handling more general 404 errors—or really, any non-success status code—is quite simple. The best practices for handling non-successful HTTP status codes in Asp.Net Core are these two strategies combined.
The pipeline will run for all requests if we add StatusCodeWithReExecute, as we have done above, but this may not always be what we want. We’ll examine how to manage projects that include both MVC and API actions in the upcoming post, where we want to handle 404 errors differently for each kind of request.