.NET Core 3.1 IAsyncExceptionFilter中无法读取HttpContext请求Body问题
问题场景
在.NET Core 3.1 Web API项目中实现了继承IAsyncExceptionFilter接口的全局异常过滤器,目标是捕获未处理异常,同时将异常信息与对应的HTTP请求数据一并记录到数据库。
过滤器实现代码如下:
public class ServiceExceptionInterceptor : IAsyncExceptionFilter { public async Task OnExceptionAsync(ExceptionContext context) { var errorLogService = (IErrorLogService) context.HttpContext.RequestServices.GetService(typeof(IErrorLogService)); var req = context.HttpContext.Request; var method = req.Method; var uri = $"{req.Scheme}://{req.Host}/{req.Path}{req.QueryString}"; string body = null; if (req.ContentLength != null) { var reader = new StreamReader(req.Body); // 此处读取到的body始终为空字符串 body = await reader.ReadToEndAsync(); } var exception = new Models.DB.Exception() { Type = context.Exception.GetType().ToString(), Message = context.Exception.Message, StackTrace = context.Exception.StackTrace, CreatedAt = DateTime.UtcNow, Method = method, Uri = uri, Body = body }; errorLogService.LogError(exception); var error = new Error() { StatusCode = 500, Message = "Something went wrong. There were some technical issues while processing your request." }; context.Result = new JsonResult(error) { StatusCode = StatusCodes.Status500InternalServerError }; } }
当前逻辑中通过req.ContentLength != null判断请求体是否存在的逻辑运行正常:无请求体时req.ContentLength为null,携带请求体时能获取到正确长度值,但读取到的请求体内容始终为空字符串。
参考网上方案在Startup中注册中间件调用EnableBuffering()启用请求缓冲后问题仍然存在:
app.Use(async (context, next) => { context.Request.EnableBuffering(); await next(); });
测试发现,只有移除Controller动作方法参数上的[FromBody]特性时,才能正常读取到请求体内容,示例接口代码如下:
[HttpPost] [Route("api/questions/{questionId}/comments")] public async Task<ActionResult<Comment>> AddComment(int questionId, /*[FromBody] AddCommentRequestBody requestBody*/) // 移除[FromBody]后过滤器可以正常读取请求体 { throw new Exception("test exception"); // 业务逻辑省略 return Ok(addedComment); }
核心诉求:在不移除Controller端点[FromBody]特性的前提下,正常读取请求体内容。
问题根因
两个核心问题导致读取失败:
EnableBuffering中间件注册顺序错误:如果该中间件放在MVC/路由/模型绑定相关中间件之后注册,[FromBody]触发的模型绑定逻辑会先读取完未开启缓冲的请求流,后续再开启缓冲没有任何效果。- 读取请求体前未重置流位置:即使成功开启请求缓冲,模型绑定逻辑读完请求体后,
Request.Body流的读取指针会停在流的末尾,直接调用ReadToEndAsync()从当前指针位置向后读取,只能得到空字符串。另外默认的StreamReader读取完成后会自动释放底层请求流,会影响后续组件的正常运行。
修复方案
按以下两步调整即可解决问题:
1. 调整缓冲中间件的注册顺序
将启用请求缓冲的中间件放到Configure方法中所有其他中间件的最前面,确保在任何组件读取请求体前就开启缓冲功能:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // 该中间件必须放在最前面,优先于所有会读取请求的组件 app.Use(async (context, next) => { context.Request.EnableBuffering(); // 大文件场景可以传入缓冲阈值,超过阈值的内容会写入磁盘,避免占用过多内存 // context.Request.EnableBuffering(1024 * 30); await next(); }); // 其余中间件按原有顺序放在后面 app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
2. 修正请求体读取逻辑
读取前先将流指针重置到流的起始位置,同时构造StreamReader时指定leaveOpen: true,避免读取完成后释放底层请求流,读取完成后可按需将指针重置回起始位置,不影响其他组件读取请求体:
string body = null; if (req.ContentLength != null && req.ContentLength > 0) { // 将流读取指针重置到起始位置 req.Body.Position = 0; // 指定leaveOpen为true,读取完成后不释放底层流 using (var reader = new StreamReader( req.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true)) { body = await reader.ReadToEndAsync(); } // 可选:读取完重置指针位置,避免影响后续逻辑 req.Body.Position = 0; }
调整完成后,即使接口参数标记了[FromBody],异常过滤器中也能正常读取到完整的请求体内容。
内容的提问来源于stack exchange,提问作者Mertcan Seğmen
相关产品推荐
相关产品推荐

