145

I'm developing a MVC3 base website and I am looking for a solution for handling errors and Render custom Views for each kind of error. So imagine that I have a "Error" Controller where his main action is "Index" (generic error page) and this controller will have a couple more actions for the errors that may appear to the user like "Handle500" or "HandleActionNotFound".

So every error that may happen on the website may be handled by this "Error" Controller (examples: "Controller" or "Action" not found, 500, 404, dbException, etc).

I am using Sitemap file to define website paths (and not route).

This question was already answered, this is a reply to Gweebz

My final applicaiton_error method is the following:

protected void Application_Error() {
//while my project is running in debug mode
if (HttpContext.Current.IsDebuggingEnabled && WebConfigurationManager.AppSettings["EnableCustomErrorPage"].Equals("false"))
{
    Log.Logger.Error("unhandled exception: ", Server.GetLastError());
}
else
{
    try
    {
        var exception = Server.GetLastError();

        Log.Logger.Error("unhandled exception: ", exception);

        Response.Clear();
        Server.ClearError();
        var routeData = new RouteData();
        routeData.Values["controller"] = "Errors";
        routeData.Values["action"] = "General";
        routeData.Values["exception"] = exception;

        IController errorsController = new ErrorsController();
        var rc = new RequestContext(new HttpContextWrapper(Context), routeData);
        errorsController.Execute(rc);
    }
    catch (Exception e)
    {
        //if Error controller failed for same reason, we will display static HTML error page
        Log.Logger.Fatal("failed to display error page, fallback to HTML error: ", e);
        Response.TransmitFile("~/error.html");
    }
}
}
Chris
  • 26,744
  • 48
  • 193
  • 345
João Louros
  • 2,752
  • 4
  • 23
  • 30
  • What should the settings be in the web.config to support this? Persumably you'd not include any httperrors settings? – philbird Nov 19 '11 at 13:43
  • http://forums.asp.net/p/1782402/4894514.aspx/1?Re+Custom+500+Error+Page+with+MVC3+IIS+7+5+Can+not+get+to+work+ has some nice tips like IE won't show your error page if it is under 512 bytes – RickAndMSFT Mar 23 '12 at 17:01

6 Answers6

201

Here's an example of how I handle custom errors. I define an ErrorsController with actions handling different HTTP errors:

public class ErrorsController : Controller
{
    public ActionResult General(Exception exception)
    {
        return Content("General failure", "text/plain");
    }

    public ActionResult Http404()
    {
        return Content("Not found", "text/plain");
    }

    public ActionResult Http403()
    {
        return Content("Forbidden", "text/plain");
    }
}

and then I subscribe for the Application_Error in Global.asax and invoke this controller:

