139

Possible Duplicate:
How can I properly handle 404 in ASP.NET MVC?

I've made the changes outlined at 404 Http error handler in Asp.Net MVC (RC 5) and I'm still getting the standard 404 error page. Do I need to change something in IIS?

Community
  • 1
  • 1
Clearly
  • 1,624
  • 3
  • 11
  • 15
  • 1
    Here is a good read on this topic @ [How to handle 404 Not Found errors effectively with ASP.NET MVC 4](http://yassershaikh.com/how-to-handle-404-not-found-errors-effectively-with-asp-net-mvc-4/) – Yasser Shaikh Nov 03 '12 at 12:03

6 Answers6

380

I've investigated A LOT on how to properly manage 404s in MVC (specifically MVC3), and this, IMHO is the best solution I've come up with:

In global.asax:

public class MvcApplication : HttpApplication
{
    protected void Application_EndRequest()
    {
        if (Context.Response.StatusCode == 404)
        {
            Response.Clear();

            var rd = new RouteData();
            rd.DataTokens["area"] = "AreaName"; // In case controller is in another area
            rd.Values["controller"] = "Errors";
            rd.Values["action"] = "NotFound";

            IController c = new ErrorsController();
            c.Execute(new RequestContext(new HttpContextWrapper(Context), rd));
        }
    }
}

ErrorsController:

public sealed class ErrorsController : Controller
{
    public ActionResult NotFound()
    {
        ActionResult result;

        object model = Request.Url.PathAndQuery;

        if (!Request.IsAjaxRequest())
            result = View(model);
        else
            result = PartialView("_NotFound", model);

        return result;
    }
}

Edit:

If you're using IoC (e.g. AutoFac), you should create your controller using:

var rc = new RequestContext(new HttpContextWrapper(Context), rd);
var c = ControllerBuilder.Current.GetControllerFactory().CreateController(rc, "Errors");
c.Execute(rc);

Instead of

IController c = new ErrorsController();
c.Execute(new RequestContext(new HttpContextWrapper(Context), rd));

(Optional)

Explanation:

There are 6 scenarios that I can think of where an ASP.NET MVC3 apps can generate 404s.

Generated by ASP.NET:

  • Scenario 1: URL does not match a route in the route table.

Generated by ASP.NET MVC:

  • Scenario 2: URL matches a route, but specifies a controller that doesn't exist.

  • Scenario 3: URL matches a route, but specifies an action that doesn't exist.

Manually generated:

  • Scenario 4: An action returns an HttpNotFoundResult by using the method HttpNotFound().

  • Scenario 5: An action throws an HttpException with the status code 404.

  • Scenario 6: An actions manually modifies the Response.StatusCode property to 404.

Objectives

  • (A) Show a custom 404 error page to the user.

  • (B) Maintain the 404 status code on the client response (specially important for SEO).

  • (C) Send the response directly, without involving a 302 redirection.

Solution Attempt: Custom Errors

<system.web>
    <customErrors mode="On">
        <error statusCode="404" redirect="~/Errors/NotFound"/>
    </customErrors>
</system.web>

Problems with this solution:

  • Does not comply with objective (A) in scenarios (1), (4), (6).
  • Does not comply with objective (B) automatically. It must be programmed manually.
  • Does not comply with objective (C).

Solution Attempt: HTTP Errors

<system.webServer>
    <httpErrors errorMode="Custom">
        <remove statusCode="404"/>
        <error statusCode="404" path="App/Errors/NotFound" responseMode="ExecuteURL"/>
    </httpErrors>
</system.webServer>

Problems with this solution:

  • Only works on IIS 7+.
  • Does not comply with objective (A) in scenarios (2), (3), (5).
  • Does not comply with objective (B) automatically. It must be programmed manually.

Solution Attempt: HTTP Errors with Replace

<system.webServer>
    <httpErrors errorMode="Custom" existingResponse="Replace">
        <remove statusCode="404"/>
        <error statusCode="404" path="App/Errors/NotFound" responseMode="ExecuteURL"/>
    </httpErrors>
</system.webServer>

Problems with this solution:

  • Only works on IIS 7+.
  • Does not comply with objective (B) automatically. It must be programmed manually.
  • It obscures application level http exceptions. E.g. can't use customErrors section, System.Web.Mvc.HandleErrorAttribute, etc. It can't only show generic error pages.

Solution Attempt customErrors and HTTP Errors

<system.web>
    <customErrors mode="On">
        <error statusCode="404" redirect="~/Errors/NotFound"/>
    </customError>
</system.web>

and

<system.webServer>
    <httpErrors errorMode="Custom">
        <remove statusCode="404"/>
        <error statusCode="404" path="App/Errors/NotFound" responseMode="ExecuteURL"/>
    </httpErrors>
</system.webServer>

Problems with this solution:

  • Only works on IIS 7+.
  • Does not comply with objective (B) automatically. It must be programmed manually.
  • Does not comply with objective (C) in scenarios (2), (3), (5).

People that have troubled with this before even tried to create their own libraries (see http://aboutcode.net/2011/02/26/handling-not-found-with-asp-net-mvc3.html). But the previous solution seems to cover all the scenarios without the complexity of using an external library.

MiguelSlv
  • 14,067
  • 15
  • 102
  • 169
Marco
  • 5,555
  • 2
  • 17
  • 23
  • In conjunction with the HandleErrorAttribute that the MVC template sets up automatically when you create a new project, this is definitely the best and simplest solution. – s1mm0t Feb 08 '12 at 10:32
  • 9
    I like your analysis but not your solution :) issues with waiting until request end are that some of the request context has already been discarded such as session state. – Dale K Mar 04 '12 at 05:44
  • 1
    Response.Clear and MVC do not mix, do not use that approach http://blogs.msdn.com/b/rickandy/archive/2012/03/01/response-redirect-and-asp-net-mvc-do-not-mix.aspx – RickAndMSFT Mar 12 '12 at 15:14
  • 1
    If one cannot use Response.Clear, is there another way to clear what has been rendered already? If this is not done, then the response to the client will have the generic error plus the custom error. Thanks for the feedback anyway! – Marco Mar 12 '12 at 20:32
  • 6
    @RickAndMSFT That article doesn't mention Response.Clear, only Response.Redirect. – Tristan Warner-Smith Jul 18 '12 at 16:06
  • 10
    @RickAndMSFT: What is the problem with `Response.Clear`? Throwing in such a mysterious comment and blog post, concluding with "*do not use that approach*" and then keeping silent when you are asked for clarification is not helpful. – Slauma Oct 24 '12 at 18:04
  • Even moreso, it seems that I cannot use the Razor engine when implementing this. Upon c.Execute(...) I receive: The view 'NotFound' or its master was not found [...] ~/Views/Errors/NotFound.aspx [...] (only aspx/ascx files). – Cornelius Oct 30 '12 at 16:41
  • This is the best 404 solution I've found. Handles every 404 case with minimal code. – Rush Frisby Feb 07 '13 at 20:44
  • rd.DataTokens["area"] = "AreaName"; // In case controller is in another area. What is area? Never heard of it. – PussInBoots Jan 18 '14 at 13:26
  • 3
    Hi @PussInBoots. In MVC if your site gets too big (lots of Controllers, Modelds and Views), you can logically group them in Areas. For more information see http://msdn.microsoft.com/en-us/library/ee671793(v=vs.100).aspx – Marco Jan 21 '14 at 16:58
  • 4
    @Marco, your summarize was really awesome, probably helped huge amount of people! I do think a framework (ASP.NET/ASP.NET MVC) creates so many confusions and complexity to just handle 404/500 errors is insufferable! Comparing other web frameworks: Yii, Rails, Django, etc, this part is really a awful one in ASP.NET! – Wayne Ye Feb 07 '14 at 06:20
  • 3
    This is a great post, and this is the best solution provided, and it covers all the details in the most common mvc setup. Just wanted to add that if you have `runAllManagedModulesForAllRequests="false"`, which you should have if you care about performance at all, this solution doesn't work for static files because they will (correctly) not go through the asp.net pipeline. Also, for all errors (not just 404s), it's better to use static files than routes, and have web.config properties as backup, so that it still works for errors in asp.net startup like routing config. – wired_in Feb 24 '14 at 02:54
  • 1
    Also, #3 in your other options is actually great for people using IIS7+ that dont care about granular control over errors (can still use ELMAH). #2 in the list of problems with it is only valid if you use a route. If you use a static file, which is what it should be, it does maintain the error status code. #3 problem is only valid if u need the error info in the view, or if u need to show different error pages for each controller/action. "Can't use customErrors section" doesn't matter because one of the main objectives is to maintain the error status code, which you can't do with customErrors. – wired_in Feb 24 '14 at 06:40
  • 1
    I have implemented this solution but I have the same problem as s1mm0t, I can't access Session state in any of the actions inside the ErrorsController. How would I go about solving that? – hakksor May 27 '14 at 10:15
  • 2
    Using MVC 5.1 and going to a undefined controller ({"The controller for path '/nonexistant/someaction' was not found or does not implement IController."}), the response status code in EndRequest is still 200, even though the server eventually returns the default 404 IIS page with an 404 status. This is highly disturbing, any ideas? – Alex Jun 23 '14 at 04:23
  • 3
    Your solution won't catch 404 errors that involve file extensions (ex. website.com/doesnt/exist.html) because the managed pipeline doesn't get invoked for those for performance reasons. – Slight Jun 24 '15 at 19:28
  • For long Urls should add some codes in RouteConfig: http://stackoverflow.com/questions/20366170/asp-net-mvc-5-http-error-404-0-not-found-with-long-non-existing-url – Omid.Hanjani Feb 04 '16 at 17:49
  • 1
    I'm not clear, is the solution shown at the top compatible with all of your objectives? If so, what was the config settings you ultimately used in web config in tandem with this code? – AaronLS Apr 21 '16 at 20:17
  • @Slight unless you have `runAllManagedModulesForAllRequests="true"`, right ? –  Jul 09 '16 at 17:54
  • 3
    Hi i am getting status code 200 with this. Please see. – It's a trap Dec 30 '16 at 16:44
  • 2
    This solution is incompatible with Autofac or any other IoC that has it's lifetime scope disposed in `Application_EndRequest`. – Rudey May 17 '17 at 12:52
  • @RuudLenders: Thank you for the comment (and suggested edit). I have updated the answer to reflect it. – Marco May 18 '17 at 15:27
  • If you want to use Marco's code to catch all files as well (like .HTML, .HTML, etc) just add this in your web.config: – Stokely Jun 29 '17 at 12:09
  • Nice post. I have been battling with this problem many times and I always end up using a similar approach (handling stuff in Global.asax.cs instead of web.config). The web.config-approach will bit you in the back one way or another :) – uggeh Feb 21 '19 at 09:16
