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

19

446

I am using RC2

Using URL Routing:

routes.MapRoute(
    "Error",
     "{*url}",
     new { controller = "Errors", action = "NotFound" }  // 404s
);

The above seems to take care of requests like this (assuming default route tables setup by initial MVC project): "/blah/blah/blah/blah"

Overriding HandleUnknownAction() in the controller itself:

// 404s - handle here (bad action requested
protected override void HandleUnknownAction(string actionName) {
    ViewData["actionName"] = actionName;
    View("NotFound").ExecuteResult(this.ControllerContext);
}  

However the previous strategies do not handle a request to a Bad/Unknown controller. For example, I do not have a "/IDoNotExist", if I request this I get the generic 404 page from the web server and not my 404 if I use routing + override.

So finally, my question is: Is there any way to catch this type of request using a route or something else in the MVC framework itself?

OR should I just default to using Web.Config customErrors as my 404 handler and forget all this? I assume if I go with customErrors I'll have to store the generic 404 page outside of /Views due to the Web.Config restrictions on direct access.

Jollify answered 6/3, 2009 at 18:21 Comment(4)
it's 404 error, i would just not bother about it. let it display 404. as definitely user mistyped something. or if it is something thats moved then your application should take that request and do redirect permanent. 404 belongs to webserver not application. you can always customize iis pages for error.Lindbergh
you can have a look at this solution as well blog.dantup.com/2009/04/…Impudent
ben.onfabrik.com/posts/aspnet-mvc-custom-error-pages also has some good informationSonyasoo
It's a shame that 4 stable releases later and more than 5 years on, the situation for handling 404s in asp.net MVC + IIS hasn't really improved and this is still the go to Q&A for how to handle it.Wilden
L
279

The code is taken from http://blogs.microsoft.co.il/blogs/shay/archive/2009/03/06/real-world-error-hadnling-in-asp-net-mvc-rc2.aspx and works in ASP.net MVC 1.0 as well

Here's how I handle http exceptions:

protected void Application_Error(object sender, EventArgs e)
{
   Exception exception = Server.GetLastError();
   // Log the exception.

   ILogger logger = Container.Resolve<ILogger>();
   logger.Error(exception);

   Response.Clear();

   HttpException httpException = exception as HttpException;

   RouteData routeData = new RouteData();
   routeData.Values.Add("controller", "Error");

   if (httpException == null)
   {
       routeData.Values.Add("action", "Index");
   }
   else //It's an Http Exception, Let's handle it.
   {
       switch (httpException.GetHttpCode())
       {
          case 404:
              // Page not found.
              routeData.Values.Add("action", "HttpError404");
              break;
          case 500:
              // Server error.
              routeData.Values.Add("action", "HttpError500");
              break;

           // Here you can handle Views to other error codes.
           // I choose a General error template  
           default:
              routeData.Values.Add("action", "General");
              break;
      }
  }           

  // Pass exception details to the target error View.
  routeData.Values.Add("error", exception);

  // Clear the error on server.
  Server.ClearError();

  // Avoid IIS7 getting in the middle
  Response.TrySkipIisCustomErrors = true; 

  // Call target Controller and pass the routeData.
  IController errorController = new ErrorController();
  errorController.Execute(new RequestContext(    
       new HttpContextWrapper(Context), routeData));
}
Lucinalucinda answered 6/3, 2009 at 21:49 Comment(14)
I'd like to add that this is fairly easy to write unit tests for the above code as well. I only had to make a few small changes. +1Harty
I'm not sure I understand why you're checking for an httpException. isn't the point of this handler to catch - for instance - a NullReferenceException and then GENERATE an http exception. when would Server.GetLastError() ever actually be an HttpException unless you had made an Http call within your actual business logic. i removed this from my implementation but would love to know if it is justified and i missed somethingLandel
update: checking for an http 404 is definitely required, but i'm still not quite sure when you'd ever get a 500. also you need to also explicitly set the Response.StatusCode = 404 or 500 otherwise google will start indexing these pages if you are returning a 200 status code which this code currently doesLandel
@Simon_Weaver: agreed! This needs to return 404 status codes. It's essentially broken as a 404 solution until it does. Check this: codinghorror.com/blog/2007/03/…Maestricht
There is an underlying flaw with this whole suggestion - by the time execution has bubbled up to the Global.asax, too much of the HttpContext is missing. You can not route back into your controllers like the example suggests. Refer to the comments in the blog link at top.Maestricht
As some of the comments from above and the linked post mention, this doesn't appear to work. The errorcontroller is hit, but a blank screen returns. (using mvc 3)Falco
something doesn't feel right, the whole purpose of MVC is to remove all that abstraction and yet here it is again...Salpingectomy
I've updated the code with a workaround to prevent blank screens for invalid requests. Essentially, the mechanism of calling Execute on the error controller is invalid if the original request failed request validation (the subsequent call to Execute will fail for the same reason). This way, I have just done a straight redirect to the action on the error controller, preventing request validation being invoked a second time. Hopefully this helps some people who had issues with blank screens.Rusk
@AlexanderN Agreed. That is exactly why my solution was birthed.Maestricht
If you clink on the link in this post you will notice that the code is difficult to read because it's using a right-to-left language setting. Hint: To make it easier to read open a DOM inspector (e.g. in Chrome hit Ctrl+Shft+I) then find an element in the code and disable the CSS element "direction:rtl" and that will make it much easier to read.Fosdick
If you return a HttpNotFound() from a controller, the Application_Error event will never get triggered. In a default MVC 5 app, if you scaffold a controller using Entity Framework, by default HttpNotFound() is generated in your code. So I don't think the ASP.NET MVC team would recommend using this.Rattail
This only has the potential to be a solution for requests in which Application_Error actually runs on error, meaning requests that A) run through the ASP.NET pipeline, and B) don't run into errors very early in the asp.net lifecycle, like if the Web.config isn't valid, or an error in setting up routes. This question is about 404's but the solution includes other errors, so beware this is far from a complete solution. Also, because you are using a controller and view rather than static files, even if it does run there are several ways the error handling code could produce an error.Precede
Regarding just 404 handling, the specific issue with just using this is when you use runAllManagedModulesForAllRequests="false" which really only makes sense when it comes to performance. You don't want all of your static file requests (.css, .js) going through the full asp.net pipeline every request, which is the only way you could trap 404's with this solution. So this solution could work for all requests that run through asp.net, but not for all of the others that you don't want to, like static files.Precede
This part is missing with this answer #347781Bigner
M
258

Requirements for 404

