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()
问题原因
EnableBuffering调用时机过晚:当前在OnActionExecuted阶段才调用request.EnableBuffering(),但此时MVC框架已经完成了请求体的读取(用于模型绑定),原始请求流已经被消费,且未被缓冲。EnableBuffering需要在请求体被读取前调用,才能将流包装为可回溯的缓冲流。- 未重置流位置给后续使用:即使读取完请求体后,没有将流的位置重置回起始位置,可能影响后续需要读取请求体的操作(虽然当前场景是异常处理,但保持流的正确性是最佳实践)。
解决方案
步骤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
相关产品推荐
相关产品推荐

