首页 文章

如何阅读ASP.NET Core Response.Body?

提问于
浏览
23

我一直在努力从ASP.NET Core操作中获取 Response.Body 属性,而我能够识别的唯一解决方案似乎不是最佳的 . 该解决方案需要将 MemoryStreamMemoryStream 交换,同时将流读入字符串变量,然后在发送到客户端之前将其交换回来 . 在下面的示例中,我试图在自定义中间件类中获取 Response.Body 值 . 由于某些原因, Response.Body 是ASP.NET Core中的一个仅设置属性?我在这里遗漏了什么,或者这是一个疏忽/错误/设计问题?有没有更好的方法来阅读 Response.Body

Current (sub-optimal) solution:

public class MyMiddleWare
{
    private readonly RequestDelegate _next;

    public MyMiddleWare(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        using (var swapStream = new MemoryStream())
        {
            var originalResponseBody = context.Response.Body;

            context.Response.Body = swapStream;

            await _next(context);

            swapStream.Seek(0, SeekOrigin.Begin);
            string responseBody = new StreamReader(swapStream).ReadToEnd();
            swapStream.Seek(0, SeekOrigin.Begin);

            await swapStream .CopyToAsync(originalResponseBody);
            context.Response.Body = originalResponseBody;
        }
    }
}

Attempted solution using EnableRewind(): 这仅适用于 Request.Body ,而不适用于 Response.Body . 这导致从 Response.Body 读取空字符串而不是实际的响应正文内容 .

Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime appLifeTime)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    app.Use(async (context, next) => {
        context.Request.EnableRewind();
        await next();
    });

    app.UseMyMiddleWare();

    app.UseMvc();

    // Dispose of Autofac container on application stop
    appLifeTime.ApplicationStopped.Register(() => this.ApplicationContainer.Dispose());
}

MyMiddleWare.cs

public class MyMiddleWare
{
    private readonly RequestDelegate _next;

    public MyMiddleWare(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        await _next(context);
        string responseBody = new StreamReader(context.Request.Body).ReadToEnd(); //responseBody is ""
        context.Request.Body.Position = 0;
    }
}

