15

I'd like to trap any unhandled exception thrown in an ASP.NET web service, but nothing I've tried has worked so far.

First off, the HttpApplication.Error event doesn't fire on web services, so that's out..

The next approach was to implement a soap extension, and add it to web.config with:

<soapExtensionTypes>
   <add type="Foo" priority="1" group="0" />
</soapExtensionTypes>

However, this doesn't work if you call the web method over JSON (which my web site does exclusively)..

My next idea would be to write my own HttpHandler for .asmx, which would hopefully derive from System.Web.Script.Services.ScriptHandlerFactory and do something smart. I haven't tried this yet.

Is there an approach I'm missing? Thanks!

Mike

UPDATE:

I'll summarize the possibly solutions here:

1) Upgrade to WCF which makes this whole thing much, much easier.

2) Since you cannot sub-class or override the RestHandler class, you would have to re-implement the whole thing as your own IHttpHandler or use reflection to manually call into its methods. Since the source to RestHandler is public and only about 500 lines long, making your own version might not be a huge amount of work but you'd then be responsible for maintaining it. I'm also unaware of any licensing restrictions involved with this code.

3) You can wrap your methods in try/catch blocks, or perhaps use LAMBDA expressions to make this code a bit cleaner. It would still require you to modify each method in your web service.

Mike Christensen
  • 88,082
  • 50
  • 208
  • 326
  • Is there any flexibility in the requirement that you can't modify the web methods? – Stephen Kennedy Nov 11 '11 at 16:57
  • Well, yes - as long as I don't have to surround all of them in giant try/catch blocks.. If the solution is clean, then I'm open to it. – Mike Christensen Nov 11 '11 at 16:58
  • I have a solution which requires a Try block and a single line Catch. You may be able to improve on it to do away with the try/catch altogether, I haven't dug that deep. Want it posted? – Stephen Kennedy Nov 11 '11 at 17:01
  • As long as the `try` block isn't around all the code in each web method (which has already been posted), then sure go ahead and post it. – Mike Christensen Nov 11 '11 at 17:04
  • It does, so the best help I can give is to point you to this question: http://stackoverflow.com/questions/2180228/handle-exceptions-in-web-services-with-elmah – Stephen Kennedy Nov 11 '11 at 17:06
  • Ah yea - no reason posting a duplicate answer. I'm interested in solutions involving IIS extensions or something that plugs in directly to the ASP.NET pipeline, such as a request handler. I'm pretty sure it's all possible in theory, but will probably require a lot of digging. – Mike Christensen Nov 11 '11 at 17:10
  • That's what Elmah does for normal ASP.NET requests but it doesn't work out of the box with asmx web services. I took the approach in the accepted answer there (but refined it slightly), that is to say I try and catch; you could look at the alternate answer if you have time, which suggests using a SoapExtension. HTH, HAND. – Stephen Kennedy Nov 11 '11 at 17:14
  • Added reflection code to my answer; personally I would not use it... but it gets the job done. – Chris Baxter Nov 17 '11 at 15:12

4 Answers4

11

As described in Capture all unhandled exceptions automatically with WebService there really is no good solution.

The reason that you cannot capture the HttpApplication.Error etc has to do with how the RestHandler has been implemented by the good folks at Microsoft. Specifically, the RestHandler explicitly catches (handles) the exception and writes out the exception details to the Response:

internal static void ExecuteWebServiceCall(HttpContext context, WebServiceMethodData methodData)
{
    try
    {
        NamedPermissionSet namedPermissionSet = HttpRuntime.NamedPermissionSet;
        if (namedPermissionSet != null)
        {
            namedPermissionSet.PermitOnly();
        }
        IDictionary<string, object> rawParams = GetRawParams(methodData, context);
        InvokeMethod(context, methodData, rawParams);
    }
    catch (Exception exception)
    {
        WriteExceptionJsonString(context, exception);
    }
}

To make matters worse, there is no clean extension point (that I could find) where you can change/extend the behavior. If you want to go down the path of writing your own IHttpHandler, I believe you will pretty much have to re-implement the RestHandler (or RestHandlerWithSession); regardless Reflector will be your friend.

For those that may choose to modify their WebMethods

If you are using Visual Studio 2008 or later, using Lambda expressions makes things not too bad (although not global/generic solution) in terms or removing duplicated code.

[WebMethod]
[ScriptMethod(UseHttpGet = true, ResponseFormat = ResponseFormat.Json)]
public String GetServerTime()
{
  return Execute(() => DateTime.Now.ToString());
}

public T Execute<T>(Func<T> action)
{
  if (action == null)
    throw new ArgumentNullException("action");

  try
  {
    return action.Invoke();
  }
  catch (Exception ex)
  {
    throw; // Do meaningful error handling/logging...
  }
}

Where Execute can be implemented in a subclass of WebService or as an extension method.

UPDATE: Reflection Evil

