Related question: Web API ApiController PUT and POST methods receive null parameters intermittently
Background
While load testing an existing Web API project I noticed a lot of null reference exceptions as a result of a parameter being null when posting to an action.
The cause seems to be a custom message handler registered to log requests while running in dev environments. Removing this handler resolves the issue.
I understand that in Web API I can only read the request body once and that reading it would always cause my parameter to be null as model binding wouldn't be able to take place. For that reason I'm using the ReadAsStringAsync() method with ContinueWith to read the body. It looks like this is behaving oddly in ~0.2% of requests (during local debugging using Apache Bench).
Code
At the most basic level I have the following:
Model
public class User
{
public string Name { get; set; }
}
API Controller
public class UsersController : ApiController
{
[HttpPost]
public void Foo(User user)
{
if (user == null)
{
throw new NullReferenceException();
}
}
}
Message Handler
public class TestMessageHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Content.ReadAsStringAsync().ContinueWith((task) =>
{
/* do stuff with task.Result */
});
return base.SendAsync(request, cancellationToken);
}
}
...which is registered during app start
GlobalConfiguration.Configuration.MessageHandlers.Add(new TestMessageHandler());
I'm using WebAPI 4.0.30506.0, the latest at the time of posting. All other MS packages in the project are also running the latest version (demo project linked below now updated to reflect this).
Testing
The initial testing was made using Loadster running against a load-balanced IIS 7.5 setup on Server 2008 R2 with .NET 4.0.30319. I'm replicating this locally on IIS 7.5 on Windows 7 with .NET 4.5.50709 using Apache Bench.
ab -n 500 -c 25 -p testdata.post -T "application/json" http://localhost/ModelBindingFail/api/users/foo
where testdata.post contains
{ "Name":"James" }
With this testing I'm seeing roughly 1 failure for the 500 requests, so ~0.2%.
Next steps...
I've put my demo project on GitHub if you want to try for youself, though besides what I've posted above it's a standard empty Web API project.
Also happy to try out any suggestions or post more information. Thanks!
I'm still investigating the root cause of this but so far my gut feeling is that ContinueWith() is being executed in a different context, or at a point by which the request stream has been disposed or something like that (once I figure that out for sure I will update this paragraph).
In terms of fixes, I've quickly road tested three that can handle 500 requests with no errors.
The simplest is to just use task.Result
, this does however have some issues (it can apparently cause deadlocks, although YMMV).
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var result = request.Content.ReadAsStringAsync().Result;
return base.SendAsync(request, cancellationToken);
}
Next, you can ensure you are properly chaining your continuations to avoid any ambiguity about context, it is however quite ugly (and I'm not 100% sure if it is side effect free):
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var result = request.Content.ReadAsStringAsync().ContinueWith(task =>
{
/* do stuff with task.Result */
});
return result.ContinueWith(t => base.SendAsync(request, cancellationToken)).Unwrap();
}
Finally, the optimal solution appears to make use of async/await to sweep away any threading nasties, obviously this may be an issue if you are stuck on .NET 4.0.
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var content = await request.Content.ReadAsStringAsync();
Debug.WriteLine(content);
return await base.SendAsync(request, cancellationToken);
}
来源:https://stackoverflow.com/questions/18256817/web-api-action-parameter-is-intermittently-null