146

Yet another solution.

Add ErrorControllers or static page to with 404 error information.

Modify your web.config (in case of controller).

<system.web>
    <customErrors mode="On" >
       <error statusCode="404" redirect="~/Errors/Error404" />
    </customErrors>
</system.web>

Or in case of static page

<system.web>
    <customErrors mode="On" >
        <error statusCode="404" redirect="~/Static404.html" />
    </customErrors>
</system.web>

This will handle both missed routes and missed actions.

EvilDr
  • 8,943
  • 14
  • 73
  • 133
Mike Chaliy
  • 25,801
  • 18
  • 67
  • 105
  • 2
    Nice! :) ErrorsController could inherit from the same base as all other controllers and thus have access to a certain functionality. Moreover, Error404 view could be wrapped in master providing the user with overall look and feel of the rest of the site without any extra work. – Dimskiy Jan 27 '11 at 16:47
  • 7
    Use `` to see actual error page during development. – Jakub Konecki Mar 12 '12 at 09:51
  • 1
    This is correct. Don't call Response.Clear(); as suggested by Mike Chaliy see http://blogs.msdn.com/b/rickandy/archive/2012/03/01/response-redirect-and-asp-net-mvc-do-not-mix.aspx – RickAndMSFT Mar 12 '12 at 15:12
  • 6
    It doesn't work if `return HttpNotFound();` returned as `ActionResult` from a controller action is used. – Slauma Oct 24 '12 at 17:25
  • 2
    This solution doesn't work. I get the default 404 error, for modes "On" and "RemoteOnly". – Gleno Oct 10 '13 at 20:23
  • 1
    Perfect, a simple solution! Note that `customErrors` must be placed within the `system.web` tag, see this answer for reference: http://stackoverflow.com/a/16254472/1298685 . This handles incorrect matched URL's, but for unmatched URL's also implement something like this in `RouteConfig.cs` : `routes.MapRoute( name: "Error", url: "{*url}", defaults: new { controller = "Error", action = "PageNotFound", id = UrlParameter.Optional });` – Ian Campbell Dec 23 '13 at 06:27
  • 4
    This results in a 302 redirect so you aren't preserving your original HTTP code. ASP.NET sure makes it more difficult than it needs to be to have control over the HTTP stack. – Justin Helgerson Apr 18 '14 at 17:04
  • Helped me to also look at the web.config documentation for httpErrors: http://www.iis.net/configreference/system.webserver/httperrors – sobelito Jan 18 '15 at 01:00
  • 1
    Suggestion: include the parent elements of your configuration so readers can see this goes inside `` – Myster Sep 30 '15 at 20:02
  • Necro comment: DO NOT USE redirectMode="ResponseRewrite" in your CustomError when not using a static page. – Atron Seige Nov 11 '15 at 14:03
  • `The configuration section 'customErrors' cannot be read because it is missing a section declaration` – tchelidze Aug 22 '16 at 07:44
