.NET 7读取HTTP响应体时遇System.NotSupportedException的排查与解决
异常成因及解决方案
异常原因
System.NotSupportedException的核心原因是:默认的HttpResponse.Body是一个仅支持写入、不支持读取/查找操作的流。当你调用await _next(context)后,后端处理逻辑已经将响应内容写入这个流并开始向客户端发送,此时尝试对原流执行CopyToAsync或Seek操作,会触发流不支持该操作的异常。
正确实现方式
要记录响应体,必须在执行后续中间件/处理程序之前,将原响应体替换为一个可读写的MemoryStream,让所有响应内容先写入这个内存流,之后我们可以安全地读取它,再将内容回写到原流发送给客户端。
修改后的完整中间件代码如下:
using System.Diagnostics; using System.Text; namespace WebApi.Middlewares; public class RequestLoggerMiddleware { private readonly RequestDelegate _next; private readonly ILogger<RequestLoggerMiddleware> _logger; public RequestLoggerMiddleware(RequestDelegate next, ILogger<RequestLoggerMiddleware> logger) { _next = next; _logger = logger; } public async Task Invoke(HttpContext context) { var sw = Stopwatch.StartNew(); string requestBodyText = string.Empty; // 处理请求体记录 if (context.Request.ContentLength > 0 && context.Request.ContentType?.Contains("application/json") == true) { var requestBodyStream = new MemoryStream(); var originalRequestBody = context.Request.Body; try { await context.Request.Body.CopyToAsync(requestBodyStream); requestBodyStream.Seek(0, SeekOrigin.Begin); requestBodyText = await new StreamReader(requestBodyStream, Encoding.UTF8).ReadToEndAsync(); requestBodyStream.Seek(0, SeekOrigin.Begin); context.Request.Body = requestBodyStream; // 重置请求体流,确保后续中间件能读取 } finally { // 此处无需恢复原流,已将MemoryStream重置,后续中间件可正常读取 } } _logger.LogInformation("Incoming request {Method} {Path} {Query} {RequestBody}", context.Request.Method, context.Request.Path, context.Request.QueryString, requestBodyText); // 处理响应体记录 var originalResponseBody = context.Response.Body; using var responseBodyStream = new MemoryStream(); context.Response.Body = responseBodyStream; try { await _next(context); // 执行后续处理,响应会写入responseBodyStream sw.Stop(); var responseTime = sw.Elapsed.TotalMilliseconds; // 重置内存流位置,读取响应内容 responseBodyStream.Seek(0, SeekOrigin.Begin); var responseBodyText = await new StreamReader(responseBodyStream, Encoding.UTF8).ReadToEndAsync(); _logger.LogInformation("Outgoing response {StatusCode} {Method} {Path} {Query} {ResponseBody} {ResponseTime:0.000}ms", context.Response.StatusCode, context.Request.Method, context.Request.Path, context.Request.QueryString, responseBodyText, responseTime); // 将响应内容复制回原流,发送给客户端 responseBodyStream.Seek(0, SeekOrigin.Begin); await responseBodyStream.CopyToAsync(originalResponseBody); } catch (Exception ex) { _logger.LogError(ex, "Error processing request {Method} {Path} {Query}", context.Request.Method, context.Request.Path, context.Request.QueryString); throw; } finally { // 恢复原响应体流,避免资源泄漏 context.Response.Body = originalResponseBody; } } }
关键注意事项
- 替换响应体的时机:必须在调用
_next(context)之前替换,确保后续所有处理逻辑都写入我们的内存流。 - 恢复原流:在finally块中恢复原响应体,避免资源泄漏。
- 重置流位置:每次读取内存流前,必须用
Seek(0, SeekOrigin.Begin)将位置重置到开头,否则会读取到空内容。 - Content-Length处理:如果原响应设置了
Content-Length,替换流后实际长度可能变化,ASP.NET Core会自动重新计算并设置正确的Content-Length,无需手动处理。 - 请求体优化:添加了对请求体的判断(仅当有内容且是JSON类型时才读取),避免无意义的流操作。
内容的提问来源于stack exchange,提问作者Muharrem Servet ANKARALI
相关产品推荐
相关产品推荐