The following are my requirements for a 404 solution and below i show how i implement it:

  • I want to handle matched routes with bad actions
  • I want to handle matched routes with bad controllers
  • I want to handle un-matched routes (arbitrary urls that my app can't understand) - i don't want these bubbling up to the Global.asax or IIS because then i can't redirect back into my MVC app properly
  • I want a way to handle in the same manner as above, custom 404s - like when an ID is submitted for an object that does not exist (maybe deleted)
  • I want all my 404s to return an MVC view (not a static page) to which i can pump more data later if necessary (good 404 designs) and they must return the HTTP 404 status code

Solution

I think you should save Application_Error in the Global.asax for higher things, like unhandled exceptions and logging (like Shay Jacoby's answer shows) but not 404 handling. This is why my suggestion keeps the 404 stuff out of the Global.asax file.

Step 1: Have a common place for 404-error logic

This is a good idea for maintainability. Use an ErrorController so that future improvements to your well designed 404 page can adapt easily. Also, make sure your response has the 404 code!

public class ErrorController : MyController
{
    #region Http404

    public ActionResult Http404(string url)
    {
        Response.StatusCode = (int)HttpStatusCode.NotFound;
        var model = new NotFoundViewModel();
        // If the url is relative ('NotFound' route) then replace with Requested path
        model.RequestedUrl = Request.Url.OriginalString.Contains(url) & Request.Url.OriginalString != url ?
            Request.Url.OriginalString : url;
        // Dont get the user stuck in a 'retry loop' by
        // allowing the Referrer to be the same as the Request
        model.ReferrerUrl = Request.UrlReferrer != null &&
            Request.UrlReferrer.OriginalString != model.RequestedUrl ?
            Request.UrlReferrer.OriginalString : null;

        // TODO: insert ILogger here

        return View("NotFound", model);
    }
    public class NotFoundViewModel
    {
        public string RequestedUrl { get; set; }
        public string ReferrerUrl { get; set; }
    }

    #endregion
}

Step 2: Use a base Controller class so you can easily invoke your custom 404 action and wire up HandleUnknownAction

404s in ASP.NET MVC need to be caught at a number of places. The first is HandleUnknownAction.

The InvokeHttp404 method creates a common place for re-routing to the ErrorController and our new Http404 action. Think DRY!

public abstract class MyController : Controller
{
    #region Http404 handling

    protected override void HandleUnknownAction(string actionName)
    {
        // If controller is ErrorController dont 'nest' exceptions
        if (this.GetType() != typeof(ErrorController))
            this.InvokeHttp404(HttpContext);
    }

    public ActionResult InvokeHttp404(HttpContextBase httpContext)
    {
        IController errorController = ObjectFactory.GetInstance<ErrorController>();
        var errorRoute = new RouteData();
        errorRoute.Values.Add("controller", "Error");
        errorRoute.Values.Add("action", "Http404");
        errorRoute.Values.Add("url", httpContext.Request.Url.OriginalString);
        errorController.Execute(new RequestContext(
             httpContext, errorRoute));

        return new EmptyResult();
    }

    #endregion
}

Step 3: Use Dependency Injection in your Controller Factory and wire up 404 HttpExceptions

Like so (it doesn't have to be StructureMap):

MVC1.0 example:

public class StructureMapControllerFactory : DefaultControllerFactory
{
    protected override IController GetControllerInstance(Type controllerType)
    {
        try
        {
            if (controllerType == null)
                return base.GetControllerInstance(controllerType);
        }
        catch (HttpException ex)
        {
            if (ex.GetHttpCode() == (int)HttpStatusCode.NotFound)
            {
                IController errorController = ObjectFactory.GetInstance<ErrorController>();
                ((ErrorController)errorController).InvokeHttp404(RequestContext.HttpContext);

                return errorController;
            }
            else
                throw ex;
        }

        return ObjectFactory.GetInstance(controllerType) as Controller;
    }
}

MVC2.0 example:

    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
        try
        {
            if (controllerType == null)
                return base.GetControllerInstance(requestContext, controllerType);
        }
        catch (HttpException ex)
        {
            if (ex.GetHttpCode() == 404)
            {
                IController errorController = ObjectFactory.GetInstance<ErrorController>();
                ((ErrorController)errorController).InvokeHttp404(requestContext.HttpContext);

                return errorController;
            }
            else
                throw ex;
        }

        return ObjectFactory.GetInstance(controllerType) as Controller;
    }

I think its better to catch errors closer to where they originate. This is why i prefer the above to the Application_Error handler.

This is the second place to catch 404s.

Step 4: Add a NotFound route to Global.asax for urls that fail to be parsed into your app

This route should point to our Http404 action. Notice the url param will be a relative url because the routing engine is stripping the domain part here? That is why we have all that conditional url logic in Step 1.

        routes.MapRoute("NotFound", "{*url}", 
            new { controller = "Error", action = "Http404" });

This is the third and final place to catch 404s in an MVC app that you don't invoke yourself. If you don't catch unmatched routes here then MVC will pass the problem up to ASP.NET (Global.asax) and you don't really want that in this situation.

Step 5: Finally, invoke 404s when your app can't find something

Like when a bad ID is submitted to my Loans controller (derives from MyController):

    //
    // GET: /Detail/ID

    public ActionResult Detail(int ID)
    {
        Loan loan = this._svc.GetLoans().WithID(ID);
        if (loan == null)
            return this.InvokeHttp404(HttpContext);
        else
            return View(loan);
    }

It would be nice if all this could be hooked up in fewer places with less code but i think this solution is more maintainable, more testable and fairly pragmatic.

Thanks for the feedback so far. I'd love to get more.

NOTE: This has been edited significantly from my original answer but the purpose/requirements are the same - this is why i have not added a new answer

Maestricht answered 6/3, 2009 at 18:21 Comment(62)
Dude - i really really like this :) I used to throw Http404Exceptions, etc and handled that in a OnError attribute type thingy .. but this damn good, too :)Asa
@cottsak buddy, with your StructureMap IoC code .. when (or how) can an HttpException get thrown? A simple example, please? And secondly, why does your code in Step #5 invoke404 and then return EmptyResult??Asa
@Krome: #1- "The controller for path '/dfsdf' could not be found or it does not implement IController." is the .Message from requesting the path '/dfsdf' - i guess when a route is not matched, MVC throws a HttpException like this. #2- Step 5 is maybe not an ideal usage example but for now it seems to be ok for me. I need to return the EmptyResult as part of the action. The fact that it's "empty" makes no difference as in the InvokeHttp404 method i re-route again. help?Maestricht
@cottsak, I really like this too. One odd thing happens for me when executing step 4: the Http404 action gets executed twice on the same request, once when the controller factory calls InvokeHttp404 and once when HandleUnknownAction calls InvokeHttp404. In my example, the non-existant URL is localhost:64434/jakds. HandleUnknownAction gets the default action named "index" as a parameter. I think I have to change the route data rather than call InvokeHttp404 in the controller factory.Daisy
@flipdoubt: In the case where you have a url that matches a route and parses a controller name and it's bad, then with my code, you will have the Http404 action invoked twice. As far as i can see there is nothing i can do about that as the internals of the routing engine dictate the checking 1st on the controller and then 2nd on the action.Maestricht
@flipdoubt: I've made some changes but it still does not remove the two runs through the Http404 action for the "bad controller" case. I'm going to leave it like it is. This is not ideal but it does not affect the actual response to the client so i think it's ok for now.Maestricht
Wow, great writeup. This is definitely a better approach than a single Application_Error handler, especially if you have custom controller factories and RESTful services.Eisele
Thanks for the full write-up. One addition is that when running under IIS7 you need to add set the property "TrySkipIisCustomErrors" to true. Otherwise IIS will still return the default 404 page. We added Response.TrySkipIiisCustomErrors=true; after the line in Step 5 that sets the status code. msdn.microsoft.com/en-us/library/…Sincere
Pretty robust and DRY, maybe I am lacking much exp, but I don't see the problem in handling the 404 (Error) in the Global.asax, that seems pretty DRY and KISS to me.Bruxelles
Thanks, helped me. However it doesn't seem to catch a direct url to a valid .aspx file, eg localhost:50096/views/home/index.aspx (when that path/file exists) still throws the default 404. Any ideas how to catch this one?Rotl
@Seba: I'd suspect that it's not catching that url because MVC routing normally ignores valid static paths to valid static resources - eg: the aspx files, images and css in content/script folders. So i guess in the internals of the routing engine this url is ignored under the assumption that you are referencing that view for some other purpose than normal routing (because, as you know, you normally access views via a route, not directly). Anything that can be parsed by your routes as a "route" would be caught by this 404 handling solution, which is by design.Maestricht
Do you use the customErrors handling from ASP.NET at all for this solution> Or has the customErrors setting become obsolete for MVC?Falco
@Ryan The customErrors section of the web.config defines static redirect pages that are handled at a high level in aspnet if not IIS. This is not what i wanted as i needed to have the result be MVC Views (so i can have data in them etc). I wouldn't say categorically that "customErrors is obsolete in MVC" but for me and this 404 solution they certainly are.Maestricht
I am getting the following error An object reference is required for the non-static field, method, or property 'System.Web.Routing.RequestContext.HttpContext.get' on the line ((ErrorController)errorController).InvokeHttp404(RequestContext.HttpContext);. Any ideas? I am using MVC 2.Axiomatic
@Dave K solution worked for me.Axiomatic
@cottsak How do you remove the 404 handling in the Application_Error method? Application_Error still picks up the 404 handling if I use a path of /adsfaAxiomatic
@Mike In response to your first question i believe the issue is a subtle MVC2 change. I have now updated the answer. Refer to the line you mentioned- you want .InvokeHttp404(requestContext.HttpContext) instead of .InvokeHttp404(RequestContext.HttpContext). Hope this helpsMaestricht
@Mike In answer to your second question about Application_Error - i'm not sure what you mean. I have a MVC2 project in front of me with this 404 implementation and i have a breakpoint in Application_Error and i get no execution in there for urls like you mentioned.Maestricht
Also, can someone update the Step 3 so StructureMap isn't used? Maybe just a generic ControllerFactory that will be easy to implement if you aren't already using a ControllerFactory.Vastah
@David Are you finding it does not work with MVC3 or you haven't tried yet?Maestricht
@cottsak: I got it to work once I figured out the controller factory. I just didn't know if there were any new methods in MVC3 that would be "preferred".Vastah
@David Ok. I try to add version-specific stuff as soon as i find it. As far as i know the MVC2 example works fine for MVC3.Maestricht
This works fine for MVC3. I switched ObjectFactory.GetInstance to MVC3's DependencyResolver.Current.GetService instead so it's more generic. I am using Ninject.Mcalister
A couple questions: Your Loans controller derives from MyController, correct? And could we see a Ninject variation? I'm using vanilla Ninject, but am a complete newbie to it and DI in general, so I'm not 100% sure how to implement step 3.Arvonio
@kevinmajor1 Yes, the Loans controller derives from MyController (answer updated). Regarding Ninject: you could use @subkamran's idea if you have MVC3 for a generic resolver. Or perhaps substitute the ObjectFactory.GetInstance lines for new StandardKernel().Get (i think)?Maestricht
Awesome. Would there be a way to expand this to handle 500 errors, too? Or would 404s suffice in most situations?Arvonio
Hmm... having a problem with your solution. Please take a look at: https://mcmap.net/q/81671/-asp-net-mvc-2-route-errors-problems-when-using-cottsak-39-s-solution/399584Arvonio
@kevinmajor1 500s are a different thing all together but you could take some of the principals from this example and apply them to your 500 solution if you like.Maestricht
Ah shit man...I already started my project without a base controller. Gone too many controllers too, is there a way to do this?Godthaab
Just update all of the controllers to derive from your base instead of Controller. Shouldn't take long.Maestricht
@cottsak I am following Step 1 and Step 3 and tried to test localhost/<somegrbagepath> and I got exception on line Response.StatusCode = (int)HttpStatusCode.NotFound; because Response is null. Any idea why ? I am using MVC3. ThanksFraught
@cottsak why it is bad idea to handle 404 in Application_ErrorSmiga
@Muhammad have a look at the Requirements up the top.Maestricht
@cottask i had gone through the requirements but i can't understand why its difficult to go back to mvc app when you are rendering custom view both on 404 and other errors like 500. why do we need to handle 404's differentlySmiga
@Muhammad I don't think it's possible to redirect back into the MVC app from event handlers in Global.asax. I don't think it's part of the Request cycle. Do you know a way?Maestricht
@cottasak no not at the moment but will look into it in free time. thanks for help thoughSmiga
How do you test a solution that use HttpContext?Xanthate
@Xanthate There are plenty of ways to fake your HttpContext for testing..Maestricht
What is the method ObjectFactory.GetInstance() ? VS 2010 can't find it.Indusium
@Alex It's the DI/IoC framework StructureMap. You can use your own DI/IoC here.Maestricht
great answer, does anyone know how to succesfully unit test this though? as i am having a nightmare trying to mock the httpcontext to get this working in testsImminent
Mocking HttpContext is always a challenge. Some suggest that we shouldn't test the framework and perhaps that includes MVC - so maybe you will have more luck testing parts of this solution that are less directly coupled to the MVC core? (this is a pretty left-field suggestion I know)Maestricht
There is one thing this great solution doesn't seems to be covering; RouteConstraint. In the case that you want to handle error logging (for 500) and proper 404 handling you'll need a condition in the "Application_Error" method to check wether its a 404 or 500. MVC is throwing an application error with a 404 status code when a route doesn't conform to constraint. Any ideas on how to handle this at an higher level?Analects
Not yet. But it's important to remember that this is a 404 solution, not a general exception/error/top-level handler solution. We're not interested in capturing exceptions in Application_Error for the purposes of this solution, just 404s. Feel free to expand and handle in Application_Error tho. I'd be interested in the implementation.Maestricht
I've read that NotFoundMvc takes care of most of these separate headaches, as stated above. I'll try it in my next project!Papyrus
This is good coverage of the different exceptions. I wonder if it catches IIS 404's for missing folders? I ended up using an error handling module similar to helephant.com/2009/02/11/… because it does catch IIS folder missing 404s - at least in IIS7 - as well as every other error on the site. It also provides a single point to catch everything and pass control to the error controller.Girlfriend
Does anyone else find it patently insane that such a common thing as 404's in a web framework is so bloody complicated.Joannejoannes
It also appears I get the IIS 404 page for any bad url ending in ".cshtml"Joannejoannes
How can you unit test step 5: "when your app can't find something"? For example, when (loan == null) how can i ensure that the code is going to return the correct Status Code > the problem lies in the fact that the InvokeHttp404 returns an EmptyResult > hence an empty response.Jointless
Hi JTech. The System.Web.Mvc namespace now has a HttpNotFoundResult type which should replace my suggestion in step 5. For example, in MVC 4 (and 3?) you can simply return HttpNotFound();. Capturing that in a unit test should be easier.Maestricht
If I use IController errorController = DependencyResolver.Current.GetService<ErrorController>(); instead of IController errorController = ObjectFactory.GetInstance<ErrorController>(); - will this affect anything? I am using MVC 5. Also what is all this ObjectFactory.GetInstance, DependencyResolver.Current.GetService stuff? Can't you just create the ErrorController like this? IController errorController = new ErrorController();?Rattail
And can someone please explain what step 3 does and what it is for? Is it for when I do this in one of my actionmethods? throw new HttpException((int)HttpStatusCode.NotFound, "Not found")? Do I need this in conjunctionn with InvokeHttp404 in my BaseController? I really don't see why I need step 3 code in my app, please help.Rattail
@Rattail this is pretty old now and there are many easier ways of doing this in MVC 4 and newer.Maestricht
Hi cottsak. That was my thought as well before starting digging but almost every 404 page on the net leads to here in one way or another and since none of the post in this thread has an ultimate answer I think people still going to use yours for the most part since it has most votes. Also, I'm curious about these easier ways, do you have a link of some sort?Rattail
@Rattail feel free to use the other suggestions below. I've look at some of them and they're very helpful.Maestricht
Yes but you said new ways. All the posts in this thread are old. I thought you were referring to a specific post/article.Rattail
@cottsak I'd be very interested in how this answer would differ for mvc5, if there are actual improvements to be made. This is a popular topic with many 'solutions' but they all seem to have their own caveats.Corrianne
This is insane. All this to return a 404 error for a resource that does not exist. This cannot be true. Microdoft, take a look at apache and PHP. It returns 404 error out of the box.Teodor
This solution won't handle urls with extensions because they don't defaultly go through the managed pipeline no matter what your route config looks like. So anything like "website.com/a/page.html" won't get handled. Also, I recommend just using Application_EndRequest and check the status code. It's much simpler and the whole "can't enter MVC again" nonsense, has no merit to begin with.Debbydebee
There is only one point that I didt not understant. Why did you use MyController class as a base class. Is there realy need ? The dependency framework creates always new object of class of ErrorController so we are here currently creating two objects of ErrorController class. What is the reason ?Agnusago
@KewinRather This is a very old post and is certainly not what I would start with today in a MVC5 or .NET Core project. Please bear that in mind. Yes, 404s were WAY too hard back in the earlier versions of ASP.NET MVC. Other answers here might be more useful to you.Maestricht
Come on! There should be a better (simpler) way to do that. Also, your solution introduces very specific application architecture for a general problem.Dissentious
O
241

