如何在.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

