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

ASP.NET Core 8.0过滤器读取HttpContext请求体为空问题

问题描述

在ASP.NET Core 8.0 Web API的异常过滤器中,尝试读取HttpContext的请求体时,即使请求确实携带了请求体,日志中记录的请求体始终为空。过滤器会在控制器抛出异常时捕获异常,并记录请求路径、方法、查询参数、请求头及异常信息,但请求体部分始终为空。

现有代码

异常过滤器

using Azure.Core;
using backend.Log;
using backend.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace backend.Filters;

public class ExceptionFilter : IActionFilter, IOrderedFilter
{
    public int Order => int.MaxValue - 10;

    public void OnActionExecuting(ActionExecutingContext context) { }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        if (context.Exception is Exception exception)
        {
            HttpRequest request = context.HttpContext.Request;

            RequestLog.Log(exception, request);

            context.Result = new ObjectResult(GenerateResponseBody(exception))
            {
                StatusCode = 500,
            };

            context.ExceptionHandled = true;
        }
    }

    private static object GenerateResponseBody(Exception exception)
    {
        string message = "Erro inesperado no servidor.";

        if (exception.Message != null)
        {
            message += " " + exception.Message;
        }

        return new
        {
            message,
            variant = RequestVariant.Error.ToString(),
        };
    }
}

请求日志类

using System.IO.Pipelines;
using System.Text.Json;
using Microsoft.Extensions.Primitives;

namespace backend.Log;

public class RequestLog
{
    public static void Log(Exception exception, HttpRequest request)
    {
        request.EnableBuffering();

        DateTime dateTime = DateTime.Now;

        string requestPath = request.Path;
        string requestMethod = request.Method;

        List<KeyValuePair<string, StringValues>> requestQuery = request
            .Query.ToList<KeyValuePair<string, StringValues>>();

        List<KeyValuePair<string, StringValues>> requestHeaders = request
            .Headers.ToList<KeyValuePair<string, StringValues>>();

        Stream requestBody = request.Body;

        string exceptionMessage = exception.Message;
        string exceptionStackTrace = exception.StackTrace;

        string logText = "";

        logText += "DateTime: " + dateTime.ToString("dd/MM/yyyy HH:mm:ss") + "\n";

        logText += "Request:\n";
        logText += "\tPath: " + requestPath + "\n";
        logText += "\tMethod: " + requestMethod + "\n";

        string requestQueryLogText = GenerateRequestQueryLogText(requestQuery);

        logText += requestQueryLogText;

        string requestHeadersLogText = GenerateRequestHeadersLogText(requestHeaders);

        logText += requestHeadersLogText;

        string requestBodyLogText = GenerateRequestBodyLogText(requestBody);

        logText += requestBodyLogText;

        logText += "Exception:\n";
        logText += "\tMessage: " + exceptionMessage + "\n";
        logText += "\tStackTrace: " + exceptionStackTrace + "\n\n";

        using (StreamWriter writer = new StreamWriter("error.log", true))
        {
            writer.WriteLine(logText);
        }
    }

    private static string GenerateRequestQueryLogText(List<KeyValuePair<string, StringValues>> requestQuery)
    {
        string logText = "\tQuery:\n";

        requestQuery.ForEach((KeyValuePair<string, StringValues> e) =>
        {
            logText += "\t\t" + e.Key + ": " + e.Value + "\n";
        });

        return logText;
    }

    private static string GenerateRequestHeadersLogText(List<KeyValuePair<string, StringValues>> requestHeaders)
    {
        string logText = "\tHeaders:\n";

        requestHeaders.ForEach((KeyValuePair<string, StringValues> e) =>
        {
            logText += "\t\t" + e.Key + ": " + e.Value + "\n";
        });

        return logText;
    }

    private static string GenerateRequestBodyLogText(Stream requestBody)
    {
        string logText = "\tBody:\n";

        requestBody.Seek(0, SeekOrigin.Begin);

        using (StreamReader reader = new StreamReader(requestBody))
        {
            while (!reader.EndOfStream)
            {
                string line = reader.ReadLine();

                logText += "\t\t" + line + "\n";
            }
        }

        return logText;
    }
}

测试场景

发送请求体:

{ "login": "login", "password": "password" }

日志记录结果(请求体为空):

DateTime: 22/03/2024 17:43:00

Request:
    Path: /auth/sign-in
    Method: POST
    Query:
    Headers:
        Accept: application/json
        Connection: close
        Host: localhost:5071
        User-Agent: axios/1.6.7
        Accept-Encoding: gzip, compress, deflate, br
        Content-Type: application/json
        Content-Length: 58
        request-start-time: 1711140180634

    Body:
        