As mentioned in my origional answer, you can abuse reflection to get what you want... specifically you can create your own HttpHandler that makes use of the internals of the RestHandler to provide an interception point for capturing exception details. I have include an "unsafe" code example below to get you started.

Personally, I would NOT use this code; but it works.

namespace WebHackery
{
  public class AjaxServiceHandler : IHttpHandler
  {
    private readonly Type _restHandlerType;
    private readonly MethodInfo _createHandler;
    private readonly MethodInfo _getRawParams;
    private readonly MethodInfo _invokeMethod;
    private readonly MethodInfo _writeExceptionJsonString;
    private readonly FieldInfo _webServiceMethodData;

    public AjaxServiceHandler()
    {
      _restHandlerType = typeof(ScriptMethodAttribute).Assembly.GetType("System.Web.Script.Services.RestHandler");

      _createHandler = _restHandlerType.GetMethod("CreateHandler", BindingFlags.NonPublic | BindingFlags.Static, null, new[] { typeof(HttpContext) }, null);
      _getRawParams = _restHandlerType.GetMethod("GetRawParams", BindingFlags.NonPublic | BindingFlags.Static);
      _invokeMethod = _restHandlerType.GetMethod("InvokeMethod", BindingFlags.NonPublic | BindingFlags.Static);
      _writeExceptionJsonString = _restHandlerType.GetMethod("WriteExceptionJsonString", BindingFlags.NonPublic | BindingFlags.Static, null, new[] { typeof(HttpContext), typeof(Exception) }, null);

      _webServiceMethodData = _restHandlerType.GetField("_webServiceMethodData", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetField);
    }

    public bool IsReusable
    {
      get { return true; }
    }

    public void ProcessRequest(HttpContext context)
    {
      var restHandler = _createHandler.Invoke(null, new Object[] { context });
      var methodData = _webServiceMethodData.GetValue(restHandler);
      var rawParams = _getRawParams.Invoke(null, new[] { methodData, context });

      try
      {
        _invokeMethod.Invoke(null, new[] { context, methodData, rawParams });
      }
      catch (Exception ex)
      {
        while (ex is TargetInvocationException)
          ex = ex.InnerException;

        // Insert Custom Error Handling HERE...

        _writeExceptionJsonString.Invoke(null, new Object[] { context, ex});
      }
    }
  }
}
Community
  • 1
  • 1
Chris Baxter
  • 16,083
  • 9
  • 51
  • 72
  • Yea I think I might look into the code and see if I can write an HttpHandler that "wraps" the existing handler in some way. Other than that, I think there's no clean solution and I should just add error handling to the more complicated web methods. +1 for your detailed answer! – Mike Christensen Nov 16 '11 at 16:42
  • Due to the way that the RestHandler has been implemented, your only option for extension will likely be Reflection abuse (assuming full trust) as everything in RestHandler is internal or private. – Chris Baxter Nov 16 '11 at 16:57
  • Eh go figure.. Wonder if you can write some sort of HttpFilter that detects SOAP exceptions in the response stream and logs them to a database. Really all I want to do here is error logging. Right now I trap exceptions the in Javascript and then re-call another webmethod to log the error. – Mike Christensen Nov 16 '11 at 17:03
  • A custom HttpModule will be able to intercept any response that can be filtered to only log status code = 500; the downside is you don't get a lot of detail in most cases. – Chris Baxter Nov 16 '11 at 20:40
  • Yea it's only really worth it if I can get the exception info, like a full stack trace, perhaps a mini-dump. Stuff I don't really want to return back to the HTTP response. – Mike Christensen Nov 16 '11 at 20:44
  • As the RestHandler swallows the exception and writes it directly to the Response output stream, you can really only log what is returned to the client. As you had mentioned above, you may have to modify the most complicated methods to get any useful error detail logged (or go the reflection route). – Chris Baxter Nov 17 '11 at 01:57
4

This a good question. I've had this problem in the web app that I work on. I'm afraid my solution wasn't smart in any way. I simply wrapped in the code in each web service method in a try/catch and returned an object which indicated whether the method had run successfully or not. Fortunately my app doesn't have a massive number of web service calls so I can get away will this. I appreciate it's not a good solution if you're doing loads of web service calls.

Phil Hale
  • 3,453
  • 2
  • 36
  • 50
  • I do the same sort of thing in the latest web app I'm working on. I have a LOT of ajax calls via jquery and when the server side method throws an exception I catch it, dress up the JSON and have a global client side method that notifies the user there was a problem and what the details of that problem was. – Chris Townsend Nov 17 '11 at 16:10
2

Have you taken a look at WCF Web API? This is currently in beta, but it is already being used in several live sites. Exposing json/xml rest api's is very easy. In it you can create your own HttpErrorHandler derived class to handle errors. It is a very clean solution. Might be worth taking a closer look for you. Migrating any existing service should also be straightforward. Hope it helps!

