ASP.NET MVC 404 Error Handling [duplicate]
Asked Answered
P

6

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?

Pericles answered 4/4, 2009 at 19:13 Comment(1)
Here is a good read on this topic @ How to handle 404 Not Found errors effectively with ASP.NET MVC 4Marva
S
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.

Smocking answered 4/4, 2009 at 20:4 Comment(11)
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.Gastrulation
Use <customErrors mode="RemoteOnly"> to see actual error page during development.Rossner
This is correct. Don't call Response.Clear(); as suggested by Mike Chaliy see blogs.msdn.com/b/rickandy/archive/2012/03/01/…Plafker
It doesn't work if return HttpNotFound(); returned as ActionResult from a controller action is used.Tourbillion
This solution doesn't work. I get the default 404 error, for modes "On" and "RemoteOnly".Caucasoid
Perfect, a simple solution! Note that customErrors must be placed within the system.web tag, see this answer for reference: https://mcmap.net/q/168068/-where-does-lt-customerrors-gt-in-web-config-go-for-mvc-applications . 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 });Eunaeunice
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.Attentive
Helped me to also look at the web.config documentation for httpErrors: iis.net/configreference/system.webserver/httperrorsDissatisfactory
Suggestion: include the parent elements of your configuration so readers can see this goes inside <configuration><system.web>Liveryman
Necro comment: DO NOT USE redirectMode="ResponseRewrite" in your CustomError when not using a static page.Logician
The configuration section 'customErrors' cannot be read because it is missing a section declarationTocology
C
381

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.

Conservationist answered 26/1, 2012 at 23:16 Comment(24)
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.Lithe
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.Innermost
Response.Clear and MVC do not mix, do not use that approach blogs.msdn.com/b/rickandy/archive/2012/03/01/…Plafker
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!Conservationist
@Plafker That article doesn't mention Response.Clear, only Response.Redirect.Mciver
@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.Tourbillion
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).Highroad
This is the best 404 solution I've found. Handles every 404 case with minimal code.Kra
rd.DataTokens["area"] = "AreaName"; // In case controller is in another area. What is area? Never heard of it.Brominate
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 msdn.microsoft.com/en-us/library/ee671793(v=vs.100).aspxConservationist
@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!Tralee
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.Worley
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.Worley
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?Babe
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?Octan
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.Pappano
For long Urls should add some codes in RouteConfig: #20366670Purdah
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?Tactical
@Pappano unless you have runAllManagedModulesForAllRequests="true", right ?Pincer
Hi i am getting status code 200 with this. Please see.Peduncle
This solution is incompatible with Autofac or any other IoC that has it's lifetime scope disposed in Application_EndRequest.Underline
@RuudLenders: Thank you for the comment (and suggested edit). I have updated the answer to reflect it.Conservationist
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: <system.webServer> <handlers> <add name="AllFilesHandler" path="." verb="GET" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" /> </handlers> </system.webServer>Boot
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 :)Broccoli
S
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.

Smocking answered 4/4, 2009 at 20:4 Comment(11)
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.Gastrulation
Use <customErrors mode="RemoteOnly"> to see actual error page during development.Rossner
This is correct. Don't call Response.Clear(); as suggested by Mike Chaliy see blogs.msdn.com/b/rickandy/archive/2012/03/01/…Plafker
It doesn't work if return HttpNotFound(); returned as ActionResult from a controller action is used.Tourbillion
This solution doesn't work. I get the default 404 error, for modes "On" and "RemoteOnly".Caucasoid
Perfect, a simple solution! Note that customErrors must be placed within the system.web tag, see this answer for reference: https://mcmap.net/q/168068/-where-does-lt-customerrors-gt-in-web-config-go-for-mvc-applications . 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 });Eunaeunice
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.Attentive
Helped me to also look at the web.config documentation for httpErrors: iis.net/configreference/system.webserver/httperrorsDissatisfactory
Suggestion: include the parent elements of your configuration so readers can see this goes inside <configuration><system.web>Liveryman
Necro comment: DO NOT USE redirectMode="ResponseRewrite" in your CustomError when not using a static page.Logician
The configuration section 'customErrors' cannot be read because it is missing a section declarationTocology
L
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.

Leid answered 11/4, 2012 at 13:3 Comment(1)
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.Brominate
P
4

Looks like this is the best way to catch everything.

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

Pericles answered 4/4, 2009 at 20:2 Comment(0)
P
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.

Poop answered 4/4, 2009 at 19:33 Comment(0)
S
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.

Smocking answered 4/4, 2009 at 19:43 Comment(4)
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.Pericles
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?Pericles
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.Smocking
Hm, RouteHandler is out of the scope. Sorry, you will not be able to do this with custom ReouteHandler.Smocking

© 2022 - 2024 — McMap. All rights reserved.