ASP.NET MVC doesn't support custom 404 pages very well. Custom controller factory, catch-all route, base controller class with HandleUnknownAction - argh!

IIS custom error pages are better alternative so far:

web.config

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

ErrorController

public class ErrorController : Controller
{
    public ActionResult PageNotFound()
    {
        Response.StatusCode = 404;
        return View();
    }
}

Sample Project

Odie answered 31/3, 2011 at 22:27 Comment(15)
THIS SHOULD HAVE BEEN THE ACCEPTED ANSWER!!! Works excellent on ASP.NET MVC 3 with IIS Express.Rent
If you're using IIS7+ this is definitely the way to go. +1!Centralism
@jayrdub I can't reproduce the problem with 302 redirect before 404 page. I have created a test project and it works as expected - 404 page returns 404 code. See dl.dropbox.com/u/2019363/Test404.zipOdie
In case the sample project becomes unavailable at some point, make sure to set the Response.StatusCode in your Error controller action methods.Boogie
is it possible to return only a status 404 when youre working in JSON within the same project?Sidoon
This is working great in iis express, but as soon as i deploy the site in production IIS 7.5, all i get is a white page instead of the error view.Alienor
@Alienor can you ask a separate question for your problem?Odie
According to my tests (with MVC3) this breaks customErrors mode="On" together with the HandleErrorAttribute to be functional. Custom error pages for unhandled exceptions in controller actions are not served anymore.Uniocular
@AndreiRinea This works well, but only if you want a single catch-all for all 404s. For example, if you want 404's for static resources (such as under /images) so you can avoid the overhead of re-rendering a custom 404 page (with layouts, images, etc) for bad resource requests.Seditious
There are a lot of cases this solution does not handle. throw new HttpException(404, "Not found") is one of them. HttpException is handled by ASP.NET and not IIS.Rattail
ExecuteURL resets HttpContext.ItemsDearden
And what if critical exception occurs in Application_Start ? It seems dangerous to redirect to some Error controller if the application cannot be started. That's why I prefer to add custom static pages for critical errors thrown by the .NET core or IIS and catch everything else in Application_Error handler which can also redirect further to the ErrorController in case it's a recoverable error which requires some user interaction in some MVC view.Gallbladder
@PavelChuchuva I have the same problem with Module. I got a blank page after deploying to IIS7.5. Here is my question #29547648Integrant
Although this naswer would be correct for different question, AFAIR, OP said to avoid dealing with web.configEastwood
One case - path like http://test-404.azurewebsites.net/foo/bar%3Cscrip%3E%3C/script%3E doesn't return custome page. Just text Bad RequestHendecasyllable
A
154

