Web Application Error Handling and Logging For ASP

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:

  1. Have a way to know automatically when an error occurs
  2. The user is very upset and calls your company

Wouldn't it be great to:

  1. Receive an automatic e-mail that contains details of the errors when an error occurs
  2. Get descriptive information about the error
  3. Know where in your code the error happened
  4. Log such information as the user, ip, and all form information
  5. Handle multiple applications and Web sites from one error handling page
  6. Provide a reference number and a friendly message to users
  7. 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

  1. Right clicking on a website and selecting properties
  2. Clicking the custom errors tab

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

Ex. -2147467238

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

Ex. /global.asa

Line - Indicates the line within the .asp file that generated the error

Ex. 16

Column - Indicates the column position within the .asp file that generated the error

Ex. 5

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:


<% 
   Dim clsTest
   Set clsTest = Server.CreateObject("SomeInvalidName.ClassName")
%>

Then the ASPErr object would contain:
----------------------------------
ASPCode: ASP 0177 
Number: -2147221005 
Source:  
Category: Server object 
File: /test.asp 
Line: 3 
Column: -1 
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
/test.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:
ASPCode  
Number -2146827282 
Source Response.Write (CurrentInterestRateEffectiveDate & 
" - " & DateAdd("m",3,CurrentInterestRateEffectiveDate) 
Category Microsoft VBScript compilation
File /navigator55integ_Local/proposalsmain/proposals/proposalscreen.asp 
Line 962 
Column 106 
Description Expected ')' 
ASPDescription 
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:

  1. Session ID
  2. Request Method
  3. Server Port
  4. Was it HTTP or HTTPs
  5. The server's address
  6. The user's address
  7. The user's browser type
  8. The URL that caused the error
  9. A reference id the customer can use when talking to support
  10. All of the posted form data
  11. All of the HTTP headers
  12. ASPCode
  13. Number
  14. Source
  15. Category
  16. File
  17. Line
  18. Column
  19. Description
  20. ASPDescription
  21. 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]
GO

Use 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%>
	<tr>
<td>ASPCode</td><td><%=AspErr.ASPCode%>

</td>
	</tr>
....
....
....
	<tr>
<td>URL</td><td><%=Request.ServerVariables("U
RL")%></td>
	</tr>

<%End If%>

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)


Sub LogErrorToDatabase()
Dim con
Dim rstError

	
	Set con = Server.CreateObject("ADODB.Connection")
	Set rstError = Server.CreateObject("ADODB.Recordset")
	
	con.open "dsn=ErrorLog;uid=ErrorLog;pwd=secret;"
	Set rstError.ActiveConnection = con
	rstError.Open "Select * From Errors",,3,3 'adOpenStatic, adLockOptimistic
	rstError.AddNew
	
	With rstError
		.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()
	End With
	
	rstError.Update
	rstError.Close
	con.Close
	Set rstError = Nothing
	Set con = Nothing

End Sub

Summary

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:

  1. Create the errorlog database from the sql script ErrorLog.sql
  2. You created the DSN named ErrorLog to access the database created in step 1
  3. You told IIS to handle 500;100 errors to the 500-100.asp custom error handler page in the custom errors section of the website properties in IIS.
  4. If you are debugging currently in Interdev, chances are a msgbox will popup telling you an error occured. This is because your running debugger (Interdev) will catch some IIS exceptions. Stop debugging, and it should go right to the error page. I recently installed the visual studio.net on my machine and I get a prompt asking me if I want to use the selected debugger when an error occurs. Just click no and it will flow through to the error page.

Other possibilities to add to this application might be:

  1. If your site has multiple applications on it, you could store an application variable containing an application name to log it.
  2. Log user information depending on your authentication methods.
  3. 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.
  4. 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 amt@gecko-software.com.



Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Protecting business operations means shifting the priorities around availability from disaster recovery to business continuity. Enterprises are shifting their focus from recovery from a disaster to preventing the disaster in the first place. With this change in mindset, disaster recovery is no longer the first line of defense; the organizations with a smarter business continuity practice are less impacted when disasters strike. This SmartSelect will provide insight to help guide your enterprise toward better …

  • Live Event Date: August 14, 2014 @ 2:00 p.m. ET / 11:00 a.m. PT Data protection has long been considered "overhead" by many organizations in the past, many chalking it up to an insurance policy or an extended warranty you may never use. The realities of today make data protection a must-have, as we live in a data driven society. The digital assets we create, share, and collaborate with others on must be managed and protected for many purposes. Check out this upcoming eSeminar and join eVault Chief Technology …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds