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