3 回答

  • 35

    在我最初的回答中,我完全误读了这个问题,并认为海报在询问如何阅读 Request.Body 但他曾问过如何阅读 Response.Body . 我将离开原来的答案以保存历史记录,但也会更新它以显示如果正确阅读它我将如何回答这个问题 .

    Original Answer

    如果您想要一个支持多次读取的缓冲流,则需要进行设置

    context.Request.EnableRewind()
    

    理想情况下,在需要读取正文的任何内容之前,尽早在中间件中执行此操作 .

    例如,您可以将以下代码放在Startup.cs文件的 Configure 方法的开头:

    app.Use(async (context, next) => {
                context.Request.EnableRewind();
                await next();
            });
    

    在启用Rewind之前,与 Request.Body 关联的流是仅向前流,不支持第二次搜索或读取流 . 这样做是为了使请求处理的默认配置尽可能轻量级和高性能 . 但是一旦启用了倒带,流就会升级到支持多次搜索和读取的流 . 您可以通过在调用 EnableRewind 之前和之后设置断点并观察 Request.Body 属性来观察此"upgrade" . 因此,例如 Request.Body.CanSeek 将从 false 更改为 true .

    更新:从ASP.NET Core 2.1 Request.EnableBuffering() 开始,它将 Request.Body 升级为 FileBufferingReadStream ,就像 Request.EnableRewind() 一样,并且由于 Request.EnableBuffering() 位于公共命名空间而不是内部命名空间,因此应优先于EnableRewind() . (感谢@ArjanEinbu指出)

    然后,你可以读取身体流,例如这样做:

    string bodyContent = new StreamReader(Request.Body).ReadToEnd();
    

    不要将 StreamReader 创建包装在using语句中,否则它将在使用块结束时关闭底层正文流,并且稍后在请求生命周期中的代码将无法读取正文 .

    另外,为了安全起见,最好遵循上面的代码行,用这行代码读取正文内容,将正文的流位置重置为0 .

    request.Body.Position = 0;
    

    这样,请求生命周期中稍后的任何代码都会找到request.Body处于尚未读取的状态 .

    Updated Answer

    对不起我原本误读了你的问题 . 将关联流升级为缓冲流的概念仍然适用 . 但是你必须手动完成它,我不知道任何内置的.Net Core功能,它允许你以 EnableRewind() 允许开发人员在读取请求流后重新读取请求流的方式读取响应流 .

    您的"hacky"方法可能完全合适 . 您基本上是将无法寻找的流转换为可以的流 . 在一天结束时, Response.Body 流必须与缓冲的流交换出来并支持搜索 . 这是中间件的另一种做法,但你会注意到它与你的方法非常相似 . 然而,我确实选择使用finally块作为添加保护,将原始流放回 Response.Body 并使用流的 Position 属性而不是 Seek 方法,因为语法稍微简单但效果与您没有区别做法 .

    public class ResponseRewindMiddleware {
            private readonly RequestDelegate next;
    
            public ResponseRewindMiddleware(RequestDelegate next) {
                this.next = next;
            }
    
            public async Task Invoke(HttpContext context) {
    
                Stream originalBody = context.Response.Body;
    
                try {
                    using (var memStream = new MemoryStream()) {
                        context.Response.Body = memStream;
    
                        await next(context);
    
                        memStream.Position = 0;
                        string responseBody = new StreamReader(memStream).ReadToEnd();
    
                        memStream.Position = 0;
                        await memStream.CopyToAsync(originalBody);
                    }
    
                } finally {
                    context.Response.Body = originalBody;
                }
    
            }
    
  • 5

    您所描述的黑客实际上是如何管理自定义中间件中的响应流的建议方法 .

    由于中间件设计的管道特性,每个中间件都不知道管道中的前一个或下一个处理器 . 无法保证当前的中间件是编写响应的中间件,除非它保留在传递它(当前中间件)控制的流之前给出的响应流 . 这种设计在OWIN中被看到并最终被融入到asp.net-core中 .

    一旦开始写入响应流,它就会将正文和 Headers (响应)发送给客户端 . 如果管道下的另一个处理程序在当前处理程序有机会之前执行该操作,那么一旦它已经被发送,它将无法向响应添加任何内容 .

    如果管道中的先前中间件遵循相同的传递策略,那么再次不能保证是实际的响应流另一条线下线 .

    引用ASP.NET Core Middleware Fundamentals

    警告在调用next之后小心修改HttpResponse,因为响应可能已经发送到客户端 . 您可以使用HttpResponse.HasStarted来检查标头是否已发送 . 警告调用write方法后不要调用next.Invoke . 中间件组件要么产生响应,要么调用next.Invoke,但不是两者 .

    来自 aspnet/BasicMiddleware Github repo的内置基本中间件示例

    ResponseCompressionMiddleware.cs

    /// <summary>
    /// Invoke the middleware.
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public async Task Invoke(HttpContext context)
    {
        if (!_provider.CheckRequestAcceptsCompression(context))
        {
            await _next(context);
            return;
        }
    
        var bodyStream = context.Response.Body;
        var originalBufferFeature = context.Features.Get<IHttpBufferingFeature>();
        var originalSendFileFeature = context.Features.Get<IHttpSendFileFeature>();
    
        var bodyWrapperStream = new BodyWrapperStream(context, bodyStream, _provider,
            originalBufferFeature, originalSendFileFeature);
        context.Response.Body = bodyWrapperStream;
        context.Features.Set<IHttpBufferingFeature>(bodyWrapperStream);
        if (originalSendFileFeature != null)
        {
            context.Features.Set<IHttpSendFileFeature>(bodyWrapperStream);
        }
    
        try
        {
            await _next(context);
            // This is not disposed via a using statement because we don't want to flush the compression buffer for unhandled exceptions,
            // that may cause secondary exceptions.
            bodyWrapperStream.Dispose();
        }
        finally
        {
            context.Response.Body = bodyStream;
            context.Features.Set(originalBufferFeature);
            if (originalSendFileFeature != null)
            {
                context.Features.Set(originalSendFileFeature);
            }
        }
    }
    
  • 1

    您可以在请求管道中使用middleware,以便记录请求和响应 .

    然而,由于以下事实增加了 memory leak 的危险:1 . 流,2 . 设置字节缓冲区和3.字符串转换

    最终可以Large Object Heap(如果请求或响应的主体大于85,000字节) . 这会增加应用程序中内存泄漏的危险 . 为了避免LOH,可以使用相关的library将内存流替换为Recyclable Memory stream .

    使用可循环内存流的实现:

    public class RequestResponseLoggingMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger _logger;
        private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
        private const int ReadChunkBufferLength = 4096;
    
        public RequestResponseLoggingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
        {
            _next = next;
            _logger = loggerFactory
                .CreateLogger<RequestResponseLoggingMiddleware>();
            _recyclableMemoryStreamManager = new RecyclableMemoryStreamManager();
        }
    
        public async Task Invoke(HttpContext context)
        {
            LogRequest(context.Request);
            await LogResponseAsync(context);
        }
    
        private void LogRequest(HttpRequest request)
        {
            request.EnableRewind();
            using (var requestStream = _recyclableMemoryStreamManager.GetStream())
            {
                request.Body.CopyTo(requestStream);
                _logger.LogInformation($"Http Request Information:{Environment.NewLine}" +
                                       $"Schema:{request.Scheme} " +
                                       $"Host: {request.Host} " +
                                       $"Path: {request.Path} " +
                                       $"QueryString: {request.QueryString} " +
                                       $"Request Body: {ReadStreamInChunks(requestStream)}");
            }
        }
    
        private async Task LogResponseAsync(HttpContext context)
        {
            var originalBody = context.Response.Body;
            using (var responseStream = _recyclableMemoryStreamManager.GetStream())
            {
                context.Response.Body = responseStream;
                await _next.Invoke(context);
                await responseStream.CopyToAsync(originalBody);
                _logger.LogInformation($"Http Response Information:{Environment.NewLine}" +
                                       $"Schema:{context.Request.Scheme} " +
                                       $"Host: {context.Request.Host} " +
                                       $"Path: {context.Request.Path} " +
                                       $"QueryString: {context.Request.QueryString} " +
                                       $"Response Body: {ReadStreamInChunks(responseStream)}");
            }
    
            context.Response.Body = originalBody;
        }
    
        private static string ReadStreamInChunks(Stream stream)
        {
            stream.Seek(0, SeekOrigin.Begin);
            string result;
            using (var textWriter = new StringWriter())
            using (var reader = new StreamReader(stream))
            {
                var readChunk = new char[ReadChunkBufferLength];
                int readChunkLength;
                //do while: is useful for the last iteration in case readChunkLength < chunkLength
                do
                {
                    readChunkLength = reader.ReadBlock(readChunk, 0, ReadChunkBufferLength);
                    textWriter.Write(readChunk, 0, readChunkLength);
                } while (readChunkLength > 0);
    
                result = textWriter.ToString();
            }
    
            return result;
        }
    }
    

    NB . 由于 textWriter.ToString() ,LOH的危害并未完全消除,另一方面,您可以使用支持结构化日志记录(即Serilog)的日志客户端库并注入可循环内存流的实例 .

相关问题