By Adam Tuliper
Let’s face it. Up until .NET there was no real error handling in ASP code. Sure, you have your “On Error Resume Next” and “On Error Goto 0”, but there was no good structured error handling. I can truly say there has not been one Web application I have come across of decent proportions that handles all errors that can arise. Almost every one would break if the database was down, rather than give the user a nice message. There comes a time in every developer’s life when he realizes he is lacking something. Most developers I come across lack error-handling skills.
I think that the most important aspect of an application is not its feature set, but how well it responds to the user, and this includes response to errors.
An application can be full of features, but one or two unhandled bugs in the right spot can ruin a great application. What I am hoping to show here is a great way to handle errors in Web applications. As Web developers, we have one easy advantage that fat-client development does not usually have. We have the ability to catch all of our errors from a top level and handle them gracefully.
How do you know if users are getting errors?
It’s the truth. Chances are if a user has a problem at your Web site, you will never know about it unless you:
- Have a way to know automatically when an error occurs
- The user is very upset and calls your company
Wouldn’t it be great to:
- Receive an automatic e-mail that contains details of the errors when an error occurs
- Get descriptive information about the error
- Know where in your code the error happened
- Log such information as the user, ip, and all form information
- Handle multiple applications and Web sites from one error handling page
- Provide a reference number and a friendly message to users
- Prevent evil-doers from possibly compromising your Web site
All when an error occurs?
This would enable you to be proactive, rather than reactive. Users who are on an intranet application who receive an error could actually be called about the error before their anger level rises against the IT staff. Your end users will be very happy. With a little bit of ASP coding, SQL, and a little help from IIS 5, this can all be accomplished. At the end of the article is a zip file that contains four files:
- 500-100.asp – The actual error-handling page
- ErrorLog.sql – A SQL script to create the database, table, and user for SQL Server
- TestPost.asp – A small web page with form data that posts to test.asp
- Test.asp – A small file that generates an error
Prevent Crucial Information From Leaking Out
This technique also helps make your Web site more hack proof.
First I must ask one thing of anyone reading this. Please, please, please validate your input from Web pages! Assume no input is safe. There are so many applications out there on the Internet that do not validate input. Please do not become another statistic. Check all of your Web applications and make sure you handle errors not only in your application, but also at a site level as mentioned in this article. There will probably be some bug we didn’t find in our application. So even with error handling on your pages, it may still be possible to raise an error in your application. These errors can give evildoers ways to get into the information in your site and do damage. As a side note, strip all invalid characters from input.
Getting Into The Details
When an unhandled error happens in IIS, IIS performs a Server.Transfer to one of many error pages. You can see the error pages defined in IIS by
When looking in this window, you will notice errors are listed by error code. Some of the errors will have a semicolon after them followed by another number. This denotes a “Sub Error Code”. Values from 100-199 are for informational messages. Values from 200-299 indicate the request was successful. Values from 300-399 indicate a redirection occurred on the server. Values from 400-499 indicate a client error occurred. Values from 500-599 indicate a server error occurred. Listed below are some error numbers and their descriptions (a generalized list is available at http://msdn.microsoft.com/library/en-us/act/htm/actml_ref_hsco.asp?frame=true)
400 Bad Request
401;1 Unauthorized – Login Failed
401;2 Unauthorized – Login failed due to server configuration
404 Not Found
500 Internal Server Error
500;12 Internal Server Error – Application Restarting
500;13 Internal Server Error – Server Too Busy
500;15 Internal Server Error – Direct Requests For Global.asa
500;100 Internal Server Error – ASP Error
The one we will concern ourselves with here is the 500;100 error. This error deals with the compiling or running of an ASP file and is raised automatically when there is an unhandled ASP error.
(note: if you want to implement your own “404 page not found” page with your own look and feel, here is where you would do it. Create your own Web page and edit the properties of the 404 error to point to your new page)
When an unhandled error occurs, IIS catches the exception and does a Server.Transfer to the error page specified in the Custom Errors configuration screen. A Server.Transfer takes all the information posted to one page and “Transfers” it to another page. This is great news for us because it means not only can we log the URL on which the error happened,
but we can also log all the form data that was posted to the page causing the error. This can go a long way in helping to reconstruct the error, especially when users don’t remember what they did.
What we will want to do is to create out own error page and put it somewhere on the Web site. I suggest selecting the 500;100 error in IIS, select “URL” for the message type. The URL should be based off of your Web site’s root. My recommended way is to create a folder or virtual directory off of your root Web called Errors. Place the new ASP file that we will create into this directory. In the 500;100 configuration screen, for the URL, enter in /Errors/500_100.asp and click OK.
Once IIS redirects to the error page, we will have several treasure chests of information available to us about the error and its source. In IIS, Server.GetLastError will return an ASPError object. This object contains the following properties:
ASPCode – Returns an error code generated by IIS. This can be empty in several cases; one being where you raise an error inside a component (Err.Raise) and your ASP code does not handle it.
Ex. ASP 0177 – The call to Server.CreateObject failed. The request object instance Cannot be created.
Number – Returns the standard COM error code
Source – Returns the actual source code, when available, of the line that caused the error
Category – Indicates if the source of the error was internal to ASP, the scripting language, or an object
File – Indicates the name of the .asp file that was being processed when the error occurred
Line – Indicates the line within the .asp file that generated the error
Column – Indicates the column position within the .asp file that generated the error
Description – Returns a short description of the error
Ex. Server.CreateObject Failed
ASPDescription – Returns a more detailed description of the error if it is an ASP-related error
Ex. The server process could not be started because the configured identity is incorrect. Check the username and password.
If I had a file named test.asp that contained:
Set clsTest = Server.CreateObject(“SomeInvalidName.ClassName”)
Then the ASPErr object would contain:
ASPCode: ASP 0177
Category: Server object
Description: Server.CreateObject Failed
ASPDescription: Invalid ProgID. For additional information specific to this
message please visit the Microsoft Online
Support site located at: http://www.microsoft.com/contentredirect.asp.
There are also some important pieces of information in the HTTP headers. We will want to log these also. They are very simply found in the Request.ServerVariables collection.
Request.ServerVariables(“ALL_HTTP”) – All of the HTTP headers (useful for systems like Netegrity and other authentication systems that rely on HTTP headers to pass information around)
Request.ServerVariables(“REQUEST_METHOD”) – This tells us if its a Get or Post, so we can then log the form parameters if its a post
Request.ServerVariables(“SERVER_PORT”) – Generally this is 80 (regular http port) or 443 if it is a secure connection
Request.ServerVariables(“HTTPS”) – This will be “off” or “on”
Request.ServerVariables(“LOCAL_ADDR”) – The Server’s addrss the error happened on. Crucial if you are running in an environment multiple servers that use one log file
Request.ServerVariables(“REMOTE_ADDR”) – The address of the client
Request.ServerVariables(“HTTP_USER_AGENT”) – Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.0)
Request.ServerVariables(“URL”) – The url of the page the error occured on
Accessing these parameters from the error page is simple. If my error page only contained the following code:
Set ASPErr = Server.GetLastError()
Response.Write ASPErr.ASPDescription & “<br>”
Response.Write Request.ServerVariables(“URL”) & “<br>”
Then my output would be:
Invalid ProgID. For additional information specific to this message please visit
the Microsoft Online Support site located at: http://www.microsoft.com/contentredirect.asp
Here is another example. If I forget a “)” on the end of my line of code then the error information will look like this:
Source Response.Write (CurrentInterestRateEffectiveDate &
” – ” & DateAdd(“m”,3,CurrentInterestRateEffectiveDate)
Category Microsoft VBScript compilation
Description Expected ‘)’
Of course we want a little bit more information than this. We also want to log this information from a database. But, what if the error page was invoked because the database server was down? This is quite possible, and luckily it is easy to get around. In my sample code you will notice that before the information is logged to the database, it is written to a test file. Once you get an error page running on your system, test it out to make sure the file you are writing to has appropriate permissions. By default, it will need write access for the IUSR_MACHINENAME account that your Web site is running under.
To sum everything up, here is the data we will log to a database:
- Session ID
- Request Method
- Server Port
- Was it HTTP or HTTPs
- The server’s address
- The user’s address
- The user’s browser type
- The URL that caused the error
- A reference id the customer can use when talking to support
- All of the posted form data
- All of the HTTP headers
- The date and time the error occurred.
Phew! This is a lot of great information. Most of this information should allow you to reconstruct an error on your system so you can zap those bugs. One idea that just came to mind, if you are having one of those killer bugs you just cannot get and you wish you had all of the prior form posted information, you could use Server.Transfer on a Web page to go to a logging page like I’ve described. The logging page would then log all of the form and URL information, then append a “Logged=Y” to the URL and then do a Server.Transfer to the original page. You would then have an entire log of every single bit of form data and URL information to track down an error. Cool, isn’t it? (Hey.. I think so.)
To log the information, let’s first create a database into which to log it all. Open up SQL Server’s query analyzer and run this script.
This will create the database and a table named errors. This could easily be converted to Microsoft access.
CREATE DATABASE [ErrorLog]
CREATE TABLE [dbo].[Errors] (
[ID] [int] IDENTITY (1, 1) NOT NULL ,
[SessionID] [char] (12) NULL ,
[RequestMethod] [char] (5) NULL ,
[ServerPort] [char] (5) NULL ,
[HTTPS] [char] (3) NULL ,
[LocalAddr] [char] (15) NULL ,
[HostAddress] [char] (15) NULL ,
[UserAgent] [varchar] (255) NULL ,
[URL] [varchar] (400) NULL ,
[CustomerRefID] [varchar] (20) NULL ,
[FormData] [varchar] (2000),
[AllHTTP] [varchar] (2000),
[ErrASPCode] [char] (10) NULL ,
[ErrNumber] [char] (11) NULL ,
[ErrSource] [varchar] (255) NULL ,
[ErrCategory] [varchar] (50) NULL ,
[ErrFile] [varchar] (255) NULL ,
[ErrLine] [int] NULL ,
[ErrColumn] [int] NULL,
[ErrDescription] [varchar] (1000) NULL ,
[ErrAspDescription] [varchar] (1000) NULL ,
[InsertDate] [datetime] NOT NULL
) ON [PRIMARY]
In the code that you will download, you will notice a section commented out like this:
<%If False Then%>
The reason I did this was to leave the code in it; you will want to use on your development system. It would be quite annoying if every time an error happened on your development system, you had to look at the database. So, uncomment this block by removing the “if” statement to use it on your development system. For your production systems, remove the entire block if you wish.
The meat of the database logging code looks like this:
(btw I do not recommend storing login names and passwords directly in the asp files /ever/, but it is included here just for simplicity)
Set con = Server.CreateObject(“ADODB.Connection”)
Set rstError = Server.CreateObject(“ADODB.Recordset”)
Set rstError.ActiveConnection = con
rstError.Open “Select * From Errors”,,3,3 ‘adOpenStatic, adLockOptimistic
.Fields(“SessionID”) = Session.SessionID
.Fields(“RequestMethod”).Value = Request.ServerVariables(“REQUEST_METHOD”)
.Fields(“ServerPort”).Value = Request.ServerVariables(“SERVER_PORT”)
.Fields(“HTTPS”).Value = Request.ServerVariables(“HTTPS”)
.Fields(“LocalAddr”).Value = Request.ServerVariables(“LOCAL_ADDR”)
.Fields(“HostAddress”).Value = Request.ServerVariables(“REMOTE_ADDR”)
.Fields(“UserAgent”).Value = Request.ServerVariables(“HTTP_USER_AGENT”)
.Fields(“URL”).Value = Request.ServerVariables(“URL”)
.Fields(“CustomerRefID”).Value = mstrCustRefID
.Fields(“FormData”).Value = Request.Form
.Fields(“AllHTTP”).Value = Replace(Request.ServerVariables(“ALL_HTTP”),vbLf,vbCrLf)
.Fields(“ErrASPCode”).Value = AspErr.ASPCode
.Fields(“ErrNumber”).Value = AspErr.Number
.Fields(“ErrSource”).Value = AspErr.Source
.Fields(“ErrCategory”).Value = AspErr.Category
.Fields(“ErrFile”).Value = AspErr.File
.Fields(“ErrLine”).Value = AspErr.Line
.Fields(“ErrColumn”).Value = AspErr.Line
.Fields(“ErrDescription”).Value = AspErr.Description
.Fields(“ErrAspDescription”).Value = AspErr.AspDescription
.Fields(“InsertDate”).Value = Now()
Set rstError = Nothing
Set con = Nothing
I cannot stress enough the importance of handling all errors that can come up in a Web application. By using a top-level error page like this, you can catch important error information while letting users know something will be done about their problem. I hope this article has tought you something important about application development. This will hopefully decrease your system’s down time by providing you with crucial information for finding and fixing errors.
To set this up on your system, simply download the zip file below and:
Other possibilities to add to this application might be:
- If your site has multiple applications on it, you could store an application variable containing an application name to log it.
- Log user information depending on your authentication methods.
- Add a trigger to the database or the Web page (components like ASPMail are good for this) to e-mail you when an error occurs.
- Create a Web page that simply queries these errors so you don’t always have to go back to the database to check for errors.
About the Author
Adam Tuliper’s professional career involes developing .Net, COM, VB, ASP, C, C++, and SQL Server solutions for business solutions and consumer applications.
He’s been developing for over six years professionaly. He is also the owner of gecko software, where they develop Internet center software, system monitoring, and security scanning software. He has a degree in CIS. He also serves as a security consultant to several companies. You can reach him at [email protected].