39

I've been struggling all day to implement error handling in my ASP.NET MVC 2 app. I've looked at a variety of techniques, but none work properly. I'm using MVC2 and .NET 4.0 (started the project before MVC3 was released; we'll upgrade after we deliver our initial release).

At this point, I'll be happy to properly handle 404 and 500 errors -- 403 (authorization required) would be great, too, followed by various other specific responses. Right now, I either get all 404s, all 500s, all 302s before the 404, or all 302s before the 500.

Here are my requirements (which should be pretty close to the basic requirements of HTTP):

  • If a resource is not found, throw a 404, and display a 404-specific page with the requested URL. DO NOT return an intermediate response code like 302. Ideally, keep the requested URL, rather than showing a new URL like /Error/NotFound -- but if the latter displays, be sure we didn't return a redirect response to get it.

  • If an internal server error occurred, throw a 500, and display a 500-specific error with some indication of what went wrong. Again, don't return an intermediate response code, and ideally don't change the URL.

Here's what I'd consider a 404:

  1. Static file not found: /Content/non-existent-dir/non-existent-file.txt
  2. Controller not found: /non-existent-controller/Foo/666
  3. Controller found, but Action not found: /Home/non-existent-action/666
  4. Controller and action found, but the action can't find the requested object: /Home/Login/non-existent-id

Here's what I'd consider a 500:

  1. Post a bad value: POST /User/New/new-user-name-too-long-for-db-column-constraint
  2. Non-data-related problem, like a Web Service endpoint not responding

Some of these problems need to be identified by specific controllers or models, and then the controllers should throw the appropriate HttpException. The rest should be handled more generically.

For 404 case #2, I tried to use a custom ControllerFactory to throw a 404 if the controller can't be found. For 404 case #3, I've tried to use a custom base controller to override HandleUnknownAction and throw a 404.

In both cases, I get a 302 before the 404. And, I never get 500 errors; if I modify Web.config to put a typo in my Web Service endpoint, I still get a 302, then a 404 saying the URL (controller/action) which uses the Web Service can't be found. I also get the requested URL as a(n unwanted) querystring param: /Error/NotFound?aspxerrorpath=/Home/non-existent-action

Both of these techniques came from http://www.niksmit.com/wp/?p=17 (How to get normal 404 (Page not found) error pages using ASP.Net MVC), pointed to from http://richarddingwall.name/2008/08/17/strategies-for-resource-based-404-errors-in-aspnet-mvc/

If in Web.config I have <customErrors mode="On" defaultRedirect="~/Error/Unknown" redirectMode="ResponseRedirect" />, I get the appropriate response code, but my Error controller never gets called. Taking out the redirectMode attribute gets me the MVC error views, but with an intervening 302 and a changed URL -- and always the same controller (Unknown = 500; if I change it to NotFound everything looks like a 404).

Here are some of the other things I've read and tried to implement:

.. along with a bunch of StackOverflow posts.

Seems to me this sort of error handling is pretty basic to Web apps, and the MVC framework ought to have defaults that do this out of the box, and let people extend it to work otherwise. Perhaps they'll do it in a future release. In the meantime, can someone give me comprehensive details on how to implement proper HTTP responses?

Val
  • 2,291
  • 7
  • 34
  • 63

4 Answers4

50

Here's one technique you could use. Define an ErrorsController which will serve the error pages:

public class ErrorsController : Controller
{
    public ActionResult Http404()
    {
        Response.StatusCode = 404;
        return Content("404", "text/plain");
    }

    public ActionResult Http500()
    {
        Response.StatusCode = 500;
        return Content("500", "text/plain");
    }

    public ActionResult Http403()
    {
        Response.StatusCode = 403;
        return Content("403", "text/plain");
    }
}

and then in Global.asax you could subscribe for the Application_Error event where you could log the exception and execute the corresponding action of the ErrorsController:

protected void Application_Error(object sender, EventArgs e)
{
    var app = (MvcApplication)sender;
    var context = app.Context;
    var ex = app.Server.GetLastError();
    context.Response.Clear();
    context.ClearError();
    var httpException = ex as HttpException;

    var routeData = new RouteData();
    routeData.Values["controller"] = "errors";
    routeData.Values["exception"] = ex;
    routeData.Values["action"] = "http500";
    if (httpException != null)
    {
        switch (httpException.GetHttpCode())
        {
            case 404:
                routeData.Values["action"] = "http404";
                break;
            case 403:
                routeData.Values["action"] = "http403";
                break;
            case 500:
                routeData.Values["action"] = "http500";
                break;
        }
    }
    IController controller = new ErrorsController();
    controller.Execute(new RequestContext(new HttpContextWrapper(context), routeData));
}

