40

I am using the middle-ware below to set up error pages for HTTP status codes 400 to 599. So visiting /error/400 shows a 400 Bad Request error page.

application.UseStatusCodePagesWithReExecute("/error/{0}");

[Route("[controller]")]
public class ErrorController : Controller
{
    [HttpGet("{statusCode}")]
    public IActionResult Error(int statusCode)
    {
        this.Response.StatusCode = statusCode;
        return this.View(statusCode);
    }
}

However, visiting /this-page-does-not-exist results in a generic IIS 404 Not Found error page.

Is there a way to handle requests that do not match any routes? How can I handle this type of request before IIS takes over? Ideally I would like to forward the request to /error/404 so that my error controller can handle it.

In ASP.NET 4.6 MVC 5, we had to use the httpErrors section in the Web.config file to do this.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <httpErrors errorMode="Custom" existingResponse="Replace">
      <remove statusCode="404" />
      <error statusCode="404" responseMode="ExecuteURL" path="/error/404/" />
    </httpErrors>
  </system.webServer>
</configuration>
Muhammad Rehan Saeed
  • 35,627
  • 39
  • 202
  • 311
  • You can use global.asax and `protected void Application_Error(object sender, EventArgs e)` to catch all errors, then redirect from there. You'll need to check if it's an IsAjaxRequest but otherwise you can redirect based on the exception – freedomn-m Jul 24 '15 at 10:13
  • 1
    This is ASP.NET 5 Beta 7 MVC 6. There is not Global.asax. – Muhammad Rehan Saeed Jul 24 '15 at 10:25
  • 1
    Really? Are you sure? Is that like you "*had* to use" httpErrors when you didn't *have* to? There is no global.asax by default, just add one - http://stackoverflow.com/questions/24718640/mvc-6-with-vnext-do-we-still-need-the-global-asax – freedomn-m Jul 24 '15 at 10:46
  • I'm looking for a built in ASP.NET 5 method of doing this if there is one. ASP.NET 5 still requires a web.config file (to enable GZip compression etc.) so I'd rather use that than Global.asax. – Muhammad Rehan Saeed Jul 24 '15 at 11:28
  • 1
    found this related question but not sure it really answers the question http://stackoverflow.com/questions/29421164/mvc-6-404-not-found – Joe Audette Jul 24 '15 at 13:14
  • @JoeAudette Yes, that question is similar. Mine is specifically trying to get a custom 404 when navigating to ```/this-page-does-not-exist```. Too bad that question does not have an answer to this either. – Muhammad Rehan Saeed Jul 24 '15 at 13:40

5 Answers5

25

One of the best tutorials I found is this: https://joonasw.net/view/custom-error-pages

The summary is here:

1. First add a controller like ErrorController then add this action to it:

[Route("404")]
public IActionResult PageNotFound()
{
    string originalPath = "unknown";
    if (HttpContext.Items.ContainsKey("originalPath"))
    {
        originalPath = HttpContext.Items["originalPath"] as string;
    }
    return View();
}

Note: You can add the action to another existing controller like HomeController.

2. Now add the PageNotFound.cshtml view. Something like this:

@{
    ViewBag.Title = "404";
}

<h1>404 - Page not found</h1>

<p>Oops, better check that URL.</p>

3. And the important part is here. Add this code to Startup class, inside Configure method:

app.Use(async (ctx, next) =>
{
    await next();

    if(ctx.Response.StatusCode == 404 && !ctx.Response.HasStarted)
    {
        //Re-execute the request so the user gets the error page
        string originalPath = ctx.Request.Path.Value;
        ctx.Items["originalPath"] = originalPath;
        ctx.Request.Path = "/error/404";
        await next();
    }
});

Note that it must be added before routing configs like app.UseEndpoints....

Mahdi Ataollahi
  • 4,544
  • 4
  • 30
  • 38
9

Based on this SO item, IIS gets the 404 (and therefore handles it) before it reaches UseStatusCodePagesWithReExecute.

Have you tried this: https://github.com/aspnet/Diagnostics/issues/144? It suggests terminating the request that received a 404 so it does not go to IIS to handle. And here's the code to add to your Startup to do so:

