In the days of "classic" ASP you used to get cryptic—an often times downright misleading—error messages. "White pages" leave your visitors in the dark who don't know what happened and what to do next besides closing the browser or navigating away.
It's obvious that "white pages" are ugly but there's another angle to this—ASP.NET displays an exception stack trace which may reveal what your code does and how it works. These days, when the word "security" is in vogue, this could be a dangerous side effect.
Custom error pages are not luxury any more, they are a must-have. You have several ways to implement them.
Trapping Errors On Page Level
Every time you create a web form in Visual Studio .NET you see that your page class derives from System.Web.UI.Page
. The Page object helps you trap page-level errors. For this to happen you need to override its OnError
method as follows:
protected override void OnError(EventArgs e) { // At this point we have information about the error HttpContext ctx = HttpContext.Current; Exception exception = ctx.Server.GetLastError (); string errorInfo = "
Offending URL: " + ctx.Request.Url.ToString () + "
Source: " + exception.Source + "
Message: " + exception.Message + "
Stack trace: " + exception.StackTrace; ctx.Response.Write (errorInfo); // -------------------------------------------------- // To let the page finish running we clear the error // -------------------------------------------------- ctx.Server.ClearError (); base.OnError (e); }
This works for one page only, you may say. To have every page benefit from this kind of error handing we need to take advantage of the Page Controller pattern. You define a base class and have every page inherit from it. Downloadsample code for this article and see the CustomError1 project for an example.
Later on in this article you will learn why may need to collect exception information in this manner. Stay tuned.
Trapping Errors On Application Level
The idea of capturing errors on the application level is somewhat similar. At this point we need to rehash our understanding of the Global.asax
file.
From the moment you request a page in your browser to the moment you see a response on your screen a complex process takes place on the server. Your request travels through the ASP.NET pipeline.
In the eyes of IIS each virtual directory is an application. When a request within a certain virtual directory is placed, the pipeline creates an instance ofHttpApplication
to process the request. The runtime maintains a pool ofHttpApplication
objects. The same instance of HttpApplication
will service a request it is responsible for. This instance can be pooled and reused only after it is done processing a request.
Global.asax
is optional which means if you are not interested in any session or application events you can live without it. Otherwise the ASP.NET runtime parses your global.asax, compiles a class derived from HttpApplication
and hands it a request for your web application.
HttpApplication fires a number of events. One of them is Error
. To implement your own handler for application-level errors your global.asax
file needs to have code similar to this:
protected void Application_Error(object sender, EventArgs e) { }
When any exception is thrown now—be it a general exception or a 404—it will end up in Application_Error
. The following implementation of this handler is similar to the one above:
protected void Application_Error(Object sender, EventArgs e) { // At this point we have information about the error HttpContext ctx = HttpContext.Current; Exception exception = ctx.Server.GetLastError (); string errorInfo = "
Offending URL: " + ctx.Request.Url.ToString () + "
Source: " + exception.Source + "
Message: " + exception.Message + "
Stack trace: " + exception.StackTrace; ctx.Response.Write (errorInfo); // -------------------------------------------------- // To let the page finish running we clear the error // -------------------------------------------------- ctx.Server.ClearError (); }
Be careful when modifying global.asax. The ASP.NET framework detects that you changed it, flushes all session state and closed all browser sessions and—in essence—reboots your application. When a new page request arrives, the framework will parse global.asax and compile a new object derived from HttpApplication again.
Setting Custom Error Pages In web.config
If an exception has not been handed by the Page
object, or theHttpApplication
object and has not been cleared throughServer.ClearError()
it will be dealt with according to the settings ofweb.config
.
When you first create an ASP.NET web project in Visual Studio .NET you get aweb.config
for free with a small
With this setting your visitors will see a canned error page much like the one from ASP days. To save your face you can have ASP.NET display a nice page with an apology and a suggested plan of action.
The mode
attribute can be one of the following:
- On – error details are not shown to anybody, even local users. If you specified a custom error page it will be always used.
- Off – everyone will see error details, both local and remote users. If you specified a custom error page it will NOT be used.
- RemoteOnly – local users will see detailed error pages with a stack trace and compilation details, while remote users with be presented with a concise page notifying them that an error occurred. If a custom error page is available, it will be shown to the remote users only.
Displaying a concise yet not-so-pretty error page to visitors is still not good enough, so you need to put together a custom error page and specify it this way:
Should anything happen now, you will see a detailed stack trace and remote users will be automatically redirected to the custom error page,GeneralError.aspx
. How you apologize to users for the inconvenience is up to you. Ian Lloyd gives a couple of suggestions as to the look and feel of a custom 404 page.
The
tag may also contain several
(see MSDN) subtags for more granular error handling. Each
tag allows you to set a custom condition based upon an HTTP status code. For example, you may display a custom 404 for missing pages and a general error page for all other exceptions:
The URL to a custom error page may be relative (~/error/PageNotFound.aspx
) or absolute (http://www.yoursite.com/errors/PageNotFound.aspx
). The tilde (~) in front of URLs means that these URLs point to the root of your web application. Please download sample code for this article and see the CustomErrors3 project.
That's really all there's to it. Before we move on to the next (and last approach) a few words about clearing errors.
Clearing Errors
You probably noticed I chose to call Server.ClearError()
in both OnError
and Application_Error
above. I call it to let the page run its course. What happens if you comment it out? The exception will leave Application_Error
and continue to crawl up the stack until it's handled and put to rest. If you set custom error pages in web.config
the runtime will act accordingly—you get to collect exception information AND see a friendly error page. We'll talk about utilizing this information a little later.
Handling Errors In An HttpModule
Much is written about HTTP modules. They are an integral part of the ASP.NET pipeline model. Suffice it to say that they act as content filters. An HTTP module class implements the IHttpModule
interface (see MSDN). With the help ofHttpModule
s you can pre- and post-process a request and modify its content.IHttpModule
is a simple interface:
public interface IHttpModule { void Dispose(); void Init(HttpApplication context); }
As you see the context parameter is of type HttpApplication
. It will come in very handy when we write out own HttpModule
. Implementation of a simpleHttpModule
may look as follows:
using System; using System.Web; namespace AspNetResources.CustomErrors4 { public class MyErrorModule : IHttpModule { public void Init (HttpApplication app) { app.Error += new System.EventHandler (OnError); } public void OnError (object obj, EventArgs args) { // At this point we have information about the error HttpContext ctx = HttpContext.Current; Exception exception = ctx.Server.GetLastError (); string errorInfo = "
Offending URL: " + ctx.Request.Url.ToString () + "
Source: " + exception.Source + "
Message: " + exception.Message + "
Stack trace: " + exception.StackTrace; ctx.Response.Write (errorInfo); // -------------------------------------------------- // To let the page finish running we clear the error // -------------------------------------------------- ctx.Server.ClearError (); } public void Dispose () {} } }
The Init method is where you wire events exposed by HttpApplication
. Wait, is it the same HttpApplication
we talked about before? Yes, it is. You've already learned how to add handlers for various evens toGlobal.asax
. What you do here is essentially the same. Personally, I think writing an HttpModule
to trap errors is a much more elegant solution than customizing your Global.asax
file. Again, if you comment out the line withServer.ClearError()
the exception will travel back up the stack and—provided your web.config
is properly configured—a custom error page will be served.
To plug our HttpModule
into the pipeline we need to add a few lines toweb.config
:
As MSDN states the type
attribute specifies a comma-separated class/assembly combination. In our case MyErrorModule
is the name of a class from the AspNetResources.CustomErrors4
assembly. Tim Ewald and Keith Brown wrote an excellent article for MSDN Magazine on this subject.
You will find a full-fledged sample in the CustomErrors4 project in code download.
To gain deeper understanding of HttpModules
and their place in the HTTP pipeline I encourage you to search the web since the nuts and bolts are not relevant to the topic of error handling.
What about HTML pages?
What happens if you request a non-existent HTML page? This question comes up in news groups very often.
By default you will get a canned "white page". When you install the .NET Framework is maps certain file extensions to the ASP.NET ISAPI,aspnet_isapi.dll
. Neither HTML nor HTM files are mapped to it (because they are not ASP.NET pages). However, you can configure IIS to treat them as ASP.NET pages and serve our custom error pages.
- Run the IIS Manager
- Select a web application
- Right click and go to Properties
- On the Virtual Directory tab click Configuration
- In the Extension column find
.aspx
, double click and copy the full path toaspnet_isapi.dll
. It should be something likeC:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\«
aspnet_isapi.dll - Click Add and paste the path in the Executable box
- In the Extension box type
.html
- Make sure Check that file exists is NOT checked
- Dismiss all dialogs
If you type a wrong URL to an HTML page now you should get our user-friendly error page.
Analyze That
The reason we went this far with error trapping is the insight we gain about the problem. As strange as it may sound, exceptions are your friends because once you trap one you can alert responsible people right away. There are several ways to go about it:
- Write it to the system event log. You could have WMI monitor events in the log and act on them, too. See MSDN for more information
- Write it to a file
- Email alerts
Please refer to an excellent whitepaper, Exception Management Architecture Guide from Microsoft for a comprehensive discussion of different aspects of error handling.
I've implemented the last option—email alerts—in a production environment and it worked great. Once someone pulls up a (custom) error page we get an email and jump right on it. Given the fact that users grow impatient with faulty sites and web applications, it's critical to be notified of errors right away.
The Path of 404
As I was researching the topic of custom error pages I couldn't help wondering where 404s originate from and how we end up seeing custom 404 pages. To follow this exercise you will need MSDN and Lutz Roeder's Reflector.
When IIS receives a resource request it first figures out if it will process it directly or match against an ISAPI. If it is one of the ASP.NET resources, IIS hands the request to the ASP.NET ISAPI, aspnet_isapi.dll
.
For example, when a request for an .aspx page comes the runtime creates a whole pipeline of objects. At about this time an object of type HttpApplication
(which we already talked about) is instantiated. This object represents your web application. By tapping into the various events ofHttpApplication
you can follow request execution every step of the way (the image on the left shows the sequence of these events).
Next, HttpApplication
calls itsMapHttpHandler
method which returns an instance of IHttpHandler
(an object that implements IHttpHandler
, to be more precise). The IHttpHandler
interface is a very simple one:
public interface IHttpHandler { void ProcessRequest(HttpContext context); bool IsReusable { get; } }
The IsReusable
property specifies if the same instance of the handler can be pooled and reused repeatedly. Each request gets its own instance ofHttpHandler
which is dedicated to it throughout the lifetime of the request itself. Once the request is processed its HttpHandler is returned to a pool and later reused for another request.
The ProcessRequest
method is where magic happens. Ultimately, this method processes a request and generates a response stream which travels back up the pipeline, leaves the web server and is delivered to the client. How does HttpApplication
know which HttpHandler
to instantiate? It's all pre-configured in machine.config
:
......
This is only an excerpt. You have more HttHandlers
configured for you. See how .aspx files are mapped to the System.Web.UI.PageHandlerFactory
class?
To instantiate the right handler HttpApplication
calls its MapHttpHandler
method:
internal IHttpHandler MapHttpHandler ( HttpContext context, string requestType, string path, string pathTranslated, bool useAppConfig);
If you follow the assembly code of this method you will also see a call to thePageHandlerFactory.GetHandler
method which returns an instance ofHttpHandler
:
L_0022: call HttpApplication.GetHandlerMapping L_0027: stloc.2 L_0028: ldloc.2 L_0029: brtrue.s L_004a L_002b: ldc.i4.s 42 L_002d: call PerfCounters.IncrementCounter L_0032: ldc.i4.s 41 L_0034: call PerfCounters.IncrementCounter L_0039: ldstr "Http_handler_not_found_for_request_type" L_003e: ldarg.2 L_003f: call HttpRuntime.FormatResourceString L_0044: newobj HttpException..ctor L_0049: throw L_004a: ldarg.0 L_004b: ldloc.2 L_004c: call HttpApplication.GetFactory L_0051: stloc.3 L_0052: ldloc.3 L_0053: ldarg.1 L_0054: ldarg.2 L_0055: ldarg.3 L_0056: ldarg.s pathTranslated L_0058: callvirt IHttpHandlerFactory.GetHandler
Every ASP.NET page you write, whether you insert a base class in-between or not, ultimately derives from the System.Web.UI.Page
class. It's interesting to note that the Page class inherits the IHttpHandler
interface and is anHttpHandler
itself! What that means is the runtime will at some point callPage.ProcessRequest
!
Page.ProcessRequest
request delegates all work to its internal method,ProcessRequestMain
:
if (this.IsTransacted) { this.ProcessRequestTransacted(); } else { this.ProcessRequestMain(); }
Finally, ProcessRequestMain
is where all the fun stuff happens. Among all the many things it does, it defines an exception handler as follows:
Try { // code skipped catch (Exception exception4) { exception1 = exception4; PerfCounters.IncrementCounter(34); PerfCounters.IncrementCounter(36); if (!this.HandleError(exception1)) { throw; } } }
If you follow HandleError
further you'll notice that it will try to look up the name of your custom error page and redirect you to it:
if ((this._errorPage != null) && CustomErrors.GetSettings(base.Context).« CustomErrorsEnabled(this._request)) { this._response.RedirectToErrorPage(this._errorPage); return true; } internal bool RedirectToErrorPage(string url) { bool flag1; try { if (url == null) { flag1 = false; goto L_0062; } if (this._headersWritten) { flag1 = false; goto L_0062; } if (this.Request.QueryString["aspxerrorpath"] != null) { flag1 = false; goto L_0062; } if (url.IndexOf(63) < url =" string.Concat(url," aspxerrorpath="" flag1 =" false;">This method does you a favor by appending the offending URL. You can see
?aspxerrorpath=
in the URL each time a custom 404 page is displayed.If everything goes smooth—no exceptions thrown, no redirects to custom error pages—
ProcessRequest
finishes its job, and the response travels back through the pipeline.Conclusion
This article gave a detailed overview of ASP.NET custom error pages and several different approaches of setting them up. It's important that users see meaningful, friendly error pages. By the same token it's important that people on the other end are alerted about problems right away. ASP.NET provides a powerful and flexible framework to achieve both goals.
Комментариев нет:
Отправить комментарий