Exception:

    Message: testando log

    StackTrace:    at backend.Controllers.AuthController.SignIn(SignInDTO signInDTO) in C:\Users\jhose\desktop\projetos\day-manager\backend\Controllers\AuthController.cs:line 24
   at lambda_method3(Closure, Object, Object[])
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncActionResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()
问题原因
  1. EnableBuffering调用时机过晚:当前在OnActionExecuted阶段才调用request.EnableBuffering(),但此时MVC框架已经完成了请求体的读取(用于模型绑定),原始请求流已经被消费,且未被缓冲。EnableBuffering需要在请求体被读取前调用,才能将流包装为可回溯的缓冲流。
  2. 未重置流位置给后续使用:即使读取完请求体后,没有将流的位置重置回起始位置,可能影响后续需要读取请求体的操作(虽然当前场景是异常处理,但保持流的正确性是最佳实践)。
解决方案

步骤1:提前开启请求体缓冲

修改异常过滤器,在OnActionExecuting阶段就调用EnableBuffering(),确保请求体在被MVC读取前就被缓冲:

public void OnActionExecuting(ActionExecutingContext context) 
{
    // 提前开启缓冲,确保请求体可被多次读取
    context.HttpContext.Request.EnableBuffering();
}

步骤2:优化请求体读取逻辑

修改GenerateRequestBodyLogText方法,读取完成后将流位置重置回起始位置,同时简化读取代码:

private static string GenerateRequestBodyLogText(Stream requestBody)
{
    string logText = "\tBody:\n";

    if (!requestBody.CanSeek)
    {
        return logText + "\t\t无法读取请求体(流不支持回溯)\n";
    }

    // 记录当前位置,读完后恢复
    long originalPosition = requestBody.Position;
    requestBody.Seek(0, SeekOrigin.Begin);

    using (StreamReader reader = new StreamReader(requestBody))
    {
        // 一次性读取所有内容,比逐行读取更高效
        string bodyContent = reader.ReadToEnd();
        if (!string.IsNullOrEmpty(bodyContent))
        {
            logText += "\t\t" + bodyContent + "\n";
        }
    }

    // 恢复流的原始位置,避免影响后续操作
    requestBody.Seek(originalPosition, SeekOrigin.Begin);

    return logText;
}

步骤3:(可选)异步读取优化

如果你的API处理异步请求,建议将日志方法改为异步版本,避免同步阻塞:

public static async Task LogAsync(Exception exception, HttpRequest request)
{
    // ... 其他代码保持不变 ...

    string requestBodyLogText = await GenerateRequestBodyLogTextAsync(requestBody);

    // ... 其他代码保持不变 ...

    using (StreamWriter writer = new StreamWriter("error.log", true))
    {
        await writer.WriteLineAsync(logText);
    }
}

private static async Task<string> GenerateRequestBodyLogTextAsync(Stream requestBody)
{
    string logText = "\tBody:\n";

    if (!requestBody.CanSeek)
    {
        return logText + "\t\t无法读取请求体(流不支持回溯)\n";
    }

    long originalPosition = requestBody.Position;
    requestBody.Seek(0, SeekOrigin.Begin);

    using (StreamReader reader = new StreamReader(requestBody))
    {
        string bodyContent = await reader.ReadToEndAsync();
        if (!string.IsNullOrEmpty(bodyContent))
        {
            logText += "\t\t" + bodyContent + "\n";
        }
    }

    requestBody.Seek(originalPosition, SeekOrigin.Begin);

    return logText;
}

同时修改过滤器的OnActionExecuted为异步版本:

public async void OnActionExecuted(ActionExecutedContext context)
{
    if (context.Exception is Exception exception)
    {
        HttpRequest request = context.HttpContext.Request;

        await RequestLog.LogAsync(exception, request);

        context.Result = new ObjectResult(GenerateResponseBody(exception))
        {
            StatusCode = 500,
        };

        context.ExceptionHandled = true;
    }
}
验证结果

修改后重新发送请求,日志中会正确记录请求体内容:

DateTime: 22/03/2024 17:43:00

Request:
    Path: /auth/sign-in
    Method: POST
    Query:
    Headers:
        Accept: application/json
        Connection: close
        Host: localhost:5071
        User-Agent: axios/1.6.7
        Accept-Encoding: gzip, compress, deflate, br
        Content-Type: application/json
        Content-Length: 58
        request-start-time: 1711140180634

    Body:
        { "login": "login", "password": "password" }
        
Exception:

    Message: testando log

    StackTrace:    at backend.Controllers.AuthController.SignIn(SignInDTO signInDTO) in C:\Users\jhose\desktop\projetos\day-manager\backend\Controllers\AuthController.cs:line 24
   at lambda_method3(Closure, Object, Object[])
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncActionResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()

内容的提问来源于stack exchange,提问作者Jhosef Matheus

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 03:54:54