In IIS 5.0 and above, there is a new object called the ASPError object, which allows you to have more control over how your errors appear to users, and how you can track these errors and prevent them from occuring in the future. This article will quickly guide you through creating a tiny application that will track 500 errors and allow you to review them at the end of the day. The article assumes you are using SQL Server as your database backend, but could be modified easily to work with Access or any other database platform.
Before we start, let's force a pretty simple 500 error (save it as <wwwroot>\force500error.asp) with the following code:
<% functionThatDoesNotExist() %> |
This should result in the following error:
Microsoft VBScript runtime error '800a000d' Type mismatch: 'functionThatDoesNotExist' /force500error.asp, line 2 |
Now, let's create a simple example of a 500 error handler page. First, create the following ASP page:
<% Set ASPErr = Server.GetLastError Response.Write ASPErr.Description %> |
Save this file in <wwwroot>\500_try.asp (the default file in IIS for handling custom 500 errors is called 500-100.asp - we should leave this file alone, in case we need to restore the default behavior at some point). Now, go into Internet Services Manager, right-click the Default Web Site (or any web site or application where you want to isolate this test), choose properties, on the Custom Errors tab, scroll down to 500;100 and hit Edit, verify that Message Type has "URL" selected, and make sure the URL points to /500_try.asp (or wherever you placed it).
Once you've done this, you should get a prettier error when you hit force500error.asp:
| Type mismatch: 'functionThatDoesNotExist' |
That's just for starters. You can customize the 500 handler page later by adding your web site graphics, navigation, and even other content. Keep in mind that, like the custom 404 error page described in
Article #2162, path is important when defining the images and other references in your 500 handler page. If this page is expected to handle errors in ASP pages in the root of your site / application as well as sub-folders, all possible locations need to be aware of the exact path to any referenced files.
All right, so let's create a table that will store the information we might want to track. In addition to the extra information provided by the ASPError object that is not available with the traditional ON ERROR RESUME NEXT architecture, it might also be useful to store referrer, user agent, ip and post data as well. So our table will look like this (and since we are going to query by date after the fact, we put a clustered index on the datetime column):
CREATE TABLE HTTP500Errors ( id INT IDENTITY(1,1) PRIMARY KEY NONCLUSTERED, dt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, url VARCHAR(1024) NOT NULL DEFAULT '', aspDesc VARCHAR(512) NOT NULL DEFAULT '', aspNumber VARCHAR(16) NOT NULL DEFAULT '', aspLine INT NOT NULL DEFAULT 0, aspColumn INT NOT NULL DEFAULT 0, errDesc VARCHAR(512) NOT NULL DEFAULT '', errNumber VARCHAR(16) NOT NULL DEFAULT '', ref VARCHAR(1024) NOT NULL DEFAULT '', ip VARCHAR(15) NOT NULL DEFAULT '', ua VARCHAR(1024) NOT NULL DEFAULT '', method VARCHAR(4) NOT NULL DEFAULT 'GET', data TEXT NOT NULL DEFAULT '' ); GO CREATE CLUSTERED INDEX ci_HTTP500Errors_dt ON HTTP500Errors(dt); GO -- replace ??? with the user you connect to from ASP GRANT DELETE, INSERT, SELECT, UPDATE ON HTTP500Errors TO [???] GO |
And the following stored procedure will facilitate inserting data into this table when an error occurs:
CREATE PROCEDURE dbo.logHTTP500Errors @url VARCHAR(1024)='', @aspDesc VARCHAR(512)='', @aspNumber VARCHAR(16)='', @aspLine INT=0, @aspColumn INT=0, @errDesc VARCHAR(512)='', @errNumber VARCHAR(16)='', @ref VARCHAR(1024)='', @ip VARCHAR(15)='', @ua VARCHAR(1024)='', @method VARCHAR(4)='GET', @data TEXT='' AS BEGIN SET NOCOUNT ON; INSERT HTTP500Errors ( url, aspDesc, aspNumber, aspLine, aspColumn, errDesc, errNumber, ref, ip, ua, method, data ) SELECT @url, @aspDesc, @aspNumber, @aspLine, @aspColumn, @errDesc, @errNumber, @ref, @ip, @ua, @method, @data; END GO -- replace ??? with the user you connect to from ASP GRANT EXEC ON logHTTP500Errors TO [???] GO |
So now that we have a table to store our data, and a stored procedure to interface with it, let's create the following ASP page to replace 500_try.asp from above:
<% Response.Status = "500 Internal Server Error" Set ASPErr = Server.GetLastError() ' build the URL string strURLPrefix = "http://" if Request.ServerVariables("HTTPS") <> "off" then strURLPrefix = "https://" strPageName = Request.ServerVariables("SCRIPT_NAME") strServerName = Request.ServerVariables("SERVER_NAME") strURL = strURLPrefix & strServerName & strPageName ' get the extended error information strASPDesc = ASPErr.ASPDescription strASPNumber = ASPErr.ASPCode intASPLine = clng(ASPErr.Line) intASPColumn = clng(ASPErr.Column) strDesc = ASPErr.Description strNumber = "0x" & Hex(ASPErr.Number) ' and some extra info, such as user agent strRef = Request.ServerVariables("HTTP_REFERER") strIP = Request.ServerVariables("REMOTE_ADDR") strUA = Request.ServerVariables("HTTP_USER_AGENT") strMethod = Request.ServerVariables("REQUEST_METHOD") strData = Request.QueryString() if strMethod = "POST" then strData = Request.Form() ' let's build our SQL string: sql = "EXEC dbo.logHTTP500Errors " & _ " @url='" & fixSQL(strURL, 1024) & "'," & _ " @aspDesc='" & fixSQL(strASPDesc, 512) & "'," & _ " @aspNumber='" & fixSQL(strASPNumber, 16) & "'," & _ " @aspLine=" & intASPLine & "," & _ " @aspColumn=" & intASPColumn & "," & _ " @errDesc='" & fixSQL(strDesc, 512) & "'," & _ " @errNumber='" & fixSQL(strNumber, 16) & "'," & _ " @ref='" & fixSQL(strRef, 1024) & "'," & _ " @ip='" & fixSQL(strIP, 16) & "'," & _ " @ua='" & fixSQL(strUA, 1024) & "'," & _ " @method='" & fixSQL(strMethod, 4) & "'," & _ " @data='" & fixSQL(strData, len(strData)) & "'" set conn = CreateObject("ADODB.Connection") ' don't forget to update your connection string! conn.open "<connection string>" conn.execute(sql) conn.close set conn = nothing ' a little function to trim the data in case some of it ' exceeds bounds, and replace those pesky apostrophes function fixSQL(str, intLength) fixSQL = left(replace(str,"'","''"), intLength) end function ' tell the user something (you can enter your ' site's graphics and navigation etc. here) Response.Write "There was an error on this page. The server " & _ " administrator has been notified and will investigate." %> |
This ASP page will store all the relevant data. You can test it by hitting force500error.asp again, and then using Query Analyzer to run a quick SELECT * query:
| SELECT * FROM HTTP500Errors |
You should see the results of any 500 errors that have occured since you set up the table and new 500 ASP page.
Now here is a stored procedure that will return all of the 500 errors that occurred during the current day (you can modify this to go back to the beginning of the week, or the beginning of the month if you like):
CREATE PROCEDURE dbo.retrieveHTTP500ErrorsToday AS BEGIN SET NOCOUNT ON; DECLARE @dt DATETIME; SET @dt = CONVERT(DATETIME, CONVERT(CHAR(10), GETDATE(), 101)); SELECT id, dt, url, aspDesc, aspNumber, aspLine, aspColumn, errDesc, errNumber, ref, ip, ua, method, data FROM HTTP500Errors WHERE dt >= @dt ORDER BY dt; END GO -- replace ??? with the user you connect to from ASP GRANT EXEC ON dbo.retrieveHTTP500ErrorsToday TO [???] GO |
And here is a crude ASP page to review the results:
<% set conn = CreateObject("ADODB.Connection") conn.open "<connection string>" set rs = conn.execute("EXEC dbo.retrieveHTTP500ErrorsToday") if not rs.eof then do while not rs.eof response.write "<hr size=1>" response.write rs("errNumber") & ": " & rs("errDesc") & "<br>" response.write rs("url") & "<br>" response.write rs("dt") response.write " (link to details record here...)" rs.movenext loop response.write "<hr size=1>" else response.write "No 500 errors today." end if conn.close set conn = nothing %> |
I'll leave the extended details page as an exercise for the reader.
Note that Server.GetLastError and the ASPError object are *only* available in the custom 500 error handler. In your normal ASP pages, they will not work as illustrated above. For more information, see the
ASPError Object and
Server.GetLastError topics in MSDN's Online Library.