[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上的一些设计. 临到头了打不动字了, 也不想翻源代码了, 最重要的还是懒. 哈哈.
所以这个任务就交给搜索引擎吧... 搜索了一下有不少讲这个的, 啊哈哈.

posted @   czd890  阅读(482)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示