How to inspect MVC response stream using OWIN midd

2020-06-03 07:38发布

问题:

This question has been asked before in a few forms but I cannot get any of the answers to work, I'm losing my hair and unsure if the problem is just that the solutions were from 2 years ago and things have changed.

How can I safely intercept the Response stream in a custom Owin Middleware - I based my code on this, it looks like it should work, but it doesn't

OWIN OnSendingHeaders Callback - Reading Response Body - seems to be a different OWIN version, because method signature doesn't work

What I want to do is write an OMC that can inspect the response stream from MVC.

What I did (amongst several other attempts), is to add an OMC that sets context.Response.Body to a MemoryStream, so I can rewind it and inspect what was written by downstream components:

    public async Task Invoke(IDictionary<string, object> env)
    {

        IOwinContext context = new OwinContext(env);

        // Buffer the response
        var stream = context.Response.Body;
        var buffer = new MemoryStream();
        context.Response.Body = buffer;
        .......

What I find is that the MemoryStream is always empty, unless I write to it from another OMC. So it seems that downstream OMCs are using my MemoryStream, but MVC responses are not, as if the OWIN pipeline completes before the request goes to MVC, but that's not right is it?

Complete code:

public partial class Startup
{
    public void Configuration(IAppBuilder app)
    {
        ConfigureAuth(app);
        app.Use(new ResponseExaminerMiddleware());

        // Specify the stage for the OMC
        //app.UseStageMarker(PipelineStage.Authenticate);
    }


}




public class ResponseExaminerMiddleware
{
    private AppFunc next;

    public void Initialize(AppFunc next)
    {
        this.next = next;
    }

    public async Task Invoke(IDictionary<string, object> env)
    {

        IOwinContext context = new OwinContext(env);

        // Buffer the response
        var stream = context.Response.Body;
        var buffer = new MemoryStream();
        context.Response.Body = buffer;

        await this.next(env);

        buffer.Seek(0, SeekOrigin.Begin);
        var reader = new StreamReader(buffer);
        string responseBody = await reader.ReadToEndAsync();

        // Now, you can access response body.
        System.Diagnostics.Debug.WriteLine(responseBody);

        // You need to do this so that the response we buffered
        // is flushed out to the client application.
        buffer.Seek(0, SeekOrigin.Begin);
        await buffer.CopyToAsync(stream);

    }

}

For what it's worth I also tried a suggestion where the response.Body stream is set to a Stream subclass, just so I could monitor what is written to the stream and bizarrely the Stream.Write method is called, but with an empty byte array, never any actual content...

回答1:

MVC does not pass its request through OWIN pipeline. To capture MVC response we need to make custom response filter that captures response data

/// <summary>
/// Stream capturing the data going to another stream
/// </summary>
internal class OutputCaptureStream : Stream
{
    private Stream InnerStream;
    public MemoryStream CapturedData { get; private set; }

    public OutputCaptureStream(Stream inner)
    {
        InnerStream = inner;
        CapturedData = new MemoryStream();
    }

    public override bool CanRead
    {
        get { return InnerStream.CanRead; }
    }

    public override bool CanSeek
    {
        get { return InnerStream.CanSeek; }
    }

    public override bool CanWrite
    {
        get { return InnerStream.CanWrite; }
    }

    public override void Flush()
    {
        InnerStream.Flush();
    }

    public override long Length
    {
        get { return InnerStream.Length; }
    }

    public override long Position
    {
        get { return InnerStream.Position; }
        set { CapturedData.Position = InnerStream.Position = value; }
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        return InnerStream.Read(buffer, offset, count);
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        CapturedData.Seek(offset, origin);
        return InnerStream.Seek(offset, origin);
    }

    public override void SetLength(long value)
    {
        CapturedData.SetLength(value);
        InnerStream.SetLength(value);
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        CapturedData.Write(buffer, offset, count);
        InnerStream.Write(buffer, offset, count);
    }
}

And then we make a logging middleware that can log both kinds of responses properly

public class LoggerMiddleware : OwinMiddleware
{
    public LoggerMiddleware(OwinMiddleware next): base(next)
    {
    }

    public async override Task Invoke(IOwinContext context)
    {
        //to intercept MVC responses, because they don't go through OWIN
        HttpResponse httpResponse = HttpContext.Current.Response;
        OutputCaptureStream outputCapture = new OutputCaptureStream(httpResponse.Filter);
        httpResponse.Filter = outputCapture;

        IOwinResponse owinResponse = context.Response;
        //buffer the response stream in order to intercept downstream writes
        Stream owinResponseStream = owinResponse.Body;
        owinResponse.Body = new MemoryStream();

        await Next.Invoke(context);

        if (outputCapture.CapturedData.Length == 0) {
            //response is formed by OWIN
            //make sure the response we buffered is flushed to the client
            owinResponse.Body.Position = 0;
            await owinResponse.Body.CopyToAsync(owinResponseStream);
        } else {   
            //response by MVC
            //write captured data to response body as if it was written by OWIN         
            outputCapture.CapturedData.Position = 0;
            outputCapture.CapturedData.CopyTo(owinResponse.Body);
        }

        LogResponse(owinResponse);
    }
}