Quick Answer

enter image description here

For the lazy people out there:

Install-Package MagicalUnicornMvcErrorToolkit -Version 1.0

Then remove this line from global.asax

GlobalFilters.Filters.Add(new HandleErrorAttribute());

And this is only for IIS7+ and IIS Express.

It won't work if you're using Cassini


Long, explained answer

I know this has been answered. But the answer is really simple (cheers to David Fowler and Damian Edwards for really answering this).

There is no need to do anything custom. For ASP.NET MVC3, all the bits and pieces are there.

Step 1 -> Update your web.config in TWO spots.

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

and

<system.webServer>
    <httpErrors errorMode="Custom">
      <remove statusCode="404" subStatusCode="-1" />
      <error statusCode="404" path="/NotFound" responseMode="ExecuteURL" />
      <remove statusCode="500" subStatusCode="-1" />
      <error statusCode="500" path="/ServerError" responseMode="ExecuteURL" />
    </httpErrors>    

...
<system.webServer>
...
</system.web>

Now take careful note of the routes I've decided to use. You can use anything, but my routes are

  • /NotFound <- for a 404 not found, error page.
  • /ServerError <- for any other error, include errors that happen in my code. this is a 500 Internal Server Error

See how the first section in <system.web> only has one custom entry? The statusCode="404" entry? I've only listed one status code because all other errors, including the 500 Server Error (ie. those pesky error that happens when your code has a bug and crashes the user's request) .. all the other errors are handled by the setting defaultRedirect="/ServerError" .. which says, if you are not a 404 page not found, then please goto the route /ServerError.

OK, that's out of the way; now to my routes listed in global.asax

Step 2 - Creating the routes in Global.asax

Here's my full route section:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.IgnoreRoute("{*favicon}", new {favicon = @"(.*/)?favicon.ico(/.*)?"});

    routes.MapRoute(
        "Error - 404",
        "NotFound",
        new { controller = "Error", action = "NotFound" }
        );

    routes.MapRoute(
        "Error - 500",
        "ServerError",
        new { controller = "Error", action = "ServerError"}
        );

    routes.MapRoute(
        "Default", // Route name
        "{controller}/{action}/{id}", // URL with parameters
        new {controller = "Home", action = "Index", id = UrlParameter.Optional}
        );
}

That lists two ignore routes -> axd's and favicons. Then I have my two explicit error handling routes .. followed by any other routes. In this case, the default one. Of course, I have more, but that's special to my web site. (Just make sure the error routes are at the top of the list. Order is imperative).

Finally, while we are inside our global.asax file, we do not globally register the HandleError attribute.

Remove this line from global.asax

GlobalFilters.Filters.Add(new HandleErrorAttribute());

Step 3 - Create the controller with the action methods

Now .. we add a controller with two action methods ...

public class ErrorController : Controller
{
    public ActionResult NotFound()
    {
        Response.StatusCode = (int)HttpStatusCode.NotFound;
        return View();
    }

    public ActionResult ServerError()
    {
        Response.StatusCode = (int)HttpStatusCode.InternalServerError;

        // Todo: Pass the exception into the view model, which you can make.
        //       That's an exercise, dear reader, for -you-.
        //       In case you want to pass it to the view, if you're admin, etc.
        // if (User.IsAdmin) // <-- I just made that up :)
        // {
        //     var exception = Server.GetLastError();
        //     // etc..
        // }

        return View();
    }

    // Secret test method
    public ActionResult ThrowError()
    {
        throw new NotImplementedException("Pew ^ Pew");
    }
}

OK, let's check this out. First of all, there is NO [HandleError] attribute here. Why? Because the built in ASP.NET framework is already handling errors AND we have specified all the things we need to do to handle an error. It's in this method!

Next, I have the two action methods. Nothing tough there. If you wish to show any exception info, then you can use Server.GetLastError() to get that info.

Bonus: Yes, I made a third action method, to test error handling.

Step 4 - Create the Views

And finally, create two views. Put them in the normal view spot, for this controller.

enter image description here

Bonus comments

  • You don't need an Application_Error(object sender, EventArgs e)
  • The above steps all work 100% perfectly with Elmah

And that, my friends, should be it.