app.Run(context =>
{
   context.Response.StatusCode = 404;
   return Task.FromResult(0);
});

This appears to be an IIS-only issue.

Community
  • 1
  • 1
Frank Fajardo
  • 7,034
  • 1
  • 29
  • 47
  • This works but when I try to use ```application.UseStatusCodePagesWithReExecute("/error/{0}");``` in conjunction with this code I get 404 errors all the time. Is there some way of combining the two? – Muhammad Rehan Saeed Jul 29 '15 at 10:28
  • Hi @MuhammadRehanSaeed, based on [this](http://www.codemag.com/Article/1501081), `App.Run()` is a catchall interface that fires when nothing else in the call chain handles a request. So if 404 is return for every request, there might be something else wrong with your `Startup.cs`. The code above should be at the end of your Startup. – Frank Fajardo Jul 30 '15 at 01:27
  • 1
    @MuhammadRehanSaeed, the above code works on a test I did, with `UseStatusCodePagesWithReExecute` using the same `ErrorController` you have. On my Startup, I have these in sequence: `app.UseStatusCodePagesWithReExecute()`, `app.UseMvc()`, the above `app.Run()` block, although changing the order of the first two does not matter. It returns the 404 custom error page only on non-existent resource. – Frank Fajardo Jul 30 '15 at 04:23
  • Thanks Frank, I have discovered that UseStatusCodePagesWithReExecute MUST come before UseMvc for it to work. – Hoots Nov 13 '17 at 17:26
  • When I try this I get `Web server failed to listen on port xxxx` – GuidoG Dec 15 '22 at 08:37
8

You can use fallback in EndPoint in asp.net core Like below (inside app.UseEndpoints) and with razor pages (NotFound is a razor page in Pages folder instead controller)

 endpoints.MapRazorPages();
            
 endpoints.MapFallback( context => {
    context.Response.Redirect("/NotFound");
    return Task.CompletedTask;
  });
Shojaeddin
  • 1,851
  • 1
  • 18
  • 16
  • 2
    `/NotFound` is not `Not Found`, but exists as a resource. If the resource indicated in the original request URI does not exist, the direct response should contain a 404 status and not found content, rather than a redirect. – 93196.93 Oct 14 '22 at 05:39
3

After working on few hours for handling 500 and 404 errors, I have implemented below given solution.

For handling 500 Server Side Errors you can use app.UseExceptionHandler middleware but app.UseExceptionHandler middleware only handles unhandled exceptions while 404 isn't an exception. For handling 404 errors I've designed another custom middleware which is checking response status code and if it is 404 then returning user to my custom 404 error page.

if (env.IsDevelopment())
   {
       app.UseDeveloperExceptionPage();
   }
   else
   {
       //Hnadle unhandled exceptions 500 erros
       app.UseExceptionHandler("/Pages500");
       //Handle 404 erros
       app.Use(async (ctx, next) =>
       {
           await next();
           if (ctx.Response.StatusCode == 404 && !ctx.Response.HasStarted)
           {
               //Re-execute the request so the user gets the error page
               ctx.Request.Path = "/Pages404";
               await next();
           }
       });
   }

Note: You must add app.UseExceptionHandler("/Pages500"); middleware at the starting of your Configure method so that it can handle exceptions from all middlewares. The custom middleware can be placed any ware before app.UseEndpoins middleware but still, it's good to place in the beginning of Configure method. /Pages500 and /Pages404 urls are my custom pages, you can design for your application.

habib
  • 2,366
  • 5
  • 25
  • 41
2

Asp.net core 3.1 and 5

in your HomeController.cs :

public class HomeController : Controller
{
   [Route("/NotFound")]
   public IActionResult PageNotFound()
   {
      return View();
   }
}

in Startup.cs > ConfigureServices method:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
   app.Use(async (context, next) =>
   {
      await next();
      if (context.Response.StatusCode == 404)
      {
         context.Request.Path = "/NotFound";
         await next();
      }
   });
   
   app.UseHttpsRedirection();
   app.UseStaticFiles();
}