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