Asa answered 21/9, 2011 at 12:2 Comment(4)
So I tried to implement this but a couple problems... first, You need a ~ before the path in weeb.config or it doesnt work for virtual directories. 2-If IIS custom errors fires and the view is using a layout it doesn't render at all, just a white page. I solved that by adding this line into the controller "Response.TrySkipIisCustomErrors = true;" . However, it still doesn't work if you go to a url that is a file but 404.. like mysite/whatever/fake.html gets a white page.All
-1, sorry, for me any solution that does change url for 404 is wrong. and with webconfig there is no way in MVC that you can handle it without changing url, or you need to create static html files or aspx (yes, plain old aspx files) to be able to do it. your solution is fine if you like ?aspxerrorpath=/er/not/found to have in urls.Cardio
This might sounds really weird - but my answer was provided ages ago and I agree with your @Gutek, I don't like doing a redirect to an error page anymore. I used to (ref to my answer :P). If the error occured on /some/resource .. then THAT resource should return a 404 or 500, etc. MASSIVE SEO implications otherwise. Ahh .. how times change :)Asa
@Cardio Are you aware of customErrors redirectMode="ResponseRewrite"? And returning 404's is not ideal from a security perspectiveUrine
G
91

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;
    }
}

(Optional)

Explanation:

AFAIK, there are 6 different cases that an ASP.NET MVC3 apps can generate 404s.

(Automatically generated by ASP.NET Framework:)

(1) An URL does not find a match in the route table.

(Automatically generated by ASP.NET MVC Framework:)

(2) An URL finds a match in the route table, but specifies a non-existent controller.

(3) An URL finds a match in the route table, but specifies a non-existant action.

(Manually generated:)

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

(5) An action throws an HttpException with the status code 404.

(6) An actions manually modifies the Response.StatusCode property to 404.

Normally, you want to accomplish 3 objectives:

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

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

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

There are various ways to try to accomplish this:

(1)

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

Problems with this solution:

  1. Does not comply with objective (1) in cases (1), (4), (6).
  2. Does not comply with objective (2) automatically. It must be programmed manually.
  3. Does not comply with objective (3).

(2)

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

Problems with this solution:

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

(3)

<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:

  1. Only works on IIS 7+.
  2. Does not comply with objective (2) automatically. It must be programmed manually.
  3. 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.

(4)

<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:

  1. Only works on IIS 7+.
  2. Does not comply with objective (2) automatically. It must be programmed manually.
  3. Does not comply with objective (3) in cases (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 cases without the complexity of using an external library.

Gisele answered 26/1, 2012 at 23:20 Comment(8)
Great answer. Worthy of many more upvotes. Why does your global.asax code not work/belong in Application_Error.Calvary
Thanks! It cannot be done under Application_Error, because explicit 404s thrown from a controller are not considered errors on ASP.NET. If you return a HttpNotFound() from a controller, the Application_Error event will never trigger.Gisele
I think you forgot public ActionResult NotFound() {} in your ErrorsController. Also, can you explain how your _NotFound partial would look like for AJAX requests?Spraddle
You're right, thanks for the feedback. I've update it. What the _NotFound should contain, depedends on how you want to handle the 404 on the client. If you only do an alert, then return simple text, if you show a more elaborate error message, then put HTML and put JavaScript logic on the client to show it.Gisele
With MVC 4 I always get MissingMethodException: Cannot create an abstract class on the line c.Execute(new RequestContext(new HttpContextWrapper(Context), rd)); Any ideas?Also
@µBio I've just implemented this in MVC 4 and it's working fine. Application_Error sets HttpStatusCode.InternalServerError, Application_EndRequest handles 401, 403, 404 and any other error in the 400 and 500 range. I'm also manually logging 401s and 403s to ELMAH.Actiniform
If the "not found" URL includes a dot in the path (e.g. example.com/hi.bob) then the Application_EndRequest doesn't fire at all, and I get IE's generic 404 page.Garlen
@µBio make sure that you are using System.Web.Routing.RequestContext. I got the same error and it was trying to use System.ServiceModel.Channels.RequestContext instead.Eubanks
A
14

I really like cottsaks solution and think its very clearly explained. my only addition was to alter step 2 as follows

public abstract class MyController : Controller
{

    #region Http404 handling

    protected override void HandleUnknownAction(string actionName)
    {
        //if controller is ErrorController dont 'nest' exceptions
        if(this.GetType() != typeof(ErrorController))
        this.InvokeHttp404(HttpContext);
    }

    public ActionResult InvokeHttp404(HttpContextBase httpContext)
    {
        IController errorController = ObjectFactory.GetInstance<ErrorController>();
        var errorRoute = new RouteData();
        errorRoute.Values.Add("controller", "Error");
        errorRoute.Values.Add("action", "Http404");
        errorRoute.Values.Add("url", httpContext.Request.Url.OriginalString);
        errorController.Execute(new RequestContext(
             httpContext, errorRoute));

        return new EmptyResult();
    }

    #endregion
}

Basically this stops urls containing invalid actions AND controllers from triggering the exception routine twice. eg for urls such as asdfsdf/dfgdfgd

Armand answered 29/5, 2010 at 20:50 Comment(2)
This is excellent. Those "twice" cases were beginning to bother me. updated my answerMaestricht
does the above solution work at all if user enter wrong controller and action name?Oft
N
7

The only way I could get @cottsak's method to work for invalid controllers was to modify the existing route request in the CustomControllerFactory, like so:

public class CustomControllerFactory : DefaultControllerFactory
{
    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
        try
        {
            if (controllerType == null)
                return base.GetControllerInstance(requestContext, controllerType); 
            else
                return ObjectFactory.GetInstance(controllerType) as Controller;
        }
        catch (HttpException ex)
        {
            if (ex.GetHttpCode() == (int)HttpStatusCode.NotFound)
            {
                requestContext.RouteData.Values["controller"] = "Error";
                requestContext.RouteData.Values["action"] = "Http404";
                requestContext.RouteData.Values.Add("url", requestContext.HttpContext.Request.Url.OriginalString);

                return ObjectFactory.GetInstance<ErrorController>();
            }
            else
                throw ex;
        }
    }
}

I should mention I'm using MVC 2.0.

Novobiocin answered 29/7, 2010 at 23:43 Comment(3)
Do you know why? (MVC2 specific?)Maestricht
I think the key was to modify the existing request rather than make a new one, but I did this a little while ago so I'm not sure that was it. The "InvokeHttp404" didn't work from the controller factory.Novobiocin
I've updated my answer today with some MVC2 specifics. Can you please tell me if my solution as detailed above still does not work for you?Maestricht
M
5

Here is another method using MVC tools which you can handle requests to bad controller names, bad route names, and any other criteria you see fit inside of an Action method. Personally, I prefer to avoid as many web.config settings as possible, because they do the 302 / 200 redirect and do not support ResponseRewrite (Server.Transfer) using Razor views. I'd prefer to return a 404 with a custom error page for SEO reasons.

Some of this is new take on cottsak's technique above.

This solution also uses minimal web.config settings favoring the MVC 3 Error Filters instead.

Usage

Just throw a HttpException from an action or custom ActionFilterAttribute.

Throw New HttpException(HttpStatusCode.NotFound, "[Custom Exception Message Here]")

Step 1

Add the following setting to your web.config. This is required to use MVC's HandleErrorAttribute.

<customErrors mode="On" redirectMode="ResponseRedirect" />

Step 2

Add a custom HandleHttpErrorAttribute similar to the MVC framework's HandleErrorAttribute, except for HTTP errors:

