Disable *all* exception handling in ASP.NET Web API 2 (to make room for my own)?
I want to wire up exception handling in a middleware component, something like this:
public override async Task Invoke(IOwinContext context)
{
try
{
await Next.Invoke(context);
}
catch (Exception ex)
{
// Log error and return 500 response
}
}
However, some of the exceptions I would like to catch are being caught and converted to HttpErrorResponse
s by the Web API pipeline before I can get to them. In the process, I lose a lot of details about the errors, so I can't get useful stack traces when debugging etc (the debugger doesn't even stop when the exception is thrown - I have to manually step through the code and see where it fails...).
I tried adding a custom exception handler with the following implementation:
public Task HandleAsync(ExceptionHandlerContext context, CancellationToken cancellationToken)
{
var owinContext = context.Request.GetOwinContext();
owinContext.Set(Constants.ContextKeys.Exception, context.Exception);
return Task.FromResult(0);
}
registered through config.Services.Replace(typeof(IExceptionHandler), new MyExceptionHandler());
in my startup configuration, but looking at it after executing Next.Invoke(context)
through
context.Get<Exception>(Constants.ContextKeys.Exception);
still doesn't give me all the detail I want, as well as failing to stop at the fault point with the debugger.
Is there a way I can completely turn off all built-in error handling, so that my own middleware can take care of it?
Clarification, since a lot of people seem to misunderstand what I'm after:
- The built-in error handling in Web API catches some (but not all) exceptions and rewrites them into 500 responses.
- I want to catch all exceptions, do some logging, and then emit 500 responses with the information I choose (for most of them, see next bullet).
- There are also some exceptions that signal business logic faults, for which I want to return 40x errors instead.
- I want this to be at the top of the (app) pipeline, i.e. wrapping everything else in the request lifecycle
- I want to handle this using OWIN, to make it portable to a possible future self-hosted scenario (i.e. it's not written in stone that this app will always be hosted on IIS - HTTP modules, Global.asax.cs et al are not relevant here).
Solution 1:
Update: I blogged about this. When researching the blog post, I found some potential for improvement; I've updated the relevant parts of this answer. For more detail on why I think this is better than all other suggestions here, or the default behavior, read the entire post :)
I have now gone with the following approach, which seems to work OK, even if not 100 % compliant with what I was looking for:
-
Create a class
PassthroughExceptionHandler
:public class PassthroughExceptionHandler : IExceptionHandler { public Task HandleAsync(ExceptionHandlerContext context, CancellationToken cancellationToken) { // don't just throw the exception; that will ruin the stack trace var info = ExceptionDispatchInfo.Capture(context.Exception); info.Throw(); return Task.CompletedTask; } }
-
Let that class replace the
IExceptionHandler
service of Web API:config.Services.Replace(typeof(IExceptionHandler), new PassthroughExceptionHandler());
-
Create a middleware class which does what I want:
public class ExceptionHandlerMiddleware { public override async Task Invoke(IOwinContext context) { try { await Next?.Invoke(context); } catch (Exception ex) { // handle and/or log } } }
-
Register that middleware first in the stack:
app.Use<ExceptionHandlerMiddleware>() .UseStageMarker(PipelineStage.Authenticate) // other middlewares omitted for brevity .UseStageMarker(PipelineStage.PreHandlerExecute) .UseWebApi(config);
I will still award the bounty to anyone who comes up with (bounty expired...) I'm still looking for a better solution, which, for example, breaks when an unhandled exception is thrown. (This approach makes VS break when I rethrow the exception in the handler, but the original call stack is lost; I have to set a breakpoint at the faulting line and debug again to be able to intercept the state when an exception is thrown.)
Solution 2:
Not sure if this will work for you, but I have a similar requirement to send all errors back as JSON even for not found errors. I created a base controller and overrode the ExecuteAsync allowing me to create my own responses.
public class ControllerBase : ApiController
{
protected string ClassName = "ControllerBase::";
public override System.Threading.Tasks.Task<HttpResponseMessage> ExecuteAsync(System.Web.Http.Controllers.HttpControllerContext controllerContext, System.Threading.CancellationToken cancellationToken)
{
try
{
System.Threading.Tasks.Task<HttpResponseMessage> TaskList = base.ExecuteAsync(controllerContext, cancellationToken);
if (TaskList.Exception != null && TaskList.Exception.GetBaseException() != null)
{
JSONErrorResponse AsyncError = new JSONErrorResponse();
AsyncError.ExceptionMessage = TaskList.Exception.GetBaseException().Message;
AsyncError.ErrorMessage = string.Format("Unknown error {0} ExecuteAsync {1}", ClassName ,controllerContext.Request.RequestUri.AbsolutePath);
AsyncError.HttpErrorCode = HttpStatusCode.BadRequest;
HttpResponseMessage ErrorResponse = controllerContext.Request.CreateResponse(AsyncError.HttpErrorCode, AsyncError);
return System.Threading.Tasks.Task.Run<HttpResponseMessage>(() => ErrorResponse);
}
return TaskList;
}
catch (Exception Error)
{
JSONErrorResponse BadParameters = new JSONErrorResponse();
BadParameters.ExceptionMessage = Error.Message;
BadParameters.ErrorMessage = string.Format("Method [{0}], or URL [{1}] not found, verify your request", controllerContext.Request.Method.Method, controllerContext.Request.RequestUri.AbsolutePath);
BadParameters.HttpErrorCode = HttpStatusCode.NotFound;
HttpResponseMessage ErrorResponse = controllerContext.Request.CreateResponse(BadParameters.HttpErrorCode, BadParameters);
return System.Threading.Tasks.Task.Run<HttpResponseMessage>(() => ErrorResponse);
}
}
}
public class JSONErrorResponse
{
//Possible message from exception
public string ExceptionMessage { get; set; }
//Possible custom error message
public string ErrorMessage { get; set; }
//Http error code
public HttpStatusCode HttpErrorCode { get; set; }
}