天天看點

[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上的一些設計. 臨到頭了打不動字了, 也不想翻源代碼了, 最重要的還是懶. 哈哈.

是以這個任務就交給搜尋引擎吧... 搜尋了一下有不少講這個的, 啊哈哈.