<AttributeUsage(AttributeTargets.All, AllowMultiple:=True)>
Public Class HandleHttpErrorAttribute
    Inherits FilterAttribute
    Implements IExceptionFilter

    Private Const m_DefaultViewFormat As String = "ErrorHttp{0}"

    Private m_HttpCode As HttpStatusCode
    Private m_Master As String
    Private m_View As String

    Public Property HttpCode As HttpStatusCode
        Get
            If m_HttpCode = 0 Then
                Return HttpStatusCode.NotFound
            End If
            Return m_HttpCode
        End Get
        Set(value As HttpStatusCode)
            m_HttpCode = value
        End Set
    End Property

    Public Property Master As String
        Get
            Return If(m_Master, String.Empty)
        End Get
        Set(value As String)
            m_Master = value
        End Set
    End Property

    Public Property View As String
        Get
            If String.IsNullOrEmpty(m_View) Then
                Return String.Format(m_DefaultViewFormat, Me.HttpCode)
            End If
            Return m_View
        End Get
        Set(value As String)
            m_View = value
        End Set
    End Property

    Public Sub OnException(filterContext As System.Web.Mvc.ExceptionContext) Implements System.Web.Mvc.IExceptionFilter.OnException
        If filterContext Is Nothing Then Throw New ArgumentException("filterContext")

        If filterContext.IsChildAction Then
            Return
        End If

        If filterContext.ExceptionHandled OrElse Not filterContext.HttpContext.IsCustomErrorEnabled Then
            Return
        End If

        Dim ex As HttpException = TryCast(filterContext.Exception, HttpException)
        If ex Is Nothing OrElse ex.GetHttpCode = HttpStatusCode.InternalServerError Then
            Return
        End If

        If ex.GetHttpCode <> Me.HttpCode Then
            Return
        End If

        Dim controllerName As String = filterContext.RouteData.Values("controller")
        Dim actionName As String = filterContext.RouteData.Values("action")
        Dim model As New HandleErrorInfo(filterContext.Exception, controllerName, actionName)

        filterContext.Result = New ViewResult With {
            .ViewName = Me.View,
            .MasterName = Me.Master,
            .ViewData = New ViewDataDictionary(Of HandleErrorInfo)(model),
            .TempData = filterContext.Controller.TempData
        }
        filterContext.ExceptionHandled = True
        filterContext.HttpContext.Response.Clear()
        filterContext.HttpContext.Response.StatusCode = Me.HttpCode
        filterContext.HttpContext.Response.TrySkipIisCustomErrors = True
    End Sub
End Class

Step 3

Add Filters to the GlobalFilterCollection (GlobalFilters.Filters) in Global.asax. This example will route all InternalServerError (500) errors to the Error shared view (Views/Shared/Error.vbhtml). NotFound (404) errors will be sent to ErrorHttp404.vbhtml in the shared views as well. I've added a 401 error here to show you how this can be extended for additional HTTP error codes. Note that these must be shared views, and they all use the System.Web.Mvc.HandleErrorInfo object as a the model.

filters.Add(New HandleHttpErrorAttribute With {.View = "ErrorHttp401", .HttpCode = HttpStatusCode.Unauthorized})
filters.Add(New HandleHttpErrorAttribute With {.View = "ErrorHttp404", .HttpCode = HttpStatusCode.NotFound})
filters.Add(New HandleErrorAttribute With {.View = "Error"})

Step 4

Create a base controller class and inherit from it in your controllers. This step allows us to handle unknown action names and raise the HTTP 404 error to our HandleHttpErrorAttribute.

Public Class BaseController
    Inherits System.Web.Mvc.Controller

    Protected Overrides Sub HandleUnknownAction(actionName As String)
        Me.ActionInvoker.InvokeAction(Me.ControllerContext, "Unknown")
    End Sub

    Public Function Unknown() As ActionResult
        Throw New HttpException(HttpStatusCode.NotFound, "The specified controller or action does not exist.")
        Return New EmptyResult
    End Function
End Class

Step 5

Create a ControllerFactory override, and override it in your Global.asax file in Application_Start. This step allows us to raise the HTTP 404 exception when an invalid controller name has been specified.

Public Class MyControllerFactory
    Inherits DefaultControllerFactory

    Protected Overrides Function GetControllerInstance(requestContext As System.Web.Routing.RequestContext, controllerType As System.Type) As System.Web.Mvc.IController
        Try
            Return MyBase.GetControllerInstance(requestContext, controllerType)
        Catch ex As HttpException
            Return DependencyResolver.Current.GetService(Of BaseController)()
        End Try
    End Function
End Class

'In Global.asax.vb Application_Start:

controllerBuilder.Current.SetControllerFactory(New MyControllerFactory)

Step 6

Include a special route in your RoutTable.Routes for the BaseController Unknown action. This will help us raise a 404 in the case where a user accesses an unknown controller, or unknown action.

'BaseController
routes.MapRoute( _
    "Unknown", "BaseController/{action}/{id}", _
    New With {.controller = "BaseController", .action = "Unknown", .id = UrlParameter.Optional} _
)

Summary

This example demonstrated how one can use the MVC framework to return 404 Http Error Codes to the browser without a redirect using filter attributes and shared error views. It also demonstrates showing the same custom error page when invalid controller names and action names are specified.

I'll add a screenshot of an invalid controller name, action name, and a custom 404 raised from the Home/TriggerNotFound action if I get enough votes to post one =). Fiddler returns a 404 message when I access the following URLs using this solution:

/InvalidController
/Home/InvalidRoute
/InvalidController/InvalidRoute
/Home/TriggerNotFound

cottsak's post above and these articles were good references.

Maegan answered 1/11, 2011 at 14:11 Comment(2)
Hmm, I couldn't get this to work: The IControllerFactory 'aaa.bbb.CustomControllerFactory' did not return a controller for the name '123'. - any ideas why I would get that?Moncton
redirectMode="ResponseRedirect". This will return a 302 Found + a 200 OK which is not good for SEO!Rattail
A
5

My shortened solution that works with unhandled areas, controllers and actions:

  1. Create a view 404.cshtml.

  2. Create a base class for your controllers:

    public class Controller : System.Web.Mvc.Controller
    {
        protected override void HandleUnknownAction(string actionName)
        {
            Http404().ExecuteResult(ControllerContext);
        }
    
        protected virtual ViewResult Http404()
        {
            Response.StatusCode = (int)HttpStatusCode.NotFound;
            return View("404");
        }
    }
    
  3. Create a custom controller factory returning your base controller as a fallback:

    public class ControllerFactory : DefaultControllerFactory
    {
        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            if (controllerType != null)
                return base.GetControllerInstance(requestContext, controllerType);
    
            return new Controller();
        }
    }
    
  4. Add to Application_Start() the following line:

    ControllerBuilder.Current.SetControllerFactory(typeof(ControllerFactory));
    
Anacoluthia answered 23/5, 2014 at 13:9 Comment(0)
A
4

In MVC4 WebAPI 404 can be handle in the following way,

COURSES APICONTROLLER

    // GET /api/courses/5
    public HttpResponseMessage<Courses> Get(int id)
    {
        HttpResponseMessage<Courses> resp = null;

        var aCourse = _courses.Where(c => c.Id == id).FirstOrDefault();

        resp = aCourse == null ? new HttpResponseMessage<Courses>(System.Net.HttpStatusCode.NotFound) : new HttpResponseMessage<Courses>(aCourse);

        return resp;
    }

HOME CONTROLLER

public ActionResult Course(int id)
{
    return View(id);
}

VIEW

<div id="course"></div>
<script type="text/javascript">
    var id = @Model;
    var course = $('#course');
    $.ajax({    
        url: '/api/courses/' + id,
        success: function (data) {
            course.text(data.Name);
        },
        statusCode: {
            404: function() 
            {
                course.text('Course not available!');    
            }
        }
    });
</script>

GLOBAL

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );

    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

RESULTS

enter image description here

Arteaga answered 8/5, 2012 at 12:59 Comment(0)
W
3

Dealing with errors in ASP.NET MVC is just a pain in the butt. I tried a whole lot of suggestions on this page and on other questions and sites and nothing works good. One suggestion was to handle errors on web.config inside system.webserver but that just returns blank pages.

My goal when coming up with this solution was to;

  • NOT REDIRECT
  • Return PROPER STATUS CODES not 200/Ok like the default error handling

