[Net 6 AspNetCore Bug] 解決返回IAsyncEnumerable<T>型別時丟擲的OperationCanceledException會被AspNetCore 框架吞掉的Bug

czd890發表於2021-11-26

記錄一個我認為是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上的一些設計. 臨到頭了打不動字了, 也不想翻原始碼了, 最重要的還是懶. 哈哈.
所以這個任務就交給搜尋引擎吧... 搜尋了一下有不少講這個的, 啊哈哈.

相關文章