4

相关问题:Web API ApiController PUT 和 POST 方法间歇性接收空参数

背景

在对现有 Web API 项目进行负载测试时,我注意到很多空引用异常,因为在发布到操作时参数为空。

原因似乎是在开发环境中运行时注册以记录请求的自定义消息处理程序。删除此处理程序可解决此问题。

我知道在 Web API 中我只能读取一次请求正文,并且读取它总是会导致我的参数为空,因为模型绑定将无法发生。出于这个原因,我使用带有 ContinueWith 的 ReadAsStringAsync() 方法来读取正文。看起来这在大约 0.2% 的请求中表现得很奇怪(在使用 Apache Bench 进行本地调试期间)。

代码

在最基本的层面上,我有以下几点:

模型

public class User
{
    public string Name { get; set; }
}

API 控制器

public class UsersController : ApiController
{
    [HttpPost]
    public void Foo(User user)
    {
        if (user == null)
        {
            throw new NullReferenceException();
        }
    }
}

消息处理程序

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);
    }
}

...在应用程序启动期间注册

GlobalConfiguration.Configuration.MessageHandlers.Add(new TestMessageHandler());

我正在使用发布时最新的 WebAPI 4.0.30506.0。项目中的所有其他 MS 包也在运行最新版本(下面链接的演示项目现已更新以反映这一点)。

测试

最初的测试是使用Loadster在带有 .NET 4.0.30319 的 Server 2008 R2 上针对负载平衡的 IIS 7.5 设置运行的。我正在使用 Apache Bench 使用 .NET 4.5.50709 在 Windows 7 上的 IIS 7.5 上本地复制此内容。

ab -n 500 -c 25 -p testdata.post -T "application/json" http://localhost/ModelBindingFail/api/users/foo

其中 testdata.post 包含

{ "Name":"James" }

通过这个测试,我看到 500 个请求大约有 1 次失败,所以约为 0.2%。

下一步...

如果您想亲自尝试,我已经将我的演示项目放在了GitHub 上,尽管除了我在上面发布的内容之外,它是一个标准的空 Web API 项目。

也很高兴尝试任何建议或发布更多信息。谢谢!

4

1 回答 1

2

我仍在调查此问题的根本原因,但到目前为止,我的直觉是 ContinueWith() 是在不同的上下文中执行的,或者在请求流已被处理或类似的情况下执行(一旦我认为我肯定会更新这一段)。

在修复方面,我已经快速测试了三个可以处理 500 个请求且没有错误的三个。

最简单的就是使用task.Result,但这确实有一些问题(它显然会导致死锁,尽管 YMMV)。

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
    var result = request.Content.ReadAsStringAsync().Result;
    return base.SendAsync(request, cancellationToken);
}

接下来,您可以确保正确链接您的延续以避免任何关于上下文的歧义,但是它非常难看(而且我不能 100% 确定它是否没有副作用):

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();
}

最后,最佳解决方案似乎是使用 async/await 来清除任何线程问题,如果您卡在 .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);
}
于 2013-11-05T12:15:13.483 回答