Tallan's Technology Blog

Tallan's Top Technologists Share Their Thoughts on Today's Technology Challenges

Automatically log errors with Elmah in ScriptService-WebService calls

Karl Schwirz

If you’ve used Elmah logging in the past then you know it’s a very useful and easy to set up tool in your web applications.  However, when we encounter errors in web services we sometimes do not get the desired logging into the xml file or database, depending on how you have Elmah configured.  This is because when a error occurs in a script service enabled web service, the errors get serialized into JSON and returned to the ajax caller which is then take by the javascript to handle the exception.  The application is left unaware of the exception occurrence.

There is a way however, to intercept the message before it is returned to the front-end and log the error automatically the way we are used to with Elmah.

First we set up an ErrorHandlerFilter class which will override the default error stream message ( {“Message”:”There was an error processing the request.”, “StackTrace”:””, “ExceptionType”:””})} with a more meaningful error message with details.

   1: using System;

   2: using System.Collections.Generic;

   3: using System.IO;

   4: using System.Linq;

   5: using System.Text;

   6:

   7: namespace Namespace.ElmahExtension

   8: {

   9:     public class ErrorHandlerFilter : Stream

  10:     {

  11:

  12:         private readonly Stream _responseFilter;

  13:

  14:         public List<byte> OriginalBytesWritten { get; private set; }

  15:

  16:         private const string Content =

  17:           "{\"Message\":\"There was an error processing the request.\"" +

  18:           ",\"StackTrace\":\"\",\"ExceptionType\":\"\"}";

  19:

  20:         public ErrorHandlerFilter(Stream responseFilter)

  21:         {

  22:             _responseFilter = responseFilter;

  23:             OriginalBytesWritten = new List<byte>();

  24:         }

  25:

  26:         public override void Flush()

  27:         {

  28:             byte[] bytes = Encoding.UTF8.GetBytes(Content);

  29:             _responseFilter.Write(bytes, 0, bytes.Length);

  30:             _responseFilter.Flush();

  31:         }

  32:

  33:         public override long Seek(long offset, SeekOrigin origin)

  34:         {

  35:             return _responseFilter.Seek(offset, origin);

  36:         }

  37:

  38:         public override void SetLength(long value)

  39:         {

  40:             _responseFilter.SetLength(value);

  41:         }

  42:

  43:         public override int Read(byte[] buffer, int offset, int count)

  44:         {

  45:             return _responseFilter.Read(buffer, offset, count);

  46:         }

  47:

  48:         public override void Write(byte[] buffer, int offset, int count)

  49:         {

  50:             for (int i = offset; i < offset + count; i++)

  51:             {

  52:                 OriginalBytesWritten.Add(buffer[i]);

  53:             }

  54:         }

  55:

  56:         public override bool CanRead

  57:         {

  58:             get { return _responseFilter.CanRead; }

  59:         }

  60:

  61:         public override bool CanSeek

  62:         {

  63:             get { return _responseFilter.CanSeek; }

  64:         }

  65:

  66:         public override bool CanWrite

  67:         {

  68:             get { return _responseFilter.CanWrite; }

  69:         }

  70:

  71:         public override long Length

  72:         {

  73:             get { return _responseFilter.Length; }

  74:         }

  75:

  76:         public override long Position

  77:         {

  78:             get { return _responseFilter.Position; }

  79:             set { _responseFilter.Position = value; }

  80:         }

  81:     }

  82: }

Now, we’re going to need to set up a class that implements IHttpModule.  which we’ll use to intercept the message being passed after the Request and EndRequest handlers execute.

   1: using System;

   2: using System.Diagnostics;

   3: using System.IO;

   4: using System.Text;

   5: using System.Web;

   6: using System.Web.Services.Protocols;

   7: using Elmah;

   8:

   9: namespace Namespace.ElmahExtension

  10: {

  11:     public class ElmahExtension : IHttpModule

  12:     {

  13:         private HttpApplication _application;

  14:

  15:         public void Dispose()

  16:         {

  17:         }

  18:

  19:         public void Init(HttpApplication context)

  20:         {

  21:             _application = context;

  22:             _application.PostRequestHandlerExecute += OnPostRequestHandlerExecute;

  23:             _application.EndRequest += OnEndRequest;

  24:         }

  25:

  26:         private void OnPostRequestHandlerExecute(object sender, EventArgs e)

  27:         {

  28:             var context = sender as HttpApplication;

  29:             if(context != null && (context.Request.Path.Contains(".asmx") && (context.Response.StatusCode >= 400 && context.Response.StatusCode < 600)))

  30:             {

  31:                 context.Response.Filter = new ErrorHandlerFilter(context.Response.Filter);

  32:             }

  33:         }

  34:

  35:         static void OnEndRequest(object sender, EventArgs e)

  36:         {

  37:             var context = (HttpApplication)sender;

  38:             var errorHandlerFilter = context.Response.Filter as ErrorHandlerFilter;

  39:

  40:             if (errorHandlerFilter == null)

  41:             {

  42:                 return;

  43:             }

  44:

  45:             var originalContent = Encoding.UTF8.GetString(errorHandlerFilter.OriginalBytesWritten.ToArray());

  46:             ErrorLog.GetDefault(context.Context).Log(new Error(new Exception(originalContent)));

  47:         }

  48:     }

  49:

  50: }

Notice in the OnPostRequestHandlerExecute function we’re intercepting any request that comes from a Webservice (.asmx page) and has a status code of greater than 400 (All error code types).  At his point we want to set the ErrorHandelerFilter to the current Response.Filter value.

Now in the OnEndRequest function we want to get the current context and set the error handler based on the filter which should now be persisting the values we set in the previous function.  Once we have the error values all we have to do is call the Elmah manual logging API and log the exception.

The final piece you have to do is set up the webConfig to recognize and use your IHttpModule code. n In your config file navigate down to System.Web >> HttpModules and fill in the attribute accordingly.

   1: <system.web>

   2:     <httpModules>

   3:       <add name="ElmahExtension" type="Namespace.ElmahExtension.ElmahExtension"/>

   4:     </httpModules>

   5: </system.web>

Once you compile and run, any errors encountered in your web services should now be logged by Elmah just like any others.

 

Thanks to Reddy Kadasani, Mike Gerety and Dylan Barrett for their contributions to this solution.

2 Comments. Leave new

Hi, tried this code but still not working, my web services (in a aspx file) return JSON. Any ideias?

Works great thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

\\\