You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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:

问题原因及解决方案

核心问题

  1. 中间件执行顺序错误:当前中间件在管道末尾执行,控制器的[FromBody]已读取过请求体,默认请求体流不可重读,调用EnableBuffering也无法恢复已读取内容。
  2. 请求体读取逻辑错误:原代码使用request.BodyReader.ReadAsync()但未将读取内容写入缓冲区,导致请求体内容为空。
  3. 读取时机太晚:在_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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 18:48:19