IIS 7 Error Pages taking over 500 Errors
Recently I’ve run into a problem on my production server where results that produce errors that are handled by the application (either in Application_Error or in some cases for JSON or XML based custom services) would return IIS error pages instead of my custom error content generated in code. Basically what’s happening in all of this handled error code is that the application somehow intercepts an error – either Application_Error, or in the case of a JSON service an exception in the RPC call for example – and an error handler kicks in to handle the problem. The error handler then sets the status code to 500 and generates some output that is appropriate. For Application_Error a custom HTML page is generated . For the JSON service a custom JSON error object is returned.
For example the following is the error output routine in my custom JSON Request processor:
/// <summary> /// Returns an error response to the client from a callback. Code /// should exit after this call. /// </summary> /// <param name="ErrorMessage"></param> public void WriteErrorResponse(string errorMessage, string stackTrace) { CallbackException Error = new CallbackException(); Error.message = errorMessage; Error.isCallbackError = true; Error.stackTrace = stackTrace; JSONSerializer Serializer = new JSONSerializer(); string Result = Serializer.Serialize(Error); HttpResponse Response = HttpContext.Current.Response; Response.ContentType = ControlResources.STR_JsonContentType; Response.StatusCode = 500; Response.Write(Result); HttpContext.Current.ApplicationInstance.CompleteRequest(); //Response.End(); }
The status code is set to 500, and in this case a JSON error object is written to the response. The idea is that the request should generate a 500 error, but still provide appropriately formatted error information – in this case JSON – to the client. This works fine on my development machine on Vista with IIS 7, but fails in the live environment on IIS 7 Windows Server 2008.
On the live server I the response that the server generates is not my JSON object, but rather than HTML error page that IIS generates which results in an error without an information of what went wrong. So instead of a message that lets the user know that something went wrong (like Login first please) the result comes back with a non-descript error message:
rather than the more descriptive error message that should be returned. Not cool!
The above is maybe a little bit specialized of an example since it involves a custom handler and a JSON HTTP response returned. But this issue really affects any server response that needs to return a controlled 500 error. Another example, and that’s likely more inline what most of you are doing: Trying to generate custom dynamic Application_Error messages that display error messages specific to the application and include specific application related information in it that static error pages can handle.
I tend to have fairly elaborate Application_Error handlers that allow me to switch error display modes easily for example, and allow users to create custom error pages through the admin interface. Here’s a small snippet of the handler that returns a generic error message from the application.
When an error occurs in the application Application_Error fires and eventually this code block is fired:
else if (App.Configuration.DebugMode == DebugModes.ApplicationErrorMessage) { string stockMessage = App.Configuration.ApplicationErrorMessage; // Handle some stock errors that may require special error pages HttpException httpException = serverException as HttpException; if (httpException != null) { int HttpCode = httpException.GetHttpCode(); Server.ClearError(); if (HttpCode == 404) // Page Not Found { Response.StatusCode = 404; MessageDisplay.DisplayMessage("Page not found", "You've accessed an invalid page on this Web server. " + stockMessage); return; } if (HttpCode == 401) // Access Denied { Response.StatusCode = 401; MessageDisplay.DisplayMessage("Access Denied", "You've accessed a resource that requires a valid login. " + stockMessage); return; } } // Display a generic error message Server.ClearError(); Response.StatusCode = 500; MessageDisplay.DisplayMessage("Application Error", "We're sorry, but an unhandled error occurred on the server. " + stockMessage); return; } return;
that is supposed to return a custom error page. However, I still get the IIS 500 error page instead:
What’s confusing about this is that the above error is an IIS error not an ASP.NET error. The real confusion here occurs because the error is trapped by ASP.NET, but then ultimately still handled by IIS which looks at the 500 status code and returns the stock IIS error page. There are two layers of indirection at work here – the ASP.NET customErrors settings and the IIS error page handlers.
Specifically this relates to the Error Pages server setting in the IIS admin interface:
which effectively tells IIS how to display errors. What’s not so obvious here is that this setting overrides the local Web.config setting of <customErrors> – if a 500 error is returned the above error kicks in but only if customErrors is kicking in (On or RemoteOnly). This behavior is new to IIS 7 I believe, maybe even new to IIS 7 SP1 and related to the excellent new server information that is displayed on these error pages when running locally. The behavior I describe realistically applies only to Integrated Mode – the classic ISAPI mode still handles errors the way they always were handled.
What really bites about this is that it’s hard to catch this particular issue. In development you typically never actually see this because you’re likely to be running with RemoteOnly for <customErrors<, so at development the behavior is actually just peachy – you see your code generated errors just fine. The error pages are ignored and ASP.NET does as ASP.NET should do. However, once customErrors are on the behavior changes and the IIS error pages take over.
This is just another reason to test your application in a staging environment or at least enable a full non-debug configuration on your application and test. This is also one reason why every app I have I usually have a test page that bombs on purpose to see what the error behavior is so I can quickly test it in the production environment and get an idea what errors do. Now that I think about it it might be prudent to set up a test page that checks both core errors and ‘service’ style errors from things like a JSON or XML service as well.
Anyway, I’d argue that this is a bug. While I agree that IIS should return error pages on completely unhandled errors and display the generic 500 error page, I do feel that IIS should always respect the output generated by the application if there’s no hard error that rippled all the way into the runtime (ie. no Application_Error handler).
Enter Response.TrySkipIisCustomErrors
There’s a solution to this problem with the deftly named TrySkipIisCustomErrors property on the Response object which is tailor made for this particular scenario. In a nutshell this property when set to true at any point in the request prevents IIS from injecting its custom error pages. This flag is new in ASP.NET 3.5 – so if you’re running 2.0 only you’re out of luck.
So in my error handler code above in Application_Error I can now add this flag and bingo I’m back in business getting my own custom error page to display:
// Display a generic error message Server.ClearError(); Response.StatusCode = 500; Response.TrySkipIisCustomErrors = true; MessageDisplay.DisplayMessage("Application Error", "We're sorry, but an unhandled error occurred on the server. " + StockMessage); return;
Same with my JsonCallbackMethodProcessHandler which returns JSON on errors:
public void WriteErrorResponse(string errorMessage, string stackTrace) { CallbackException Error = new CallbackException(); Error.message = errorMessage; Error.isCallbackError = true; Error.stackTrace = stackTrace; JSONSerializer Serializer = new JSONSerializer(); string Result = Serializer.Serialize(Error); HttpResponse Response = HttpContext.Current.Response; Response.ContentType = ControlResources.STR_JsonContentType; Response.TrySkipIisCustomErrors = true; Response.StatusCode = 500; Response.Write(Result); HttpContext.Current.ApplicationInstance.CompleteRequest(); //Response.End(); }
Thanks to Eilon Lipton, and Michael Campbell for pointing out that there is such a beast as Response.TrySkipIisCustomErrors – not where I would have looked for sure.
It seems to me that the better default for TrySkipIisCustomErrors should be true to begin with. After all that’s why we run ASP.NET - to be control of our environment.
Other Posts you might also like
HttpResponse.TrySkipIisCustomErrors Property
Gets or sets a value that specifies whether IIS 7.0 custom errors are disabled.
true
to disable IIS custom errors; otherwise, false
.
The TrySkipIisCustomErrors property is used only when your application is hosted in IIS 7.0 and later. When running in Classic mode, the TrySkipIisCustomErrors property default value is true
. When running in Integrated mode, the TrySkipIisCustomErrors property default value is false
.