5

The response from Marco is the BEST solution. I needed to control my error handling, and I mean really CONTROL it. Of course, I have extended the solution a little and created a full error management system that manages everything. I have also read about this solution in other blogs and it seems very acceptable by most of the advanced developers.

Here is the final code that I am using:

protected void Application_EndRequest()
    {
        if (Context.Response.StatusCode == 404)
        {
            var exception = Server.GetLastError();
            var httpException = exception as HttpException;
            Response.Clear();
            Server.ClearError();
            var routeData = new RouteData();
            routeData.Values["controller"] = "ErrorManager";
            routeData.Values["action"] = "Fire404Error";
            routeData.Values["exception"] = exception;
            Response.StatusCode = 500;

            if (httpException != null)
            {
                Response.StatusCode = httpException.GetHttpCode();
                switch (Response.StatusCode)
                {
                    case 404:
                        routeData.Values["action"] = "Fire404Error";
                        break;
                }
            }
            // Avoid IIS7 getting in the middle
            Response.TrySkipIisCustomErrors = true;
            IController errormanagerController = new ErrorManagerController();
            HttpContextWrapper wrapper = new HttpContextWrapper(Context);
            var rc = new RequestContext(wrapper, routeData);
            errormanagerController.Execute(rc);
        }
    }

and inside my ErrorManagerController :

        public void Fire404Error(HttpException exception)
    {
        //you can place any other error handling code here
        throw new PageNotFoundException("page or resource");
    }

