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

  • 相关阅读:
    jquery ui draggable,droppable 学习总结
    VSCode设置网页代码实时预览
    ionic3-修改APP应用图标(icon)和APP启动界面(Splash)
    Ionic3页面的生命周期
    videogular2 在ionic3项目里报错(rxjs_1.fromEvent is not a function)
    IDEA的maven项目的netty包的导入(其他jar同)
    maven的安装与项目的创建
    给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
    使用二分法查询二维整型数组的值(找到返回其坐标)
    乐观锁以及悲观锁
  • 原文地址:https://www.cnblogs.com/calvinK/p/15608044.html
Copyright © 2020-2023  润新知