.NET 6 Web API中间件读取请求体为空的问题排查
.NET 6 Web API自定义中间件捕获请求体为空问题解决
问题描述
在.NET 6 Web API中实现了自定义错误捕获中间件,期望在捕获异常时打印请求路由及请求体内容,但日志中请求体始终为空。
相关代码
自定义中间件代码
using System.Net.Mime; using System.Net; using System.Transactions; using System.Text; using Microsoft.AspNetCore.Http; namespace MyWebApi.Middlewares { public static class MyCustomMiddlewareExtensions { public static IApplicationBuilder UseMyCustomMiddleware( this IApplicationBuilder builder) { return builder.UseMiddleware<MyCustomMiddleware>(); } } public class MyCustomMiddleware { private readonly RequestDelegate _next; private readonly ILogger<MyCustomMiddleware> _logger; public MyCustomMiddleware(RequestDelegate next, ILogger<MyCustomMiddleware> logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext context) { try { await _next(context); if (context.Response.StatusCode == (int)HttpStatusCode.Unauthorized) throw new UnauthorizedAccessException(); } catch (Exception ex) { _logger.LogError("({errorCode})Message Error: {ex}\r\nqueryString/Body:{queryString}", "500", ex, await GetInfo(context)); } } private async Task<string> GetInfo(HttpContext context) { var request = context.Request; string result = string.Empty; if (request.Method == HttpMethods.Post && request.ContentLength > 0) { request.EnableBuffering(); request.Body.Position = 0; var buffer = new byte[Convert.ToInt32(request.ContentLength)]; await request.BodyReader.ReadAsync(); var requestContent = Encoding.UTF8.GetString(buffer); request.Body.Position = 0; result = string.Concat('[', request.Method, "]: ", request.Path, '/', request.QueryString, "\r\nBody: ", requestContent); } else { result = string.Concat('[', request.Method, "]: ", request.Path, '/', request.QueryString); } return result; } } }
Program.cs 配置代码
var app = builder.Build(); //Middlewares app.UseMyCustomMiddleware();
控制器代码
namespace MyWebApi.Controllers.V4 { [ApiController] [EnableCors("cors")] [Authorize] [Route("v{version:apiVersion}/Products")] [ApiVersion("4.0")] public class ProductsController : Controller { private readonly IConfiguration _configuration; private readonly IproductsBusinessLogic _productsBusinessLogic; private readonly IValidator<ProductsRequestDto> _ProductsRequestDtoValidator; private readonly ILogger<ProductsController> _logger; public ProductsController( ILogger<ProductsController> logger, IConfiguration configuration, IproductsBusinessLogic productsBusinessLogic, IValidator<ProductsRequestDto> ProductsRequestDtoValidator ) { _logger = logger; _configuration = configuration; _productsBusinessLogic = productsBusinessLogic; _ProductsRequestDtoValidator = ProductsRequestDtoValidator; } [MapToApiVersion("4.0")] [HttpPost] public async Task<IActionResult> Register([FromBody] ProductsRequestDto request) { var results = await _ProductsRequestDtoValidator.ValidateAsync(request); results.AddToModelState(ModelState, null); if (!results.IsValid) { return new ValidationFailedResult(results); } var result = await _productsBusinessLogic.Register(request); return Ok(result); } } }
当前日志输出
queryString/Body:[POST]: /v4.0/Products/ Body:
问题原因及解决方案
核心问题
- 中间件执行顺序错误:当前中间件在管道末尾执行,控制器的
[FromBody]已读取过请求体,默认请求体流不可重读,调用EnableBuffering也无法恢复已读取内容。 - 请求体读取逻辑错误:原代码使用
request.BodyReader.ReadAsync()但未将读取内容写入缓冲区,导致请求体内容为空。 - 读取时机太晚:在
_next执行完成后才尝试读取请求体,此时请求体流已被控制器消费。
修正步骤
1. 调整中间件执行顺序
在Program.cs中将自定义中间件放在路由、授权逻辑之前,确保控制器处理请求前启用请求体缓冲:
var app = builder.Build(); // 先启用自定义中间件,再执行路由、授权等逻辑 app.UseMyCustomMiddleware(); app.UseRouting(); app.UseAuthorization(); app.MapControllers(); app.Run();
2. 修改中间件逻辑,提前缓存请求体
修改InvokeAsync方法,在调用_next前启用缓冲并保存请求体内容,避免异常时读取已消费的流:
public async Task InvokeAsync(HttpContext context) { // 提前缓存请求体内容 string requestBody = string.Empty; var request = context.Request; if (request.Method == HttpMethods.Post && request.ContentLength > 0) { request.EnableBuffering(); // 使用StreamReader安全读取请求体,避免手动缓冲区操作错误 using var reader = new StreamReader(request.Body, Encoding.UTF8, leaveOpen: true); requestBody = await reader.ReadToEndAsync(); // 重置流位置,确保后续控制器能正常读取 request.Body.Position = 0; } try { await _next(context); if (context.Response.StatusCode == (int)HttpStatusCode.Unauthorized) throw new UnauthorizedAccessException(); } catch (Exception ex) { // 直接使用提前缓存的请求体拼接日志内容 string requestInfo = !string.IsNullOrEmpty(requestBody) ? $"[{request.Method}]: {request.Path}{request.QueryString}\r\nBody: {requestBody}" : $"[{request.Method}]: {request.Path}{request.QueryString}"; _logger.LogError("({errorCode})Message Error: {ex}\r\nqueryString/Body:{queryString}", "500", ex, requestInfo); } }
3. 移除冗余方法
删除原GetInfo方法,简化代码结构。
验证结果
修正后,控制器抛出异常时,日志将正确打印请求路由及完整请求体内容。
内容的提问来源于stack exchange,提问作者Wilson Narro
相关产品推荐
相关产品推荐