Now, in my Action, I am throwing a Custom Exception that I have created. And my Controller is inheriting from a custom Controller Based class that I have created. The Custom Base Controller was created to override error handling. Here is my custom Base Controller class:

public class MyBasePageController : Controller
{
    protected override void OnException(ExceptionContext filterContext)
    {
        filterContext.GetType();
        filterContext.ExceptionHandled = true;
        this.View("ErrorManager", filterContext).ExecuteResult(this.ControllerContext);
        base.OnException(filterContext);
    }
}

The "ErrorManager" in the above code is just a view that is using a Model based on ExceptionContext

My solution works perfectly and I am able to handle ANY error on my website and display different messages based on ANY exception type.

Yousi
  • 811
  • 3
  • 12
  • 26
  • 3
    I don't agree with you that it's the BEST solution. A common task such as this one should not be this complicated to setup. Marcos answer is great but you really dont wan't that much code for simple things. – PussInBoots Jan 18 '14 at 14:16
4

Looks like this is the best way to catch everything.

How can I properly handle 404 in ASP.NET MVC?

Community
  • 1
  • 1
Clearly
  • 1,624
  • 3
  • 11
  • 15
1

In IIS, you can specify a redirect to "certain" page based on error code. In you example, you can configure 404 - > Your customized 404 error page.

J.W.
  • 17,991
  • 7
  • 43
  • 76
1

What I can recomend is to look on FilterAttribute. For example MVC already has HandleErrorAttribute. You can customize it to handle only 404. Reply if you are interesed I will look example.

BTW

Solution(with last route) that you have accepted in previous question does not work in much of the situations. Second solution with HandleUnknownAction will work but require to make this change in each controller or to have single base controller.

My choice is a solution with HandleUnknownAction.

Donny V.
  • 22,248
  • 13
  • 65
  • 79
Mike Chaliy
  • 25,801
  • 18
  • 67
  • 105
  • It looks like the problem is that the standard default route of "{controller}/{action}/{id}" catches everythgin so it doesn't get to the last route. I thought that if a controller could not be found that the next route would be evaluated. – Clearly Apr 04 '09 at 19:48
  • HandleUnknownAction only works with Actions that are not foudn. What if a route is amtched but a resulting controller can not be found? What si the best way to handle that? – Clearly Apr 04 '09 at 19:52
  • Yes this is correct, only when action is not found. You can try to combine both solutions. HandleUnknownAction for missed actions and route for missed controllers. Other possible solution is custom RouteHandler. – Mike Chaliy Apr 04 '09 at 19:55
  • Hm, RouteHandler is out of the scope. Sorry, you will not be able to do this with custom ReouteHandler. – Mike Chaliy Apr 04 '09 at 19:57