2

I am trying to understand how Web API Http pipeline works!

In my Web API project, I am using the following technique to log/handle exceptions:

  • ExceptionHandler -> Handle exceptions at the global level
  • ExceptionFilterAttribute -> Handle custom exception thrown by user
  • DelegatingHandler -> log request and response data

Sample code for each implementation:

ExceptionFilter:

public class CustomExceptionFilter : ExceptionFilterAttribute
{
    public override void OnException(HttpActionExecutedContext context)
    {
        var request = context.ActionContext.Request;
        if (context.Exception is ItemNotFoundException)
        {
            context.Response = request.CreateResponse(HttpStatusCode.NotFound, context.Exception.Message);
        }
        else if (context.Exception is InvalidRequestException)
        {
            context.Response = request.CreateResponse(HttpStatusCode.BadRequest, context.Exception.Message);
        }
    }
}

Exception Handler:

public class GlobalExceptionHandler : ExceptionHandler
{
    public override void Handle(ExceptionHandlerContext context)
    {
        var result = new HttpResponseMessage(HttpStatusCode.InternalServerError)
        {
            Content = new StringContent(Constant.ErrorMessage.InternalServerError)
        };
        context.Result = new ErrorMessageResult(context.Request, result);
    }
}

public class ErrorMessageResult : IHttpActionResult
{
    private readonly HttpRequestMessage _request;
    private readonly HttpResponseMessage _httpResponseMessage;

    public ErrorMessageResult(HttpRequestMessage request, HttpResponseMessage httpResponseMessage)
    {
        _request = request;
        _httpResponseMessage = httpResponseMessage;
    }

    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        return Task.FromResult(_httpResponseMessage);
    }
}

DelegatingHandler:

public class LogRequestAndResponseHandler : DelegatingHandler
{
    private readonly ILoggingService _loggingService;

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        string requestBody = await request.Content.ReadAsStringAsync();
        _loggingService.FirstLevelServiceLog(requestBody);
        var result = await base.SendAsync(request, cancellationToken);
        if (result.Content != null)
        {
            var responseBody = await result.Content.ReadAsStringAsync();
            _loggingService.FirstLevelServiceLog(responseBody);
        }
        return result;
    }
}

Observation:

  • When there is an custom exception CustomExceptionFilter is getting invoked and later the response is logged in LogRequestAndResponseHandler.
  • However, if the exception is not handled, it goes in GlobalExceptionHandler then the response DOES NOT come to LogRequestAndResponseHandler for logging.

Could anyone let me know, what code change have to be done in CustomExceptionFilter/GlobalExceptionHandler in order to receive the response in DelegatingHandler?

Solution: (Updated 10/09/2018)

Okay, so i found the solution here

By modifying ExceptionHandler code, i am able to catch the response in DelegatingHandler

Key was to inherit from IExceptionHandler rather than ExceptionHandler

Code:

public class GlobalExceptionHandler : IExceptionHandler
{
    public Task HandleAsync(ExceptionHandlerContext context, CancellationToken cancellationToken)
    {
        var httpResponse = context.Request.CreateResponse(HttpStatusCode.InternalServerError, Constant.ErrorMessage.InternalServerError);
        context.Result = new ResponseMessageResult(httpResponse);

        return Task.FromResult(0);
    }
}

Question:

  • I am still not able to understand how it's working? What is the difference between IExceptionHandler & ExceptionHandler?

Could anyone shed some light on this?

Shaggy
  • 5,422
  • 28
  • 98
  • 163

1 Answers1

3

ExceptionHandler implements IExceptionHandler like this:

Task IExceptionHandler.HandleAsync(ExceptionHandlerContext context, CancellationToken cancellationToken)
{
  if (context == null)
    throw new ArgumentNullException(nameof (context));
  ExceptionContext exceptionContext = context.ExceptionContext;
  if (!this.ShouldHandle(context))
    return TaskHelpers.Completed();
  return this.HandleAsync(context, cancellationToken);
}

Where I suspect you're seeing the difference is in that ShouldHandle check, which is implemented like this:

public virtual bool ShouldHandle(ExceptionHandlerContext context)
{
  if (context == null)
    throw new ArgumentNullException(nameof (context));
  return context.ExceptionContext.CatchBlock.IsTopLevel;
}

I'm not intimately familiar with the pipeline, but from what I've seen it appears that exceptions can be handled at various points, and the ExceptionHandler base class assumes you probably only want to handle exceptions at the top level of the execution stack. I've seen cases where other handlers like CORS get in the way of this, and the catch block never ends up being at the top level.

If this is what you're seeing, you can still extend ExceptionHandler, and override the ShouldHandle method to just always return true. Or you could be more surgical and specifically detect whether CORS is likely to get in the way of the top-level check as suggested in this comment.

StriplingWarrior
  • 151,543
  • 27
  • 246
  • 315