In my ASP.NET MVC application I want to have a generic error page that is showed together with returning HTTP 500. Something like this page on Stack Overflow.
Of course I'd like to reuse as much code as possible so that the page is styled the same way as other pages in my application, so I want to have an ASP.NET page reusing the master pages, not a plain HTML file. However if I do so I have a page that contains some server-side executable code for creating actual HTML served to the client. In case of Stack Overflow generic error page it's the code checking whether the user is registered, retrieving his rep, etc, and inserting elements for that into HTML.
That code can fail and if that happens the page isn't properly constructed and an exception is thrown in the server.
How do I handle that situation (error when building HTML for the error page) gracefully? What's the typical approach?
I like to start by categorize the possible errors.
Categorize the errors.
- Page Not Found (this is full my log).
- Breaking parameters of the page by trying to hack it
- Error from wrong user data input.
- Totally Unknown error - a new bug that we must fix.
- a Very hard General error - nothing runs - eg, database is not opens at all.
Our Goal
To now show the error page, but only in rare cases.
So when user make actions with out page, we try to capture all wrong input of the user, and show him what to do to continue with out any error.
Global Error Handler
You can start by capture on global.asax
all errors using the void Application_Error(object sender, EventArgs e)
void Application_Error(object sender, EventArgs e)
{
try
{
Exception LastOneError = Server.GetLastError();
if (LastOneError != null)
{
Debug.Fail("Unhandled error: " + LastOneError.ToString());
if (!(EventLog.SourceExists(SourceName)))
EventLog.CreateEventSource(SourceName, LogName);
EventLog MyLog = new EventLog();
MyLog.Source = SourceName;
StringBuilder cReportMe = new StringBuilder();
cReportMe.Append("[Error come from ip:");
cReportMe.Append(GetRemoteHostIP());
cReportMe.Append("] ");
cReportMe.Append("Last Error:");
cReportMe.Append(LastOneError.ToString());
if (LastOneError.ToString().Contains("does not exist."))
{
// page not found
MyLog.WriteEntry(cReportMe.ToString(), EventLogEntryType.Warning, 998);
}
else
{
MyLog.WriteEntry(cReportMe.ToString(), EventLogEntryType.Error, 999);
}
}
}
catch (Exception ex)
{
Debug.Fail("Unhandled error: " + GlobalFun.GetErrorMessage(ex));
}
string cTheFile = HttpContext.Current.Request.Path;
// to avoid close loop and stackoverflow
if(!cTheFile.EndsWith("error.aspx"))
Server.Transfer("~/error.aspx");
}
This global error handle have one main goal, to tell me whats is not working and fail to handle it before reach here, so I log it (not manner the way of log it) and fix it soon. When I debug my code I did not make the server transfer to be able to locate the error fast.
For the hacking error case
In this case is better to now show any error but just reload the page by making a redirect. How to know if its trying to hack the page ?
If on post back you get some CRC/hach error of your parameters you know that. Look that answer https://stackoverflow.com/a/2551810/159270 to see an example of a viewstate error and how to handle it.
I know that MVC did not have viewstate, but you may have other encrypted string on your code, or some kind of security that you can know when its broken. This is the general idea:
if(IsPostBack && HashErrorOnParametres)
{
LogIt();
Responce.Redirect(Request.RawUrl, true);
return;
}
For a very hard General Error
Let say that your database is not open at all, then all users start to see the error page from the general handler. There you may have an extra option to restart the pool, or stop the pages after 20 errors in the last 5 minute, or something similar, and send you email to run and fix this very hard error.
For the rest soft errors
I think that all the possible known errors must be handled inside the page using try/catch and show to the user a message for what go wrong, if this error is from the user, and of course log it to see it and fix it.
Breaking the page in parts, maybe one part is throw the error and the rest working good, if this is not so important you can simple hide this part, and show the rest, until you fix it. For example if you just show information's about a product, and a part that speak about one part is throw an error, you can simple hide this part and see it on the log and fix it.
Some time I was try to handle the error per page, using protected override void OnError(EventArgs e)
but this is not help me, and I remove it. I handle the errors per action, and if they are not critical I hide them until I fix them.
The rest normal errors is shown on the user, eg is not entered the correct data... a message for that and tell them what to fix.
As I say, my goal is to not show the error page at all.
Basically in your web.config you need something like this:
<customErrors mode="RemoteOnly" defaultRedirect="~/ErrorPages/Oops.aspx">
</customErrors>
Naturally the path to your error page will differ to the above.
You can get more general with how you deal with specific errors, such as a 404 like this:
<customErrors mode="RemoteOnly" defaultRedirect="~/ErrorPages/Oops.aspx">
<error statusCode="404" redirect="~/ErrorPages/404.aspx" />
</customErrors>
EDIT:
Based on the comments, here's what I'm getting at:
When you build the "oops.aspx" page I would recommend that you keep it simple; if possible, have no executing code at all. If you do need to execute code in "oops.aspx" then place that code in a try..catch
block and eat up the exception:
try {
// Your oops code.
}
catch (Exception e) {
// Do nothing with the exception.
// Maybe plonk it in a log file, or System.Diagnostics.Trace.WriteLine(e.Message);
}
The point is that you want to avoid having "oops.aspx" called because "oops.aspx" is causing an exception itself! Recursive problems ahoy.
You don't need to put a try...catch in the master page (unless you want to), my point was simply concentrating on the "oops.aspx" page.