I have created a custom Web API global exception handler like this:
public class MyGlobalExceptionHandler : ExceptionHandler { public override void Handle(ExceptionHandlerContext context) { // here I handle them all, no matter sync or not } public override Task HandleAsync(ExceptionHandlerContext context, CancellationToken cancellationToken) { // not needed, but I left it to debug and find out why it never reaches Handle() method return base.HandleAsync(context, cancellationToken); } public override bool ShouldHandle(ExceptionHandlerContext context) { // not needed, but I left it to debug and find out why it never reaches Handle() method return context.CatchBlock.IsTopLevel; } }
I'm registering it in my Global.asax Application_Start:
GlobalConfiguration.Configuration.Services.Replace(typeof(IExceptionHandler), new MyGlobalExceptionHandler());
Everything worked fine, no matter where I threw exceptions - inside controller methods or in my custom attributes above controller methods, and no matter if I call it from AJAX requests or directly from browser.
But then one day I needed CORS support for my AJAX requests. I enabled CORS globally as described in the article Enabling Cross-Origin Requests in ASP.NET Web API
var cors = new EnableCorsAttribute("*", "*", "*"); // * is just for debugging purpose config.EnableCors(cors);
At first everything seemed OK, CORS worked as expected, the server responded to OPTIONS request.
But the problems started when I wanted to check my authentication. Suddenly the exception was swallowed and I got an empty JSON {}
response instead of my custom JSON formatted exception which I create in the Handle() method of my MyGlobalExceptionHandler.
While debugging, I was surprised to find that now for AJAX requests ShouldHandle() method is called only with IsTopLevel = false, thus the exception never bubbles up and never reaches my Handle() method. As soon as I disable CORS, everything works fine again (except cross-domain requests, of course).
Why IsTopLevel is never true when I enable CORS? How should I fix this?
One more side effect is as follows. If CORS is disabled, then if I throw any exception inside Handle() method, it reaches the Application_Error handler in Global.asax. But if I enable CORS and throw exceptions in my handler methods, these exceptions never reach Application_Error.
It seems, I found exactly when this is happening.
If I throw an exception in a controller method when CORS is enabled, then CORS doesn't kick in at all and does not send the Access-Control-Allow-Origin header. When the browser doesn't receive the header, it immediately disrupts the request and this disruption seems to affect also the exception handler - it never reaches ShouldHandle() method with IsTopLevel = true. Chrome and Mozilla act like this even when I run AJAX request from a local html file to my websiet on IIS Express localhost.
But the situation is different on IE 11. When I open the html file there, it first asks me permissions to enable scripts. After I agree, then IE 11 ignores the fact that there are no CORS headers present and it doesn't disrupt the request, thus my exception handler receives IsTopLevel = true and is able to return a customised error response.
I guess, this should be fixed in the Web API core - even if I throw exceptions, CORS should still be able to kick in and send its headers, so the browser accepts the response. I have created a minimal test case application and I'll send it to the ASP.NET Team on CodePlex. Link to the test project. (the project zip file will be marked, just click Download and ignore all the other files in that folder)
I have found the source of confusion.
It seems, WebAPI by default is using this exception handler:
https://aspnetwebstack.codeplex.com/SourceControl/latest#src/System.Web.Http/ExceptionHandling/DefaultExceptionHandler.cs
and it has major differences from the suggested exception handling in this article:
http://www.asp.net/web-api/overview/web-api-routing-and-actions/web-api-global-error-handling
see chapter "Appendix: Base Class Details", where the code of default exception base class is given.
In the example it checks for IsOutermostCatchBlock
(which now seems to be moved to exceptionContext.CatchBlock.IsTopLevel
) to see if it's time to handle the exception. When CORS is enabled, such approach won't work for the reasons I described above. ASP.NET team said, this behavior is by design and they won't change anything.
I hope someone experienced will write an up-to-date article with instructions for doing exception handling the right way with and without CORS.
Currently I see two ways to work around this in my code:
don't inherit my custom exception handler from System.Web.Http.ExceptionHandling.ExceptionHandler
but implement IExceptionHandler
directly
if inheriting from System.Web.Http.ExceptionHandling.ExceptionHandler
, override ShouldHandle method and return true
always because CatchBlock.IsTopLevel
might never have true
value.
I tried both apporaches and they seem to work fine.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With