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
ExceptionHandlerMiddleware
used byUseExceptionHandler()
catches the exception and sets it on the context (context.Features.Set<IExceptionHandlerFeature>(exceptionHandlerFeature)
) - Because of that CORS workaround, there is an
await
executed 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
IFeatureCollection
is not thread safe, and it reads theIExceptionHandlerFeature
in 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.