Turns out if you want custom error pages, prepare for a lot of frustration and that dirty feeling afterwards.

First of all – mvc does not handle errors well. They claim different, but that’s not true. Let’s take a look at just a few possible errors that you can run into in a web app.

500 – internal server error

This one is mostly caused by an unhandled exception in your app. Null reference, invalid operation, whatever – if you don’t catch it, this (again in most cases) results in a 500.

This is the only problem that mvc knows how to deal with – HandleErrorAttribute. If you’re using that, have customErrors turned on in web.config and have a “Error.cshtml” file in your Views/Shared folder that’s what will be displayed. The key thing is that your code needs to be executed and throw an error, that will be detected by the attribute, that will notify the framework (mvc) to display a custom page.

404 – not found

This is an interesting one. You can trigger this in at least 3 ways.

First, in your code – so for example you try to fetch an entity by id which doesn’t exist – the good (or is it?) way to handle it is to return a HttpNotFoundResult() – there’s even a helper method for that on the Controller class called “HttpNotFound” with a param (after recent events I actually don’t recommend to specify it)

Second, by providing a url which matches a defined route but is not there. Example route {controller}/{action} and you go to foo/bar and there’s not controller named FooController. This 404 is triggered by mvc and can be handled by setting ‘the right values’ (more on that later) in customErrors tag

Third, by providing a url which does not match a route – usually strayed static files, links from previous version of a site, or pretty much anything else. This is handled by IIS and can be overwritten by setting ‘the right values’ in system.webServer/httpErrors in web.config.

400 – bad request

This one appears if you have request validation turned on (by default you do) and the incoming request is considered potentially dangerous (forbidden characters, posting malicious input…). This is handled by IIS and…is kind of hard to overwrite. The reason why I’m mentioning this one ‘by name’ is that it’s often a result of script attack attempts.

The security ones

The others

There are a lot of other status codes. Here’s the spec. All handled by IIS, and like the previously mentioned 400, kind of hard to overwrite.

Why?

So what’s this whole thing about? Well, the proper thing to do, in a website is to provide a user with a decent error page and a nice 404. The reason why 404 is so special is the use case, I hope I don’t need to explain that. If you don’t know, google around, I’m sure there’s plenty of lectures about that.

Now what about that error page. Well, it’s pretty obvious that if your code has a problem, the user should be notified about it. If the user is a person sitting in front of the monitor, then show him a nice page, if the user is a client site/app/lib/bot/spider, then they really don’t care about the page you display, they care about the response headers – mainly (if not solely) the status code.

That’s why it’s important to keep both set up properly.

Now let’s go to dreamland for a while. What would the task of setting these up look like in there? My hope would be that you provide a template/view (preferably static but the framework should allow to use server generated pages as well) for any response that is considered an error and display that to humans, plus, provide status codes that suit the error (so return a 404 if a page is not found – not a 500, not a 200 – an error is an error, and a specific one). That would be perfect (if you want a glimpse of that, learn django).

How?

Getting back to reality – mvc (and at the time of writing this article the latest stable release is 5.1.2) does not handle that well. I really hope that version 6 with it’s newly adapted OWIN architecture will handle this better, but only time will tell. So how to achieve this at mvc’s current state? I’ve already mentioned a few things, but let me get all of this into one place.

Yes, you see files ending with .aspx and I’m using Razor – that’s life.

So what do we have here. A 404.aspx, 404.html, Error.aspx. That’s not the end.

Now, let’s see the Views/Shared/Error.cshtml:

1: @{ Layout = null; }

2: @Html.Partial("~/Error.html")

That’s another file – Error.html

So in total, we have 5 files. That should handle 404s separately and the others separately. If you want to handle 500 with yet another page, you need to create 2 more files.

To at least try to keep things dry, I render the .html inside the .aspx so e.g. 404.aspx looks like this:

1: <% Response.StatusCode = 404%>

2: <% Response.WriteFile("~/404.html")%>

Now for the bad news. With all the “~” party going on, this will not work if your app is set up under a path other than root. So if you have yourdomain.com/fooapp and yourdomain.com/barapp you need to adjust accordingly.

There’s another component…

Logging these errors is helpful, for that I recommend ELMAH. But if you can’t use it for some reason, dealing with this task most likely will end with a custom http module – as it is in my case. I’ll skip the logging part cause that’s not the subject of this (already long) post, but there’s one thing I do want to mention.

If at some point you do a Server.ClearError() you loose a lot of info – try to mitigate that. For one, keep the status code, second – keep the exception (either log or show it somewhere), third, to display a custom error page, more hacking follows:

1: httpContext.Server.ClearError();

2: httpContext.Response.Clear();

3: httpContext.Response.TrySkipIisCustomErrors = true;

4: httpContext.Response.WriteFile("~/Error.html");

5: httpContext.Response.StatusCode = httpCode;

6: httpContext.Response.ContentType = "text/html; charset=utf-8";

7: httpContext.Response.End();

There are 2 magical lines there. Line 3, which makes this work, and line 6 which prevents some (don’t ask me why some and not all) the error pages to be rendered as plaintext.

So here, that’s my take on the case, it works for cases that caused all the commotion, but I have no idea how this thing will behave if you start serving it with other errors (or authentication/authorization issues maybe)