181

Background

I am developing an API Service Layer for a client and I have been requested to catch and log all errors globally.

So, while something like an unknown endpoint (or action) is easily handled by using ELMAH or by adding something like this to the Global.asax:

protected void Application_Error()
{
     Exception unhandledException = Server.GetLastError();
     //do more stuff
}

. . .unhandled errors that are not related to routing do not get logged. For example:

public class ReportController : ApiController
{
    public int test()
    {
        var foo = Convert.ToInt32("a");//Will throw error but isn't logged!!
        return foo;
    }
}

I have also tried setting the [HandleError] attribute globally by registering this filter:

filters.Add(new HandleErrorAttribute());

But that also does not log all errors.

Problem/Question

How do I intercept errors like the one generated by calling /test above so that I can log them? It seems that this answer should be obvious, but I have tried everything I can think of so far.

Ideally, I want to add some things to the error logging, such as the IP address of the requesting user, date, time, and so forth. I also want to be able to e-mail the support staff automatically when an error is encountered. All of this I can do if only I can intercept these errors when they happen!

RESOLVED!

Thanks to Darin Dimitrov, whose answer I accepted, I got this figured out. WebAPI does not handle errors in the same way as a regular MVC controller.

Here is what worked:

1) Add a custom filter to your namespace:

public class ExceptionHandlingAttribute : ExceptionFilterAttribute
{
    public override void OnException(HttpActionExecutedContext context)
    {
        if (context.Exception is BusinessException)
        {
            throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
            {
                Content = new StringContent(context.Exception.Message),
                ReasonPhrase = "Exception"
            });

        }

        //Log Critical errors
        Debug.WriteLine(context.Exception);

        throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
        {
            Content = new StringContent("An error occurred, please try again or contact the administrator."),
            ReasonPhrase = "Critical Exception"
        });
    }
}

2) Now register the filter globally in the WebApiConfig class:

public static class WebApiConfig
{
     public static void Register(HttpConfiguration config)
     {
         config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{action}/{id}", new { id = RouteParameter.Optional });
         config.Filters.Add(new ExceptionHandlingAttribute());
     }
}

OR you can skip registration and just decorate a single controller with the [ExceptionHandling] attribute.

Chris Moschini
  • 36,764
  • 19
  • 160
  • 190
Matt Cashatt
  • 23,490
  • 28
  • 78
  • 111
  • I have the same problem. Unhandled exceptions get caught in the exception filter attribute fine but when I throw a new exception it does not get caught in the exception filter attribute, any idea with regards to that? – daveBM Nov 05 '13 at 11:54
  • 1
    Unknown api controller calls like http://myhost/api/undefinedapicontroller errors are still not catched. Application_error and Exception filter code is not executed. How to catch them also ? – Andrus Nov 26 '13 at 09:03
  • 1
    Global error handling was added to WebAPI v2.1. See my response here: http://stackoverflow.com/questions/17449400/how-do-i-set-up-a-global-error-handler-in-webapi/21264726#21264726 – DarrellNorton Jan 21 '14 at 17:13
  • 1
    This will not catch errors in some circumstances, like "resource not found", or errors in a controller constructor. Refer here: http://aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/Elmah/Elmah.Server/App_Start/WebApiConfig.cs – Jordan Morris Aug 13 '14 at 00:45
  • Hi, @Matt. You have written the answer as part of the question but this is not a best practice in SO. Here answers should be separate from the question. Could you please write that as a separate answer (you can use the "Answer Your Own question" blue button at the bottom). – sashoalm Feb 05 '18 at 11:54

5 Answers5

82

As an addition to previous answers.

Yesterday, ASP.NET Web API 2.1 was oficially released.
It offers another opportunity to handle exceptions globally.
The details are given in the sample.

Briefly, you add global exception loggers and/or global exception handler (only one).
You add them to configuration:

public static void Register(HttpConfiguration config)
{
  config.MapHttpAttributeRoutes();

  // There can be multiple exception loggers.
  // (By default, no exception loggers are registered.)
  config.Services.Add(typeof(IExceptionLogger), new ElmahExceptionLogger());

  // There must be exactly one exception handler.
  // (There is a default one that may be replaced.)
  config.Services.Replace(typeof(IExceptionHandler), new GenericTextExceptionHandler());
}

And their realization:

public class ElmahExceptionLogger : ExceptionLogger
{
  public override void Log(ExceptionLoggerContext context)
  {
    ...
  }
}