Here is my solution.

1.Add the following to system.web section

   <system.web>
     <customErrors mode="On" redirectMode="ResponseRewrite">
      <error statusCode="404"  redirect="~/Error/404.aspx" />
      <error statusCode="500" redirect="~/Error/500.aspx" />
     </customErrors>
    <system.web>

The above handles any urls not handled by routes.config and unhandled exceptions especially those encountered on the views. Notice I used aspx not html. This is so I can add a response code on the code behind.

2. Create a folder called Error (or whatever you prefer) at the root of your project and add the two webforms. Below is my 404 page;

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="404.aspx.cs" Inherits="Myapp.Error._404" %>

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title >Page Not found</title>
    <link href="<%=ResolveUrl("~/Content/myapp.css")%>" rel="stylesheet" />
</head>
<body>
    <div class="top-nav">
      <a runat="server" class="company-logo" href="~/"></a>
    </div>
    <div>
        <h1>404 - Page Not found</h1>
        <p>The page you are looking for cannot be found.</p>
        <hr />
        <footer></footer>
    </div>
</body>
</html>

And on the code behind I set the response code

protected void Page_Load(object sender, EventArgs e)
{
    Response.StatusCode = 404;
}

Do the same for the 500 page

3.To handle errors within the controllers. There's many ways to do it. This is what worked for me. All my controllers inherit from a base controller. In the base controller, I have the following methods

protected ActionResult ShowNotFound()
{
    return ShowNotFound("Page not found....");
}

protected ActionResult ShowNotFound(string message)
{
    return ShowCustomError(HttpStatusCode.NotFound, message);
}

protected ActionResult ShowServerError()
{
    return ShowServerError("Application error....");
}

protected ActionResult ShowServerError(string message)
{
    return ShowCustomError(HttpStatusCode.InternalServerError, message);
}

protected ActionResult ShowNotAuthorized()
{
    return ShowNotAuthorized("You are not allowed ....");

}

protected ActionResult ShowNotAuthorized(string message)
{
    return ShowCustomError(HttpStatusCode.Forbidden, message);
}

protected ActionResult ShowCustomError(HttpStatusCode statusCode, string message)
{
    Response.StatusCode = (int)statusCode;
    string title = "";
    switch (statusCode)
    {
        case HttpStatusCode.NotFound:
            title = "404 - Not found";
            break;
        case HttpStatusCode.Forbidden:
            title = "403 - Access Denied";
            break;
        default:
            title = "500 - Application Error";
            break;
    }
    ViewBag.Title = title;
    ViewBag.Message = message;
    return View("CustomError");
}

4.Add the CustomError.cshtml to your Shared views folder. Below is mine;

<h1>@ViewBag.Title</h1>
<br />
<p>@ViewBag.Message</p>

Now in your application controller you can do something like this;

public class WidgetsController : ControllerBase
{
  [HttpGet]
  public ActionResult Edit(int id)
  {
    Try
    {
       var widget = db.getWidgetById(id);
       if(widget == null)
          return ShowNotFound();
          //or return ShowNotFound("Invalid widget!");
       return View(widget);
    }
    catch(Exception ex)
    {
       //log error
       logger.Error(ex)
       return ShowServerError();
    }
  }
}

Now for the caveat. It won't handle static file errors. So if you have a route such as example.com/widgets and the user changes it to example.com/widgets.html, they will get the IIS default error page so you have to handle IIS level errors some other way.

Wolfish answered 31/8, 2016 at 4:19 Comment(0)
E
2

Try NotFoundMVC on nuget. It works , no setup.

Ency answered 9/7, 2013 at 14:6 Comment(3)
http://localhost/Views/Shared/NotFound.cshtml does not result in a custom 404 page.Tideland
It's very easy to customise. You have access to the requested url and the referrer, so you can do what you like. I use this package, and it works really well.Monochromatic
This is a great package, providing you won't use async Task<ActionResult> actions (or other similar async ) actions. On MVC 5 this is a broken scenario. There is a fork on GitHub to circumvent that, but for me it's a no, no.Elba
S
2

My solution, in case someone finds it useful.

In Web.config:

<system.web>
    <customErrors mode="On" defaultRedirect="Error" >
      <error statusCode="404" redirect="~/Error/PageNotFound"/>
    </customErrors>
    ...
</system.web>

In Controllers/ErrorController.cs:

public class ErrorController : Controller
{
    public ActionResult PageNotFound()
    {
        if(Request.IsAjaxRequest()) {
            Response.StatusCode = (int)HttpStatusCode.NotFound;
            return Content("Not Found", "text/plain");
        }

        return View();
    }
}

Add a PageNotFound.cshtml in the Shared folder, and that's it.

Saith answered 1/10, 2013 at 10:31 Comment(2)
Doesn't this issue a 302 redirect then a 200 (OK) status to the client? Shouldn't they still be getting a 404 status?Alexine
@Saith Are you sure the line in your code should read model.RequestedUrl = Request.Url.OriginalString.Contains(url) & Request.Url.OriginalString != url ? Request.Url.OriginalString : url; and not model.RequestedUrl = Request.Url.OriginalString.Contains(url) && Request.Url.OriginalString != url ? Request.Url.OriginalString : url; (& instead of &&)?Mystical
I
2

It seems to me that the standard CustomErrors configuration should just work however, due to the reliance on Server.Transfer it seems that the internal implementation of ResponseRewrite isn't compatible with MVC.

This feels like a glaring functionality hole to me, so I decided to re-implement this feature using a HTTP module. The solution below allows you to handle any HTTP status code (including 404) by redirecting to any valid MVC route just as you would do normally.

<customErrors mode="RemoteOnly" redirectMode="ResponseRewrite">
    <error statusCode="404" redirect="404.aspx" />
    <error statusCode="500" redirect="~/MVCErrorPage" />
</customErrors>

This has been tested on the following platforms;

  • MVC4 in Integrated Pipeline Mode (IIS Express 8)
  • MVC4 in Classic Mode (VS Development Server, Cassini)
  • MVC4 in Classic Mode (IIS6)

Benefits

  • Generic solution which can be dropped into any MVC project
  • Enables support for traditional custom errors configuration
  • Works in both Integrated Pipeline and Classic modes

The Solution

namespace Foo.Bar.Modules {

    /// <summary>
    /// Enables support for CustomErrors ResponseRewrite mode in MVC.
    /// </summary>
    public class ErrorHandler : IHttpModule {

        private HttpContext HttpContext { get { return HttpContext.Current; } }
        private CustomErrorsSection CustomErrors { get; set; }

        public void Init(HttpApplication application) {
            System.Configuration.Configuration configuration = WebConfigurationManager.OpenWebConfiguration("~");
            CustomErrors = (CustomErrorsSection)configuration.GetSection("system.web/customErrors");

            application.EndRequest += Application_EndRequest;
        }

