如何在ASP.NET Core 1.0中记录HTTP响应主体

15

我正在使用ASP.NET Core 1.0 RC2创建一个公共REST API,并希望记录传入请求和传出响应。

我已经创建了一个中间件类,它被添加到管道中,在调用app.UseMvc()之前执行;

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{            
        app.UseIOMiddleware();
        app.UseMvc();            
}

我的中间件类看起来像这样:

public class IOMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task Invoke(HttpContext context)
    {
        LogRequest(context.Request);

        await _next.Invoke(context);            
    }

    private async void LogRequest(HttpRequest request)
    {
        using (var bodyReader = new StreamReader(request.Body))
        {
            string body = await bodyReader.ReadToEndAsync();

            request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));

            System.Diagnostics.Debug.Print(body);
        }
    }
}

我可以使用这个例子读取请求体流并倒带: Rewind request body stream,但是我不确定如何读取响应体,因为它的流不可读。

在Web API 2.0中,我可以使用HttpResponseMessage.Content.ReadAsByteArrayAsync()方法,但在ASP.Net Core 1.0 RC2中我该如何实现相同的功能?


这个回答解决了你的问题吗?如何读取ASP.NET Core Response.Body? - JTW
3个回答

14

问题在于request.Body是不可读的,只能写入 - 通常情况下,流会定期刷新到客户端。

您可以通过替换流并缓冲内容直到管道的其余部分完成来解决此问题。

public class IOMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task Invoke(HttpContext context)
    {
        await LogRequest(context.Request);

        await LogResponseAndInvokeNext(context);
    }

    private async Task LogRequest(HttpRequest request)
    {
        using (var bodyReader = new StreamReader(request.Body))
        {
            string body = await bodyReader.ReadToEndAsync();

            request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));
            System.Diagnostics.Debug.Print(body);
        }
    }

    private async Task LogResponseAndInvokeNext(HttpContext context)
    {
        using (var buffer = new MemoryStream())
        {
            //replace the context response with our buffer
            var stream = context.Response.Body;
            context.Response.Body = buffer;

            //invoke the rest of the pipeline
            await _next.Invoke(context);

            //reset the buffer and read out the contents
            buffer.Seek(0, SeekOrigin.Begin);
            var reader = new StreamReader(buffer);
            using (var bufferReader = new StreamReader(buffer))
            {
                string body = await bufferReader.ReadToEndAsync();

                //reset to start of stream
                buffer.Seek(0, SeekOrigin.Begin);

                //copy our content to the original stream and put it back
                await buffer.CopyToAsync(stream);
                context.Response.Body = stream;

                System.Diagnostics.Debug.Print($"Response: {body}");

            }
        }
    }
}

要读取请求正文,您可以执行以下操作以获取缓冲流,从而允许您多次查找和读取它; context.Request.EnableRewind()(在Microsoft.AspNetCore.Http.Internal.BufferingHelper中找到的HttpRequest扩展方法)。 - jfiskvik
1
我不确定这样做的副作用会是什么。由于缓冲区正在通过套接字流传输,可能会产生意外结果。正文流的读取未实现是有原因的。 - Serguei Fedorov
将请求和响应日志记录封装到一个单独的类中,同时避免了缓存文件的异常,并在 github 上正确地进行了处理和释放。我希望它能被广泛使用,因为任何错误都会被发现并解决。 - Steve Hibbert
2
@SergueiFedorov 我看到这种行为的一个副作用。现在,直到现在可以安全地流式传输到客户端的数据现在必须完全保存在内存中。一些本来可以成功逐个分段流式传输的数据,如果太大而无法存储在内存中,现在将抛出OutOfMemoryException或类似的异常。 - Dominik Szymański

4

不幸的是,如果您将Request替换为MemoryStream,则同一流将用于未来的调用。

这里有一个错误:

https://github.com/aspnet/KestrelHttpServer/issues/940

解决方法是将Request.Body流复制到本地变量中,并在最后将Body设置回原始流。

像这样:

  public async Task Invoke(HttpContext context)
    {
        //Workaround - copy original Stream
        var initalBody = context.Request.Body;

        using (var bodyReader = new StreamReader(request.Body))
        {
            string body = await bodyReader.ReadToEndAsync();
            //Do something with body
            //Replace write only request body with read/write memorystream so you can read from it later

               request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));

        //handle other middlewares
        await _next.Invoke(context);

        //Workaround - return back to original Stream
        context.Request.Body = initalBody;
    }

暂时修复(1.0.1+):) - Pavel Biryukov
25
问题要求回应正文内容,而不是请求正文内容。 - Warren Parad

1

在到处搜索后,我最终选择了这个类。对我来说它运作良好,并处理了异常情况[以前它不会返回任何响应,但是成功记录了它!]。这是许多在线帖子的集体产品。

using Microsoft.AspNetCore.Http;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNetCore.Http.Internal;



public class LoggerMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task Invoke(HttpContext context)
    {
        using (MemoryStream requestBodyStream = new MemoryStream())
        using (MemoryStream responseBodyStream = new MemoryStream())
        {
            Stream originalRequestBody = context.Request.Body;
            context.Request.EnableRewind();
            Stream originalResponseBody = context.Response.Body;

            try
            {
                await context.Request.Body.CopyToAsync(requestBodyStream);
                requestBodyStream.Seek(0, SeekOrigin.Begin);

                string requestBodyText = new StreamReader(requestBodyStream).ReadToEnd();

                requestBodyStream.Seek(0, SeekOrigin.Begin);
                context.Request.Body = requestBodyStream;

                string responseBody = "";

                context.Response.Body = responseBodyStream;

                Stopwatch watch = Stopwatch.StartNew();
                    await _next(context);
                    watch.Stop();

                responseBodyStream.Seek(0, SeekOrigin.Begin);
                responseBody = new StreamReader(responseBodyStream).ReadToEnd();
                AuditLogger.LogToAudit(context.Request.Host.Host,
                    context.Request.Path, context.Request.QueryString.ToString(), context.Connection.RemoteIpAddress.MapToIPv4().ToString(),
                        string.Join(",", context.Request.Headers.Select(he => he.Key + ":[" + he.Value + "]").ToList()),
                        requestBodyText, responseBody, DateTime.Now, watch.ElapsedMilliseconds);

                responseBodyStream.Seek(0, SeekOrigin.Begin);

                await responseBodyStream.CopyToAsync(originalResponseBody);
            }
            catch (Exception ex)
            {
                ExceptionLogger.LogToDatabse(ex);
                byte[] data = System.Text.Encoding.UTF8.GetBytes("Unhandled Error occured. Please, try again in a while.");
                originalResponseBody.Write(data, 0, data.Length);
            }
            finally
            {
                context.Request.Body = originalRequestBody;
                context.Response.Body = originalResponseBody;
            }
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接