public class GenericTextExceptionHandler : ExceptionHandler
{
  public override void Handle(ExceptionHandlerContext context)
  {
    context.Result = new InternalServerErrorTextPlainResult(
      "An unhandled exception occurred; check the log for more information.",
      Encoding.UTF8,
      context.Request);
  }
}
Vladimir
  • 7,345
  • 4
  • 34
  • 39
  • 2
    This worked perfectly. I log and handle concurrently (because I get the logID and pass it back so the user can add commentary), so I'm setting Result to a new ResponseMessageResult. This has been bugging me for a while, thanks. – Brett Sep 05 '14 at 23:41
56

If your web API is hosted inside an ASP.NET application, the Application_Error event will be called for all unhandled exceptions in your code, including the one in the test action you have shown. So all you have to do is handle this exception inside the Application_Error event. In the sample code you have shown you are only handling exception of type HttpException which is obviously not the case with the Convert.ToInt32("a") code. So make sure that you log and handle all exceptions in there:

protected void Application_Error()
{
    Exception unhandledException = Server.GetLastError();
    HttpException httpException = unhandledException as HttpException;
    if (httpException == null)
    {
        Exception innerException = unhandledException.InnerException;
        httpException = innerException as HttpException;
    }

    if (httpException != null)
    {
        int httpCode = httpException.GetHttpCode();
        switch (httpCode)
        {
            case (int)HttpStatusCode.Unauthorized:
                Response.Redirect("/Http/Error401");
                break;

            // TODO: don't forget that here you have many other status codes to test 
            // and handle in addition to 401.
        }
        else
        {
            // It was not an HttpException. This will be executed for your test action.
            // Here you should log and handle this case. Use the unhandledException instance here
        }
    }
}

Exception handling in the Web API could be done at various levels. Here's a detailed article explaining the different possibilities:

  • custom exception filter attribute which could be registered as a global exception filter

    [AttributeUsage(AttributeTargets.All)]
    public class ExceptionHandlingAttribute : ExceptionFilterAttribute
    {
        public override void OnException(HttpActionExecutedContext context)
        {
            if (context.Exception is BusinessException)
            {
                throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
                {
                    Content = new StringContent(context.Exception.Message),
                    ReasonPhrase = "Exception"
                });
            }
    
            //Log Critical errors
            Debug.WriteLine(context.Exception);
    
            throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
            {
                Content = new StringContent("An error occurred, please try again or contact the administrator."),
                ReasonPhrase = "Critical Exception"
            });
        }
    }
    
  • custom action invoker

    public class MyApiControllerActionInvoker : ApiControllerActionInvoker
    {
        public override Task<HttpResponseMessage> InvokeActionAsync(HttpActionContext actionContext, System.Threading.CancellationToken cancellationToken)
        {
            var result = base.InvokeActionAsync(actionContext, cancellationToken);
    
            if (result.Exception != null && result.Exception.GetBaseException() != null)
            {
                var baseException = result.Exception.GetBaseException();
    
                if (baseException is BusinessException)
                {
                    return Task.Run<HttpResponseMessage>(() => new HttpResponseMessage(HttpStatusCode.InternalServerError)
                    {
                        Content = new StringContent(baseException.Message),
                        ReasonPhrase = "Error"
    
                    });
                }
                else
                {
                    //Log critical error
                    Debug.WriteLine(baseException);
    
                    return Task.Run<HttpResponseMessage>(() => new HttpResponseMessage(HttpStatusCode.InternalServerError)
                    {
                        Content = new StringContent(baseException.Message),
                        ReasonPhrase = "Critical Error"
                    });
                }
            }
    
            return result;
        }
    }
    
