Error handling in MVC and nice error pages

It is vital for your application security not to show any internals when error happen. And you should be able to replace all internal error messages to nice user-friendly pages. It is a just nice for users – they are not getting splashes of oil, when engine is exploded, also another measure to improve site security.

There are lot of articles about error handling in ASP.Net MVC, but most of them do not cover the whole range. There is a very good resource on this, and I do recommend reading and understanding that first.

With error handling there are a lot of edge cases, and for every single one of them you need to provide a solution, otherwise your error messages will talk too loud about your implementation and that can lead to security vulnerability.

Upd 18/03/2016
There are a ton of similar articles on this topic. Here are some nice ones:

Here is the list of edge cases I came up with:

  1. Exception thrown in controller
  2. Controller or controller action is not found
  3. Page not found, but outside of the MVC pipeline
  4. Exception in IIS pipeline
  5. Cases when IIS can’t handle the request all together.

Exception thrown in controller.

When exceptions are thrown in your code, most of the time they will be thrown in MVC pipeline and handled by MVC error handling mechanisms. First of all you need enable CustomErrors in web.config:

<customErrors mode="On" defaultRedirect=".. will get to this later.." redirectMode="ResponseRewrite" />

For that you need to add HandleErrorAttribute to the list of MVC filters in your Global.asax.cs:

protected void Application_Start()
{
    // other configurations...

    GlobalFilters.Add(new HandleErrorAttribute());
}

This filter basically catches the exceptions from controllers and redirects users to ~/Views/Shared/Error.cshtml. The problem is that there is no controller behind this view and no easy way to log your errors. Fair enough, you probably have ELMAH writing exception messages and stack traces, but you need to check for that regularly. I prefer to look on my logs that show all messages across all our application instances. And then when debugging is required, I look on ELMAH. So here is my Error.cshtml

@model System.Web.Mvc.HandleErrorInfo

@{
    var logger = new LoggingService(.. your dependencies ..);
    logger.SetLoggerName("Internal Error Page");
    var exception = Model.Exception;

    logger.Error("Exception {0} thrown in controller {1} action {2}. Exception message: {3}", 
        exception.GetType(), Model.ControllerName, Model.ActionName, exception.Message);

    // if we are catching our Domain Exception, we want user to show the message.
    var domainException = exception as DomainException;

    var errorMessage = String.Empty;
    if (domainException != null)
    {
        errorMessage = domainException.Message;
    }
}

<html>
<head>
    <!-- include your CSS and JS here. -->
</head>
<body>  
    <h2>OOPS! Error Occurred</h2>
    @if (!String.IsNullOrEmpty(errorMessage))
    {
        <h3>Error Message: @errorMessage</h3>
    }

    Sorry about this.
</body>
</html>

Basically in the view itself, I create LoggerService and log a message. Then I check if the exception is meant to be visible by a user, and then show a message to the user. Nothing complex.

I’m doing dirty tricks here that should not be done at all! Probably there is a way to extend HandleErrorAttribute and redirect user to a page with controller, but I did not bother with it. This works as it is and if more logic is required in error handling, I can rectify that.

This parts deals with errors within MVC pipeline.

Page Not Found error

The solution above does not handle 404 errors: Page not found. And for that there are 2 cases: when the non-existing URL matches one of the MVC Routes, and does not match. You get different exceptions on these cases. Luckily, both of these are handled the same way. In web.config have this:

<customErrors mode="On" defaultRedirect="~/Content/Errors/page500.aspx" redirectMode="ResponseRewrite">
  <error statusCode="404" redirect="~/Content/Errors/page404.aspx" />
</customErrors>   

This says on all 404 pages, show page404.aspx. Probably it is possible to create MVC controller with action to show nice message, but I did not manage to get IIS to redirect to controller action on 404.

And here is page404.aspx:

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage" %>
<%@ Import Namespace="MyApplication.Domain.Services.Logging" %>
<%
    var logger = new LoggingService(ConfigurationContext.Current, new HttpLogMessageFormatter());
    logger.SetLoggerName("Page404");

    var url = HttpUtility.HtmlEncode(Request.Url.AbsoluteUri);
    logger.Error("Page not found: {0}", url);
%>

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Page not Found! Sorry about that</title>
</head>
<body>
    <h1>Page not found</h1>

    Sorry about this.
</body>
</html>

Again, on page load I write to log and then display some HTML. Same dirty tricks here: code in a view. I could’ve placed the code in code-behind file, but could not be bothered. Not much to it there.

Exceptions outside of the MVC pipeline

A potentially dangerous Request.Path
A potentially dangerous Request.Path

See that angle bracket at the end of the url. That breaks a lot of things. That is the exception in IIS pipeline. You can’t handle that in MVC code. That is only fixable in web.config:

<customErrors mode="On" defaultRedirect="~/Content/Errors/page500.aspx" redirectMode="ResponseRewrite">
  <error statusCode="404" redirect="~/Content/Errors/page404.aspx" />
</customErrors>

See that page500.aspx for the default error page. And here how it looks like:

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage" %>
<%
    var logger = new LoggingService();
    logger.SetLoggerName("Page500");
    var exception = Server.GetLastError();

    var message = String.Format("Unhandled Exception happened: {0}; with message: {1}", exception.GetType(), exception.Message);
    logger.ErrorException(message, exception);

%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Error occurred! Sorry about that</title>
</head>
<body>
<h2>OOPS! Error Occurred</h2>

Sorry about this.

<p> We have recorded this error and we will be looking into it. </p>
</body>
</html>

Again, exactly the same dirty tricks with code in a view. But this time we are getting last exception from the server and log it. This will handle all errors in IIS pipeline. Except those not in pipeline.

Missing static files

I have noticed that if you type a non-existing url and put a file extension on the end: http://example.com/blah.txt where blah.txt does not exist, you’ll get IIS error message:

2015-08-28 00_48_31-404 - File or directory not found.

This is IIS trying to be helpful and does not pass the request to MVC code and tries to serve the file. At this point IIS does not care about your <CustomErrors> section and serves you standard IIS page.

To overpower this issue you need to put this into your web.config:

<system.webServer>
    <modules>
      <remove name="UrlRoutingModule-4.0" />
      <add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" />
    </modules>
</system.webServer>

Exceptions before IIS pipeline

Apparently, even Bing search engine is suffering this problem. If you have a percent sign at the end of the url, IIS fails badly. Have a look on that live: http://www.bing.com/%. I did not even attempt to fix this. I’ve seen blog posts saying this is so deep inside of IIS, even Microsoft does not know how to fix it. So let be it.

I also encountered errors when you have www.example.com/blah. with a dot at the end of the url, the exception is not handled properly. But it looks like a problem of this particular configuration and my other sites are not affected.

But in case you are experiencing error messages when you have a dot at the end of the url, then here is the solution. You need to have URL Rewrite installed on your IIS and then in your web.config add the following rewrite rules in <system.webServer>:

  <rules>
    <rule name="Remove Trailing Dots after some text" stopProcessing="true">
      <match url="^(.*[^.])\.+$" />
      <action type="Rewrite" url="{R:1}" />
    </rule>
    <rule name="Remove All Dots" stopProcessing="true">
      <match url="^\.+$" />
      <action type="Rewrite" url="/" />
    </rule>
  </rules>
</rewrite>

First rule deals with dots after some path in url, i.e. example.com/blah.... . Second rule deals with only dots after the domain name, i.e. example.com/....