        protected void Application_EndRequest(object sender, EventArgs e) {

            // only handle rewrite mode, ignore redirect configuration (if it ain't broke don't re-implement it)
            if (CustomErrors.RedirectMode == CustomErrorsRedirectMode.ResponseRewrite && HttpContext.IsCustomErrorEnabled) {

                int statusCode = HttpContext.Response.StatusCode;

                // if this request has thrown an exception then find the real status code
                Exception exception = HttpContext.Error;
                if (exception != null) {
                    // set default error status code for application exceptions
                    statusCode = (int)HttpStatusCode.InternalServerError;
                }

                HttpException httpException = exception as HttpException;
                if (httpException != null) {
                    statusCode = httpException.GetHttpCode();
                }

                if ((HttpStatusCode)statusCode != HttpStatusCode.OK) {

                    Dictionary<int, string> errorPaths = new Dictionary<int, string>();

                    foreach (CustomError error in CustomErrors.Errors) {
                        errorPaths.Add(error.StatusCode, error.Redirect);
                    }

                    // find a custom error path for this status code
                    if (errorPaths.Keys.Contains(statusCode)) {
                        string url = errorPaths[statusCode];

                        // avoid circular redirects
                        if (!HttpContext.Request.Url.AbsolutePath.Equals(VirtualPathUtility.ToAbsolute(url))) {

                            HttpContext.Response.Clear();
                            HttpContext.Response.TrySkipIisCustomErrors = true;

                            HttpContext.Server.ClearError();

                            // do the redirect here
                            if (HttpRuntime.UsingIntegratedPipeline) {
                                HttpContext.Server.TransferRequest(url, true);
                            }
                            else {
                                HttpContext.RewritePath(url, false);

                                IHttpHandler httpHandler = new MvcHttpHandler();
                                httpHandler.ProcessRequest(HttpContext);
                            }

                            // return the original status code to the client
                            // (this won't work in integrated pipleline mode)
                            HttpContext.Response.StatusCode = statusCode;

                        }
                    }

                }

            }

        }

        public void Dispose() {

        }


    }

}

Usage

Include this as the final HTTP module in your web.config

  <system.web>
    <httpModules>
      <add name="ErrorHandler" type="Foo.Bar.Modules.ErrorHandler" />
    </httpModules>
  </system.web>

  <!-- IIS7+ -->
  <system.webServer>
    <modules>
      <add name="ErrorHandler" type="Foo.Bar.Modules.ErrorHandler" />
    </modules>
  </system.webServer>

For those of you paying attention you will notice that in Integrated Pipeline mode this will always respond with HTTP 200 due to the way Server.TransferRequest works. To return the proper error code I use the following error controller.

public class ErrorController : Controller {

    public ErrorController() { }

    public ActionResult Index(int id) {
        // pass real error code to client
        HttpContext.Response.StatusCode = id;
        HttpContext.Response.TrySkipIisCustomErrors = true;

        return View("Errors/" + id.ToString());
    }

}
Incorrect answered 31/3, 2015 at 9:37 Comment(0)
R
1

Posting an answer since my comment was too long...

It's both a comment and questions to the unicorn post/answer:

https://mcmap.net/q/80432/-how-can-i-properly-handle-404-in-asp-net-mvc

I prefer this answer over the others for it's simplicity and the fact that apparently some folks at Microsoft were consulted. I got three questions however and if they can be answered then I will call this answer the holy grail of all 404/500 error answers on the interwebs for an ASP.NET MVC (x) app.

@Pure.Krome

  1. Can you update your answer with the SEO stuff from the comments pointed out by GWB (there was never any mentioning of this in your answer) - <customErrors mode="On" redirectMode="ResponseRewrite"> and <httpErrors errorMode="Custom" existingResponse="Replace">?

  2. Can you ask your ASP.NET team friends if it is okay to do it like that - would be nice to have some confirmation - maybe it's a big no-no to change redirectMode and existingResponse in this way to be able to play nicely with SEO?!

  3. Can you add some clarification surrounding all that stuff (customErrors redirectMode="ResponseRewrite", customErrors redirectMode="ResponseRedirect", httpErrors errorMode="Custom" existingResponse="Replace", REMOVE customErrors COMPLETELY as someone suggested) after talking to your friends at Microsoft?

As I was saying; it would be supernice if we could make your answer more complete as this seem to be a fairly popular question with 54 000+ views.

Update: Unicorn answer does a 302 Found and a 200 OK and cannot be changed to only return 404 using a route. It has to be a physical file which is not very MVC:ish. So moving on to another solution. Too bad because this seemed to be the ultimate MVC:ish answer this far.

Rattail answered 14/1, 2014 at 14:36 Comment(0)
T
1

Adding my solution, which is almost identical to Herman Kan's, with a small wrinkle to allow it to work for my project.

Create a custom error controller:

public class Error404Controller : BaseController
{
    [HttpGet]
    public ActionResult PageNotFound()
    {
        Response.StatusCode = 404;
        return View("404");
    }
}

Then create a custom controller factory:

public class CustomControllerFactory : DefaultControllerFactory
{
    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
        return controllerType == null ? new Error404Controller() : base.GetControllerInstance(requestContext, controllerType);
    }
}

Finally, add an override to the custom error controller:

protected override void HandleUnknownAction(string actionName)
{
    var errorRoute = new RouteData();
    errorRoute.Values.Add("controller", "Error404");
    errorRoute.Values.Add("action", "PageNotFound");
    new Error404Controller().Execute(new RequestContext(HttpContext, errorRoute));
}

And that's it. No need for Web.config changes.

Torietorii answered 3/2, 2017 at 2:45 Comment(0)
P
1

1) Make abstract Controller class.

public abstract class MyController:Controller
{
    public ActionResult NotFound()
    {
        Response.StatusCode = 404;
        return View("NotFound");
    }

    protected override void HandleUnknownAction(string actionName)
    {
        this.ActionInvoker.InvokeAction(this.ControllerContext, "NotFound");
    }
    protected override void OnAuthorization(AuthorizationContext filterContext) { }
}  

2) Make inheritence from this abstract class in your all controllers

public class HomeController : MyController
{}  

3) And add a view named "NotFound" in you View-Shared folder.

Pinnule answered 20/2, 2017 at 13:21 Comment(0)
M
0

I went through most of the solutions posted on this thread. While this question might be old, it is still very applicable to new projects even now, so I spent quite a lot of time reading up on the answers presented here as well as else where.

As @Marco pointed out the different cases under which a 404 can happen, I checked the solution I compiled together against that list. In addition to his list of requirements, I also added one more.

  • The solution should be able to handle MVC as well as AJAX/WebAPI calls in the most appropriate manner. (i.e. if 404 happens in MVC, it should show the Not Found page and if 404 happens in WebAPI, it should not hijack the XML/JSON response so that the consuming Javascript can parse it easily).

This solution is 2 fold:

First part of it comes from @Guillaume at https://mcmap.net/q/81674/-customerrors-vs-httperrors-a-significant-design-flaw. Their solution takes care of any 404 that were caused due to invalid route, invalid controller and invalid action.

The idea is to create a WebForm and then make it call the NotFound action of your MVC Errors Controller. It does all of this without any redirect so you will not see a single 302 in Fiddler. The original URL is also preserved, which makes this solution fantastic!


Second part of it comes from @Germán at https://mcmap.net/q/81675/-how-to-return-a-view-for-httpnotfound-in-asp-net-mvc-3. Their solution takes care of any 404 returned by your actions in the form of HttpNotFoundResult() or throw new HttpException()!

The idea is to have a filter look at the response as well as the exception thrown by your MVC controllers and to call the appropriate action in your Errors Controller. Again this solution works without any redirect and the original url is preserved!


As you can see, both of these solutions together offer a very robust error handling mechanism and they achieve all the requirements listed by @Marco as well as my requirements. If you would like to see a working sample or a demo of this solution, please leave in the comments and I would be happy to put it together.

Majordomo answered 27/4, 2015 at 10:26 Comment(0)
H
0

I have gone through all articles but nothing works for me: My requirement user type anything in your url custom 404 page should show.I thought it is very straight forward.But you should understand handling of 404 properly:

 <system.web>
    <customErrors mode="On" redirectMode="ResponseRewrite">
      <error statusCode="404" redirect="~/PageNotFound.aspx"/>
    </customErrors>
  </system.web>
<system.webServer>
    <httpErrors errorMode="Custom">
      <remove statusCode="404"/>
      <error statusCode="404" path="/PageNotFound.html" responseMode="ExecuteURL"/>
    </httpErrors>
</system.webServer>

I found this article very helpfull.should be read at once.Custome error page-Ben Foster

Haircloth answered 30/7, 2015 at 6:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.