protected void Application_Error()
{
    var exception = Server.GetLastError();
    var httpException = exception as HttpException;
    Response.Clear();
    Server.ClearError();
    var routeData = new RouteData();
    routeData.Values["controller"] = "Errors";
    routeData.Values["action"] = "General";
    routeData.Values["exception"] = exception;
    Response.StatusCode = 500;
    if (httpException != null)
    {
        Response.StatusCode = httpException.GetHttpCode();
        switch (Response.StatusCode)
        {
            case 403:
                routeData.Values["action"] = "Http403";
                break;
            case 404:
                routeData.Values["action"] = "Http404";
                break;
        }
    }

    IController errorsController = new ErrorsController();
    var rc = new RequestContext(new HttpContextWrapper(Context), routeData);
    errorsController.Execute(rc);
}
Leniel Maccaferri
  • 100,159
  • 46
  • 371
  • 480
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 4
    Just a little note. Since I wanted to render a View in each case (404, 500, etc) on each ActionResult I returned a View. However I did a try catch around Application_Error contents and I case of failure an static HTML page is returned. (I can post the code if someone desire) – João Louros Mar 09 '11 at 21:37
  • @João - Could you post the final version of your Application_Error() method? Maybe just edit it into this answer or your question up above? – Jesse Webb Jul 27 '11 at 21:03
  • @Gweebz posted my application_error method on my question, I hope it helps – João Louros Jul 28 '11 at 18:13
  • I tired that, and it worked great on localhost but it still doesn't work on dev server. Did I miss anything? – bobek Sep 14 '11 at 20:12
  • 4
    I can't get razor views to render using this solution on MVC3. return View(model) for example only gets a blank screen. – Extrakun Oct 13 '11 at 08:08
  • Using Windows authentication, this doesn't seem to catch 401 errors. – Ishmael Smyrnow Dec 21 '11 at 14:37
  • As @JoãoGalaLouros correctly pointed this solution render static web pages. The ErrorsController fail to render MVC Views. – Tomas Apr 24 '12 at 06:10
  • Is there a solution so it will render MVC Views? – Rob Jul 03 '12 at 18:50
  • @Rob, of course, simply replace `return Content` inside the `ErrorsController` actions with `return View()`. – Darin Dimitrov Jul 03 '12 at 18:51
  • @Darin, thnx!:) And what if that still presents me with a blank page while return Content works? – Rob Jul 03 '12 at 18:56
  • @DarinDimitrov Okay, i'll create a new topic for this issue then. – Rob Jul 03 '12 at 19:09
  • For those of you receiving something like: A public action method 'NotFound' was not found on controller 'ErrorsController'. Make sure your route responds to {*url} as the url instead of a specific url: routes.MapRoute("Errors/NotFound", "{*url}", new { controller = "Errors", action = "NotFound" }); – sky-dev Aug 13 '12 at 19:00
  • 2
    Added TrySkipIisCustomErrors to fix it for integrated IIS7. See http://stackoverflow.com/questions/1706934/asp-net-mvc-app-custom-error-pages-not-displaying-in-shared-hosting-environment – Pavel Savara Aug 23 '12 at 11:11
  • if you want your error views to be contained within your area then add : routeData.DataTokens["area"] = Request.RequestContext.RouteData.DataTokens["area"]; – Simon_Weaver Feb 15 '13 at 08:49
  • Rob's question is here: http://stackoverflow.com/questions/11317487/mvc3-custom-error-pages-gives-blank-result Returning Views works fine if the names are correct. – Jeroen K Jul 15 '13 at 07:58
  • How do you go about executing the controller action in MVC4 when `Execute` is now a protected method? – ajbeaven Oct 07 '13 at 09:58
  • 1
    @ajbeaven, `Execute` is a method defined in the `IController` interface. This cannot possibly be protected. Look at my code more carefully: `IController errorsController = new ErrorsController();` and notice the type of the `errorsController` variable on which I am invoking the `Execute` method. It's of type `IController` so there's absolutely nothing preventing you from calling this method. And by the way `Execute` was protected in the Controller class as well in MVC 3, so there's no change in this regard. – Darin Dimitrov Oct 07 '13 at 10:29
  • Argh, that'll teach me for using `var`. Thanks for pointing that out. – ajbeaven Oct 08 '13 at 04:56
  • Returning a view from ErrorController seems to show encoded html when a controller isn't matched by the route. For instance `/Account/nonexistentaction` shows the view correctly but `/nonexistentcontroller/nonexistentaction` shows the encoded html. Using MVC4. – ajbeaven Dec 20 '13 at 01:17
  • 2
    Fixed by explicitly specifying the content type of the response: `Response.ContentType = "text/html";` – ajbeaven Dec 20 '13 at 02:15
  • @ajbeaven, thank you sir. I had the same problem and adding `Response.ContentType = "text/html";` solved it :) – Ivan Stoyanov Feb 17 '14 at 12:09
  • An interesting side-effect that may explain some "white screen" responses: using code very similar to this, I noticed I was actually causing another exception after the error redirect by trying to read the User.Identity value of the Request; for whatever reason, it's null when executing the error action this way. – Tieson T. Aug 05 '14 at 06:11
  • What is the reason you need `Response.Clear();` in your solution, @Darin? I've been using this for a while but without that line and haven't observed any issues (yet). Would you mind to elaborate on why that line is needed, please? – Manfred Oct 06 '15 at 04:49
  • `Response` is throwing response is not available in this context. – ManirajSS Jan 29 '16 at 09:59
  • @DarinDimitrov Thanks a lot but I am not sure if it can also be used with **PartialViews**. Redirect is also a problem when preferring to use **PartialView** for error and exception handling. Could you please have a look at [Global Error handling using PartialView in MVC](http://stackoverflow.com/questions/39594409/global-error-handling-using-partialview-in-mvc)? – Jack Sep 20 '16 at 12:47
18

Here is more articles How to create custom error pages with MVC http://kitsula.com/Article/MVC-Custom-Error-Pages.

Igor
  • 3,576
  • 3
  • 23
  • 18
6

You can also do this in the Web.Config File. Here is an example that works in IIS 7.5.

     <system.webServer>
          <httpErrors errorMode="DetailedLocalOnly" defaultResponseMode="File">
                <remove statusCode="502" subStatusCode="-1" />
                <remove statusCode="501" subStatusCode="-1" />
                <remove statusCode="412" subStatusCode="-1" />
                <remove statusCode="406" subStatusCode="-1" />
                <remove statusCode="405" subStatusCode="-1" />
                <remove statusCode="404" subStatusCode="-1" />
                <remove statusCode="403" subStatusCode="-1" />
                <remove statusCode="401" subStatusCode="-1" />
                <remove statusCode="500" subStatusCode="-1" />
                <error statusCode="500" path="/notfound.html" responseMode="ExecuteURL" />
                <error statusCode="401" prefixLanguageFilePath="" path="/500.html" responseMode="ExecuteURL" />
                <error statusCode="403" prefixLanguageFilePath="" path="/403.html" responseMode="ExecuteURL" />
                <error statusCode="404" prefixLanguageFilePath="" path="/404.html" responseMode="ExecuteURL" />
                <error statusCode="405" prefixLanguageFilePath="" path="/405.html" responseMode="ExecuteURL" />
                <error statusCode="406" prefixLanguageFilePath="" path="/406.html" responseMode="ExecuteURL" />
                <error statusCode="412" prefixLanguageFilePath="" path="/412.html" responseMode="ExecuteURL" />
                <error statusCode="501" prefixLanguageFilePath="" path="/501.html" responseMode="ExecuteURL" />
                <error statusCode="502" prefixLanguageFilePath="" path="/genericerror.html" responseMode="ExecuteURL" />
           </httpErrors>
</system.webServer>
Brett Allred
  • 3,459
  • 26
  • 30
3

I see you added a config value for EnableCustomErrorPage and you're also checking IsDebuggingEnabled to determine whether or not to run your error handling.

Since there's already a <customErrors/> configuration in ASP.NET (which is meant exactly for this purpose) it's easiest to just say :

    protected void Application_Error()
    {
        if (HttpContext.Current == null) 
        {
                // errors in Application_Start will end up here                
        }
        else if (HttpContext.Current.IsCustomErrorEnabled)
        {
                // custom exception handling
        }
    }

Then in the config you'd put <customErrors mode="RemoteOnly" /> which is safe to deploy like that, and when you need to test your custom error page you'd set it to <customErrors mode="On" /> so you can verify that it works.

Note you also need to check if HttpContext.Current is null because an exception in Application_Start will still his this method although there won't be an active context.

Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
2

You can display a user-friendly error page with the correct http status code by implementing Jeff Atwood's User Friendly Exception Handling module with a slight modification for the http status code. It works without any redirects. Although the code is from 2004(!), it works well with MVC. It can be configured entirely in your web.config, with no MVC project source code changes at all.

The modification required to return the original HTTP status rather than a 200 status is described in this related forum post.

Basically, in Handler.vb, you can add something like:

' In the header...
Private _exHttpEx As HttpException = Nothing

' At the top of Public Sub HandleException(ByVal ex As Exception)...
HttpContext.Current.Response.StatusCode = 500
If TypeOf ex Is HttpException Then
    _exHttpEx = CType(ex, HttpException)
    HttpContext.Current.Response.StatusCode = _exHttpEx.GetHttpCode()
End If
Martin_W
  • 1,582
  • 1
  • 19
  • 24
0

I'm using MVC 4.5 and I was having issues with Darin's solution. Note: Darin's solution is excellent and I used it to come up with my solution. Here's my modified solution:

protected void Application_Error(object sender, EventArgs e)
{           
var exception = Server.GetLastError();
var httpException = exception as HttpException;
Response.StatusCode = httpException.GetHttpCode();

Response.Clear();
Server.ClearError();


if (httpException != null)
{
    var httpContext = HttpContext.Current;

    httpContext.RewritePath("/Errors/InternalError", false);

    // MVC 3 running on IIS 7+
    if (HttpRuntime.UsingIntegratedPipeline)
    {
        switch (Response.StatusCode)
        {
            case 403:
                httpContext.Server.TransferRequest("/Errors/Http403", true);
                break;
            case 404:
                httpContext.Server.TransferRequest("/Errors/Http404", true);
                break;
            default:
                httpContext.Server.TransferRequest("/Errors/InternalError", true);
                break;
        }
    }
    else
    {
        switch (Response.StatusCode)
        {
            case 403:
                httpContext.RewritePath(string.Format("/Errors/Http403", true));
                break;
            case 404:
                httpContext.RewritePath(string.Format("/Errors/Http404", true));
                break;
            default:
                httpContext.RewritePath(string.Format("/Errors/InternalError", true));
                break;
        }

        IHttpHandler httpHandler = new MvcHttpHandler();
        httpHandler.ProcessRequest(httpContext);
    }
}
}
MVCdragon
  • 93
  • 2
  • 8