[Net 6 AspNetCore Bug] 解决返回IAsyncEnumerable<T>类型时抛出的OperationCanceledException会被AspNetCore 框架吞掉的Bug
记录一个我认为是Net6 Aspnetcore 框架的一个Bug
Bug描述
在 Net6 的apsnecore项目中, 如果我们(满足以下所有条件)
- api的返回类型是
IAsyncEnumerable<T>
, - 且我们返回的是
JsonResult
对象, 或者返回的是ObjectResult
且要求的返回协商数据类型是json
, - 且我们用的是
System.Text.Json
来序列化(模式是它), - 且我们的响应用要求的编码是
utf-8
那么在业务方法中抛出的任何OperationCanceledException
或者继承自OperationCanceledException
的任何子类异常都会被框架吃掉.
Bug重现
如果我们有这样一段代码, 然后结果就是客户端和服务端都不会收到或者记录任何错误和异常.
[HttpGet("/asyncEnumerable-cancel")] public ActionResult<IAsyncEnumerable<int>> TestAsync() { async IAsyncEnumerable<int> asyncEnumerable() { await Task.Delay(100); yield return 1; throw new OperationCanceledException(); // 或者Client 主动取消请求后 用this.HttpContext.RequestAborted.ThrowIfCancellationRequested() 或者任何地方抛出的task或operation cancel exception. } return this.Ok(asyncEnumerable()); }
测试代码
curl --location --request GET 'http://localhost:5000/asyncEnumerable-cancel' # response code is 200 curl --location --request GET 'http://localhost:5000/asyncEnumerable-cancel' --header 'Accept-Charset: utf-16' # response code is 500
显然这不是一个合理的 Behavior.
- 不同的编码响应结果不一样
- 明明抛出异常了, 但是utf-8还能收到200 ok的response http code
产生这个Bug的代码
SystemTextJsonOutputFormatter 对应的是用 return this.Ok(object)
返回的Case
SystemTextJsonResultExecutor 对应的是用 return new JsonResult(object)
返回的case
当然, 其他的实现方式或者关联代码是否也有这个Bug我就没有验证了. 以及产生这个Bug的原因就不多说了. 可以看看这2个文件的commit logs.
//核心代码就是这么点. try-catch吞掉了这个Exception
if (selectedEncoding.CodePage == Encoding.UTF8.CodePage) { try { await JsonSerializer.SerializeAsync(responseStream, context.Object, objectType, SerializerOptions, httpContext.RequestAborted); await responseStream.FlushAsync(httpContext.RequestAborted); } catch (OperationCanceledException) { } }
目前状况
昨天在 dotnet/aspnetcore/issues提交了一个issues, 等待官方的跟进.
如何手动修复这个Bug
如果是return new JsonResult(object)
, 我们可以用一个自己修复的SystemTextJsonResultExecutor
替换框架自身的.
框架自身的是这么注册的: services.TryAddSingleton<IActionResultExecutor<JsonResult>, SystemTextJsonResultExecutor>();
如果你用的是return this.Ok(object)
方式, 那么可以照着下面的代码来,
第一步, 首先从SystemTextJsonOutputFormatter copy 代码到你的本地.
然后修改构造函数并吧导致这个Bug的try-catch结构删掉即可.
// 构造函数中改动代码 public HookSystemTextJsonOutputFormatter(JsonSerializerOptions jsonSerializerOptions) { SerializerOptions = jsonSerializerOptions; SupportedEncodings.Add(Encoding.UTF8); SupportedEncodings.Add(Encoding.Unicode); SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json").CopyAsReadOnly()); SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/json").CopyAsReadOnly()); SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/*+json").CopyAsReadOnly()); } // WriteResponseBodyAsync 方法中改动代码 var responseStream = httpContext.Response.Body; if (selectedEncoding.CodePage == Encoding.UTF8.CodePage) { await JsonSerializer.SerializeAsync(responseStream, context.Object, objectType, SerializerOptions, httpContext.RequestAborted); await responseStream.FlushAsync(httpContext.RequestAborted); }
第二步, 用我们自己改造过的SystemTextJsonOutputFormatter替换系统自己的
//用IConfigureOptions方式替换我们的自带SystemTextJsonOutputFormatter. public class MvcCoreMvcOptionsSetupWithFixedSystemTextJsonOutputFormatter : IConfigureOptions<MvcOptions> { private readonly IOptions<JsonOptions> jsonOptions; public MvcCoreMvcOptionsSetupWithFixedSystemTextJsonOutputFormatter(IOptions<JsonOptions> jsonOptions) { this.jsonOptions = jsonOptions; } public void Configure(MvcOptions options) { options.OutputFormatters.RemoveType<SystemTextJsonOutputFormatter>();//删除系统自己的 options.OutputFormatters.Add(HookSystemTextJsonOutputFormatter.CreateFormatter(this.jsonOptions.Value));//替换为我们自己的 } }
// 然后在Startup.ConfigureServices
的最后应用我们的更改
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, MvcCoreMvcOptionsSetupWithFixedSystemTextJsonOutputFormatter>());
后记
Ok, 到这里就结束了, 如果后续官方修复了这个bug, 那我们只要删除上面增加的代码即可.
开始写的时候本想多介绍一些关于ActionResult(JsonResult, ObjectResult), ObjectResult的内容格式协商, 以及在ObjectResult上的一些设计. 临到头了打不动字了, 也不想翻源代码了, 最重要的还是懒. 哈哈.
所以这个任务就交给搜索引擎吧... 搜索了一下有不少讲这个的, 啊哈哈.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?