Web API操作参数间歇性为空

本文关键字:API 操作 参数 Web | 更新日期: 2023-09-27 18:27:59

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

背景

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

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

我知道在Web API中,我只能读取请求主体一次,读取它总是会导致我的参数为null,因为模型绑定无法进行。出于这个原因,我使用带有ContinueWith的ReadAsStringAsync()方法来读取正文。在大约0.2%的请求中(在使用ApacheBench进行本地调试期间),这种情况看起来很奇怪。

代码

在最基本的层面上,我有以下内容:

型号

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项目。

也很乐意尝试任何建议或发布更多信息。谢谢

Web API操作参数间歇性为空

我仍在调查这一问题的根本原因,但到目前为止,我的直觉是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);
}