You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在.NET 8中正确取消模型绑定以终止超时请求?

问题描述

负载场景下,基于.NET 8 + K8s 1.30环境的Web服务实例会抛出Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException: Unexpected end of request content异常,异常栈如下:

Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException: Unexpected end of request content.
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.Http1ContentLengthMessageBody.ReadAsyncInternal(CancellationToken cancellationToken)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpRequestStream.ReadAsyncInternal(Memory`1 destination, CancellationToken cancellationToken)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at System.Text.Json.Serialization.ReadBufferState.ReadFromStreamAsync(Stream utf8Json, CancellationToken cancellationToken, Boolean fillBuffer)
at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.DeserializeAsync(Stream utf8Json, CancellationToken cancellationToken)
at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.DeserializeAsObjectAsync(Stream utf8Json, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonInputFormatter.ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
at Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonInputFormatter.ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
at Microsoft.AspNetCore.Mvc.ModelBinding.Binders.BodyModelBinder.BindModelAsync(ModelBindingContext bindingContext)

我们期望异常与Istio负载均衡器设置的1分钟超时一致,但追踪发现部分请求在到达控制器前耗时可达10-20分钟,推测问题出在模型绑定阶段。从栈信息可见相关方法带有CancellationToken参数,但怀疑服务未正确响应取消。

已基于BodyModelBinder编写自定义包装类添加取消逻辑(代码如下),虽能减少长时请求并返回超时响应,但模型绑定仍会在后台执行、占用资源直至失败。补充:追踪Span显示请求到达控制器执行首个操作(标注'request validated')耗时超40秒。

自定义取消代码:

public async Task BindModelAsync(ModelBindingContext bindingContext)
{
    var token = CancellationTokenSource.CreateLinkedTokenSource(bindingContext.HttpContext.RequestAborted);
    var task = _actualModelBinder.BindModelAsync(bindingContext);
    var delay = Task.Delay(TimeSpan.FromSeconds(5), token.Token);
    if (await Task.WhenAny(delay, task) == task)
    {
        // Task completed within timeout.
        // Consider that the task may have faulted or been canceled.
        // We re-await the task so that any exceptions/cancellation is rethrown.
        token.Cancel();
        await task;
    }
    else if (bindingContext.HttpContext.RequestAborted.IsCancellationRequested)
    {
        throw new OperationCanceledException("Request cancelled.");
    }
    else
    {
        throw new TimeoutException("Model binding timed out.");
    }
}
优化方案建议
  • 利用请求级别的CancellationToken传递
    现有自定义绑定器仅在绑定器层面判断超时,未将取消令牌传递给底层的模型绑定流程。正确做法是将超时令牌与HttpContext.RequestAborted关联,替换上下文的取消令牌,让所有依赖该令牌的操作(流读取、Json反序列化)都能响应取消:

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        using var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(1));
        using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(
            bindingContext.HttpContext.RequestAborted, 
            timeoutTokenSource.Token);
        
        // 替换上下文的请求取消令牌
        var originalToken = bindingContext.HttpContext.RequestAborted;
        bindingContext.HttpContext.RequestAborted = linkedTokenSource.Token;
        
        try
        {
            await _actualModelBinder.BindModelAsync(bindingContext);
        }
        catch (OperationCanceledException) when (!originalToken.IsCancellationRequested)
        {
            throw new TimeoutException("Model binding timed out.");
        }
        finally
        {
            // 恢复原令牌(可选,视上下文复用情况)
            bindingContext.HttpContext.RequestAborted = originalToken;
        }
    }
    
  • 在Kestrel层面配置全局请求超时
    .NET 8的Kestrel支持直接配置全局请求超时,这会在请求处理的更早阶段触发取消,比自定义绑定器更高效,能从根源上终止耗时请求。在Program.cs中添加:

    builder.WebHost.ConfigureKestrel(options =>
    {
        // 与Istio超时保持一致,设置为1分钟
        options.Limits.RequestTimeout = TimeSpan.FromMinutes(1);
        // 可选:限制最大请求体大小,提前拒绝超大请求
        options.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10MB
    });
    

    该超时会自动关联到HttpContext.RequestAborted,所有使用该令牌的操作都会被取消,包括请求体读取、模型绑定等,避免后台任务继续占用资源。

  • 优化Json反序列化的取消支持
    从异常栈看,耗时点集中在System.Text.Json反序列化阶段。确保JsonSerializerOptions未禁用取消支持(默认启用),同时可以调整反序列化的缓冲区配置,减少大请求体的读取耗时。若使用自定义SystemTextJsonInputFormatter,需确保其在读取请求体时正确传递CancellationToken。

  • 同步Istio与服务端的超时配置
    确保Istio VirtualService中的超时设置与服务端Kestrel、自定义超时保持一致(均为1分钟),避免两端超时不一致导致的请求残留。示例Istio配置:

    apiVersion: networking.istio.io/v1alpha3
    kind: VirtualService
    metadata:
      name: your-service
    spec:
      hosts:
      - your-service.example.com
      http:
      - route:
        - destination:
            host: your-service
        timeout: 1m
    

内容的提问来源于stack exchange,提问作者fkptn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 15:28:12