santiagoIT
  • 9,411
  • 6
  • 46
  • 57
  • This is very interesting! I don't think I'd use it in production quite yet, but I will definitely be keeping an eye on this. +1 for the link! – Mike Christensen Nov 16 '11 at 16:37
  • 1
    I will second that this is drastically easier in WCF world; simply create an IErrorHandler (http://msdn.microsoft.com/en-us/library/system.servicemodel.dispatcher.ierrorhandler(v=vs.85).aspx) and associated service behavior and you are done. – Chris Baxter Nov 16 '11 at 16:59
  • Yeah sounds like my best bet for the long term is to port my code over to WCF. Wonder how hard that will be.. – Mike Christensen Nov 16 '11 at 17:06
2

Edit

Did you try to turn the error handling off in web.config and then adding an error module?

<location path="Webservices" >
    <system.web>
     <customErrors mode="Off" />
    </system.web>
</location>

Normal error handling as I think it should be even for the RestHandler

You can create a HttpModule to catch all errors and return custom result. Note that this will error message will be the response to the client instead of the expected result from the web service. You can also throw any custom exception in the application and let the module handle it if the client is to respond or notify the user to it.

Example result of correct execution:

{
  success: true,
  data: [ .... ],
  error: null
}

Example result of failed execution:

{
  success: false,
  data: null,
  error: {
    message: 'error message',
    stackTrace: 'url encoded stack trace'
  }
}

Add this to the web.config:

<modules>
  <add name="UnhandledException" type="Modules.Log.UnhandledException"/>
</modules>

You can easily add code to make the module self-register by implementing the pretty undocumented PreApplicationStartMethod function in the module and create and use your custom HttpApplication class to use instead of the standard one. By doing that you can add any module to the application by just adding it to the bin-folder of the application. It's the way I do all my apps currently and I must say that it works perfectly.

Below code will catch all application errors and return a html response with the error message (you'll need to replace the html stuff with your own json error message implementation):

using System.Web;

namespace Modules.Log
{
    using System;
    using System.Text;

    public class LogModule : IHttpModule
    {
        private bool initialized = false;
        private object initLock = new Object();
        private static string applicationName = string.Format("Server: {0}; [LogModule_AppName] installation name is not set", System.Environment.MachineName);

        public void Dispose()
        {
        }

        public void Init(HttpApplication context)
        {
            // Do this one time for each AppDomain.
            if (!initialized)
            {
                lock (initLock)
                {
                    if (!initialized)
                    {
                        context.Error += new EventHandler(this.OnUnhandledApplicationException);

                        initialized = true;
                    }
                }
            }
        }

        private void OnUnhandledApplicationException(object sender, EventArgs e)
        {
            StringBuilder message = new StringBuilder("<html><head><style>" +
                "body, table { font-size: 12px; font-family: Arial, sans-serif; }\r\n" +
                "table tr td { padding: 4px; }\r\n" +
                ".header { font-weight: 900; font-size: 14px; color: #fff; background-color: #2b4e74; }\r\n" +
                ".header2 { font-weight: 900; background-color: #c0c0c0; }\r\n" +
                "</style></head><body><table><tr><td class=\"header\">\r\n\r\nUnhandled Exception logged by LogModule.dll:\r\n\r\nappId=");

            string appId = (string)AppDomain.CurrentDomain.GetData(".appId");
            if (appId != null)
            {
                message.Append(appId);
            }

            message.Append("</td></tr>");

            HttpServerUtility server = HttpContext.Current.Server;
            Exception currentException = server.GetLastError();

            if (currentException != null)
            {
                message.AppendFormat(
                        "<tr><td class=\"header2\">TYPE</td></tr><tr><td>{0}</td></tr><tr><td class=\"header2\">REQUEST</td></tr><tr><td>{3}</td></tr><tr><td class=\"header2\">MESSAGE</td></tr><tr><td>{1}</td></tr><tr><td class=\"header2\">STACK TRACE</td></tr><tr><td>{2}</td></tr>",
                        currentException.GetType().FullName,
                        currentException.Message,
                        currentException.StackTrace,
                        HttpContext.Current != null ? HttpContext.Current.Request.FilePath : "n/a");
                server.ClearError();
            }

            message.Append("</table></body></html>");

            HttpContext.Current.Response.Write(message.ToString());
            server.ClearError();
        }
    }
}
Asken
  • 7,679
  • 10
  • 45
  • 77
  • Ajax calls to web methods do not throw unhandled exceptions (see first code snippet in my answer). This approach works for regular calls, but the RestHandler removes this as an option as the exception is "handled". – Chris Baxter Nov 17 '11 at 13:45
  • I think @CalgaryCoder sums it up pretty well - Exceptions are caught and then `WriteExceptionJsonString` is called to write out serialized exception info as a JSON string. You'd have to intercept this to have any luck. It's lame they didn't make these classes extensible, it would be some fairly straight-forward design changes to add a world of more power. – Mike Christensen Nov 17 '11 at 16:37