-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Description
I have code that use ASP.NET Core to implement a JSON API. In order to deal with errors it does something similar to this:
app.UseExceptionHandler(action =>
{
action.Run(async context =>
{
// Apply CORS to error response - workaround for https://github.com/aspnet/CORS/issues/90
var policy = await app.ApplicationServices.GetService<ICorsPolicyProvider>().GetPolicyAsync(context, ALLOW_ALL);
var result = cors.EvaluatePolicy(context, policy);
cors.ApplyResult(result, context.Response);
// Get exception and log it
var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
await LogException(exception, context.Request);
// Return API JSON error to the client
var error = (exception as ApiException)?.Error ?? Models.Error.Unknown;
await WriteErrorResponse(context, error);
});
});
I noticed recently that under some circumstances the exception objects that were logged were identical. This occurred when the server was under load and similar exceptions were generated a few ms apart, so it made me think it was a concurrency issue.
After some digging, I believe this problem occurs because access to the IFeatureCollection is not thread safe (see ApplicationInsights-aspnetcore #373), and because there is an await before retrieving the exception from IFeatureCollection.
My understanding of concurrency bug is as follow:
- The API throw an exception in on of the controllers
- The
ExceptionHandlerMiddlewareused byUseExceptionHandler()catches the exception and sets it on the context (context.Features.Set<IExceptionHandlerFeature>(exceptionHandlerFeature)) - Because of that CORS workaround, there is an
awaitexecuted next. I believe this means the code that follows could potentially be executed in a different thread. - In the next line I access the FeatureCollection via
context.Features.Get<IExceptionHandlerFeature>()?.Error - Because the
IFeatureCollectionis not thread safe, and it reads theIExceptionHandlerFeaturein a different thread from where it was written, I end up with corrupted data (i.e. the exception object from anotherHttpContext)
Does that make sense? Or am I just missing something obvious here? Unfortunately, I don't have definite proof. I was not able to reproduce the problem on a test server.
But if I am right, the whole IExceptionHandlerFeature being set inside the ExceptionHandlerMiddleware is kind of misleading. It would only safe to set features early on when the HttpContext is being initialized, and not in the way it is done for IExceptionHandlerFeature. It looks like the ExceptionHandlerMiddleware should be modified. Maybe it would be better if the Exception object was given as a parameter of the ExceptionHandler callback?
I decided to write my own ErrorMiddleware which is similar to ExceptionHandlerMiddleware but avoids using a Dictionary to capture the exception. I suppose I could have simply reordered the code and ensured the context.Features.Get<IExceptionHandlerFeature>() was executed immediately, and not after the CORS workaround.