hunter
  • 62,308
  • 19
  • 113
  • 113
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • I wish it were that simple, but the error still isn't getting caught. I have updated the question to avoid confusion. Thanks. – Matt Cashatt Mar 01 '13 at 22:49
  • @MatthewPatrickCashatt, if this exception is not getting caught in the `Application_Error` event, this means that some other code is consuming it before. For example you might have some custom HandleErrorAttributes, custom modules, ... There are gazillions of other places where exceptions could be caught and handled. But the best place to do that is the Application_Error event, because that's where all unhandled exceptions are going to end. – Darin Dimitrov Mar 01 '13 at 22:51
  • Thanks again, but no matter what, the `/test` example doesn't get hit. I have put a breakpoint on the first line (`Exception unhandledException = . . .`) but can not hit that breakpoint in the `/test` scenario. If I put in a bogus url, however, the breakpoint is hit. – Matt Cashatt Mar 01 '13 at 22:52
  • But you have a global `HandleErrorAttribute` registered. Get rid of that if you want your exception to make it through to `Application_Error`. Do you know what a `HandleErrorAttribute` is? – Darin Dimitrov Mar 01 '13 at 22:53
  • I will check and see if it is being consumed somewhere else. I recently installed ELMAH via NuGet. Perhaps that is the cause. . . – Matt Cashatt Mar 01 '13 at 22:54
  • ELMAH is not consuming the exceptions. It is only logging them. It is your `HandleErrorAttribute` that might be consuming them. But the only thing I can tell you for sure is that if the Application_Error event is not called, you have some code that is catching and consuming this exception. – Darin Dimitrov Mar 01 '13 at 22:54
  • Yes I am aware of what a `HandleErrorAttribute` is, thanks. I just commented the registration of that filter out and yet the problem persists. I appreciate your help, but I am already aware that `Application_Error` is not being called--that's the substance of my question. I will continue looking for anything else that may be catching my errors but am not hopeful that I will find much. Thanks again. – Matt Cashatt Mar 01 '13 at 22:58
  • Good luck with that. Is there something else you would like to ask? – Darin Dimitrov Mar 01 '13 at 23:00
  • Ok, to test your theory, I just created a clean, new WebAPI project in VS2012 and entered the line of code for throwing an error in `/api/values`. The same problem persists: this error does not trigger the `Application_Error` method. Unless there is something going on under the hood with .NET, I can't see any place in this naked project where the exception is being handled (I certainly didn't add a handler). Here is the project if you would like to take a look: http://www.mattcashatt.com/ErrorTest.zip. Thanks again. – Matt Cashatt Mar 01 '13 at 23:16
  • 1
    @MatthewPatrickCashatt, you are completely right. The `Application_Error` event is not the correct place to handle exceptions for the Web API because it will not be triggered in all cases. I have found a very detailed article explaining the various possibilities to achieve that: http://weblogs.asp.net/fredriknormen/archive/2012/06/11/asp-net-web-api-exception-handling.aspx – Darin Dimitrov Mar 01 '13 at 23:36
  • Thanks! I am looking into this now. – Matt Cashatt Mar 01 '13 at 23:50
  • I hate to push my luck--I am very close--but I can't seem to register this filter. I get the following error: `The given filter instance must implement one or more of the following filter interfaces: IAuthorizationFilter, IActionFilter, IResultFilter, IExceptionFilter.` Any thoughts? – Matt Cashatt Mar 01 '13 at 23:58
  • Nevermind--I got it working. I will update my question for the benefit of others. Thank you, this helped! – Matt Cashatt Mar 02 '13 at 00:11
  • 1
    @Darin Dimitrov Unknown api controller calls like http://myhost/api/undefinedapi errors are still not catched. Application_error and Exception filter code is not executed. How to catch them also ? – Andrus Nov 26 '13 at 09:05
  • @DarinDimitrov Is this still applicable on Web API 2 , or there is better way to do this in Web API 2 – Hakan Fıstık Nov 30 '15 at 15:26
8

Why rethrow etc? This works and it will make the service return status 500 etc

public class LogExceptionFilter : ExceptionFilterAttribute
{
    private static readonly ILog log = LogManager.GetLogger(typeof (LogExceptionFilter));

    public override void OnException(HttpActionExecutedContext actionExecutedContext)
    {
        log.Error("Unhandeled Exception", actionExecutedContext.Exception);
        base.OnException(actionExecutedContext);
    }
}
Anders
  • 17,306
  • 10
  • 76
  • 144
2

have you thought about doing something like an handle error action filter like

[HandleError]
public class BaseController : Controller {...}

you can also create a custom version of [HandleError] with which you can write error info and all other details to log

BenMorel
  • 34,448
  • 50
  • 182
  • 322
COLD TOLD
  • 13,513
  • 3
  • 35
  • 52
1

Wrap the whole thing in a try/catch and log the unhandled exception, then pass it on. Unless there's a better built-in way to do it.

Here's a reference Catch All (handled or unhandled) Exceptions

(edit: oh API)

Community
  • 1
  • 1
Tim
  • 857
  • 6
  • 13
  • Just in case, he'd need to rethrow the exception as well. – DigCamara Mar 01 '13 at 22:48
  • @DigCamara Sorry, that's what I meant by pass it on. throw; should handle that. I originally said "decide whether to exit or reload," then realized he had said it's an API. In that case, best to let the App decide what it wants to do by passing it on. – Tim Mar 01 '13 at 22:57
  • 2
    This is a bad answer because it will result in loads of duplicated code in every action. – Jansky Nov 27 '17 at 14:17