And now all that's left is to start throwing proper exceptions:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        throw new HttpException(404, "NotFound");
    }
}
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 1
    Looks good -- I'll give it a go in a few hours and let you know how it works. One thing I'm not seeing: where do you handle the case of Controller not found? – Val Feb 06 '11 at 17:11
  • 2
    @Val, when a controller is not found an HttpException with code 404 is automatically thrown by ASP.NET MVC so it will enter the first case. – Darin Dimitrov Feb 06 '11 at 22:21
  • 1
    OK, I implemented the basics, and now I'm going back to implement custom error views for the various HTTP errors, and passing relevant info to display in each of them. I have more work to wire this up -- build an Error view model, populate it in the ErrorController, etc. but the basic functionality works great. No 302s, and, as you commented, missing actions and controllers are handled properly. Thanks -- this is the simplest and cleanest properly working example I've seen! – Val Feb 07 '11 at 00:17
  • As always @DarinDimitrov's answer is outstanding! Now I'm getting 404 response in Firebug instead of 302. – Leniel Maccaferri Mar 18 '12 at 19:33
  • @Darin: Can this be done in MVC3 as well or is there a different way to do it? If so, what way? – Brendan Vogt Apr 20 '12 at 07:49
  • It works perfectly on local but when i deploy it on iis 7.5 asp.net 4.5 and mvc4 iis 404 page shows up. How can i fix it ? Thanks – Barbaros Alp Jun 19 '13 at 08:59
  • Hey, I'm late to the party here, does this same structure apply to MVC 4, or have some of the things that @Val said should be defaults in MVC been implemented since this answer was posted? – ganders Jul 10 '14 at 13:20
  • Worked for me on 5.2.3.0. Thanks Darin, you got yourself some real MVC-fu. – Martin Hansen Lennox Sep 11 '19 at 23:09
4

For HTTP 404 errors (without redirects) take a look at my blog post on the subject. This might give you some good ideas:

http://hectorcorrea.com/blog/returning-http-404-in-asp-net-mvc/16

Hector Correa
  • 26,290
  • 8
  • 57
  • 73
  • Nice -- you're doing what Darin recommended, tho he has some more infrastructure around it to be able to handle multiple exception types. I'm now working to try to move his logic out of Application_Error and into the ErrorController itself. – Val Feb 08 '11 at 18:14
  • Content no longer available –  Sep 14 '17 at 14:26
  • @Darrren Link is available again. – Hector Correa Sep 14 '17 at 14:56
0

This doesn't answer your question, but it is important to note that HTTP status 500 indicates that something went wrong on the server, so your example:

POST /User/New/new-user-name-too-long-for-db-column-constraint

Is not valid grounds to throw a 500, its a data validation issue and should be handled by MVC data annotations or a jQuery validation framework or etc. Just showing an error message next to the TextBox saying "User Name too long" is much better.

JK.
  • 21,477
  • 35
  • 135
  • 214
  • 1
    Yes, I *should* validate and throw a validation error. But if I fail to validate and get a constraints error from the DB when it fails to insert or update the row, and I don't check properly for that, I want to throw an Internal Server Error. In fact, that's how I found we were missing some validation! – Val Feb 06 '11 at 17:07
0

This is a very old question. but I thought It's worth it if I introduce you to a much much cleaner way to handle Http Exceptions that I saw in dear "Jesse Webb's answer".

The solution is to use the httpErrors element of the system.webServer section:

<httpErrors errorMode="Custom" existingResponse="Replace">
  <remove statusCode="404" subStatusCode="-1" />
  <remove statusCode="500" subStatusCode="-1" />
  <error statusCode="404" path="/Error/NotFound" responseMode="ExecuteURL" />
  <error statusCode="500" path="/Error" responseMode="ExecuteURL" />
</httpErrors>

You also can log all exceptions in this way. "Read the "Jesse Webb's answer"".

This really feels much cleaner and also works as well as every other solution (without redirect).

Note: This only works work in IIS 7 and and newer. (Because of the httpErrors element which was recently added.

Arad Alvand
  • 8,607
  • 10
  • 51
  • 71