35

I am re-implementing a request logger as Owin Middleware which logs the request url and body of all incoming requests. I am able to read the body, but if I do the body parameter in my controller is null.

I'm guessing it's null because the stream position is at the end so there is nothing left to read when it tries to deserialize the body. I had a similar issue in a previous version of Web API but was able to set the Stream position back to 0. This particular stream throws a This stream does not support seek operations exception.

In the most recent version of Web API 2.0 I could call Request.HttpContent.ReadAsStringAsync()inside my request logger, and the body would still arrive to the controller in tact.

How can I rewind the stream after reading it?

or

How can I read the request body without consuming it?

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

    public override Task Invoke(IOwinContext context)
    {
        return Task.Run(() => {
            string body = new StreamReader(context.Request.Body).ReadToEnd();
            // log body

            context.Request.Body.Position = 0; // cannot set stream position back to 0
            Console.WriteLine(context.Request.Body.CanSeek); // prints false
            this.Next.Invoke(context);
        });
    }
}

public class SampleController : ApiController 
{
    public void Post(ModelClass body)
    {
        // body is now null if the middleware reads it
    }
}
Despertar
  • 21,627
  • 11
  • 81
  • 79

4 Answers4

45

Just found one solution. Replacing the original stream with a new stream containing the data.

    public override async Task Invoke(IOwinContext context)
    {
        return Task.Run(() => {
            string body = new StreamReader(context.Request.Body).ReadToEnd();
            // log body

            byte[] requestData = Encoding.UTF8.GetBytes(body);
            context.Request.Body = new MemoryStream(requestData);
            await this.Next.Invoke(context);
        });
    }

If you are dealing with larger amounts of data, I'm sure a FileStream would also work as the replacement.

Despertar
  • 21,627
  • 11
  • 81
  • 79
  • 5
    Another alternative: Stream anotherStream = new MemoryStream(); context.Request.Body.CopyToAsync(anotherStream); – John Korsnes Nov 17 '14 at 15:05
  • 13
    In ASP.NET Core (where the invoke method would have the following signature `async Task Invoke(HttpContext context)`) you can do the following to get a buffered stream which allows you to seek and read it multiple times; `context.Request.EnableRewind()` (HttpRequest extension method found in `Microsoft.AspNetCore.Http.Internal.BufferingHelper`). – jfiskvik Jul 13 '16 at 14:25
  • @jfiskvik Your input was extremely helpful for me, It helped me rewind my stream after some processing that happened in my MiddleWare – ExtremeSwat Nov 02 '16 at 15:51
  • 1
    When using `.NET Core 3.1` (and up) you can use `context.Request.EnableBuffering()` (instead of `context.Request.EnableRewind()` per the answer of @jfiskvik) found in `Microsoft.AspNetCore.Http`. This enables me to reset the body like so: `context.Request.Body.Seek(0, SeekOrigin.Begin);`. – Aage May 17 '21 at 07:48
  • 2
    WARNING: This answer is dangerous: - The `Task` result from `Next.Invoke()` is not used (or awaited) - `Task.Run` will run the continuation in a thread-pool thread - no `SynchronizationContext` and so `HttpContext.Current` will not be set correctly. Under load, this code will do BAD things. – eddiewould Aug 09 '21 at 04:42
  • @eddiewould I added an await to the Next.Invoke(). – Despertar Oct 19 '21 at 14:50
13

Here's a small improvement to the first answer by Despertar, which helped me a lot, but I ran into an issue when working with binary data. The intermediate step of extracting the stream into a string and then putting it back into a byte array using Encoding.UTF8.GetBytes(body) messes up the binary content (the contents will change unless it is an UTF8 encoded string). Here's my fix using Stream.CopyTo():

    public override async Task Invoke(IOwinContext context)
    {
        // read out body (wait for all bytes)
        using (var streamCopy = new MemoryStream())
        {
            context.Request.Body.CopyTo(streamCopy);
            streamCopy.Position = 0; // rewind

            string body = new StreamReader(streamCopy).ReadToEnd();
            // log body

            streamCopy.Position = 0; // rewind again
            context.Request.Body = streamCopy; // put back in place for downstream handlers

            await this.Next.Invoke(context);
        }
    }

Also, MemoryStream is nice because you can check the stream's length before logging the full body (which is something I don't want to do in case someone uploads a huge file).

Efrain
  • 3,248
  • 4
  • 34
  • 61
0

I have the same need and solved the issue by doing the below:

In Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<IISServerOptions>(options =>
    {
        options.AllowSynchronousIO = true;
    });
    [...]
}
public void Configure(IApplicationBuilder app..)
{
   app.UseWhen(
      ab => ab.UseMiddleware<EnableRequestBodyBufferingMiddleware>()
   );
}

Create a new file for your middleware class. In your middleware:

public class EnableRequestBodyBufferingMiddleware
{
        private readonly RequestDelegate _next;

        public EnableRequestBodyBufferingMiddleware(RequestDelegate next) =>
            _next = next;

        public async Task InvokeAsync(HttpContext context)
        {
            Stream originalBody = context.Response.Body;
            try
            {
                using (var memStream = new MemoryStream())
                {
                    context.Request.EnableBuffering();

                    context.Request.Body.CopyTo(memStream);
                    memStream.Position = 0; // rewind

                    string body = new StreamReader(memStream).ReadToEnd();

                    memStream.Position = 0; // rewind again
                    context.Request.Body = memStream; // put back in place for downstream handlers

                    await _next(context);
                }
            }
            finally
            {
                context.Response.Body = originalBody;
            }
        }
}

In your controller:

[HttpPost()]
public async Task<IActionResult> GetRequestBodyEndpoint([FromBody]YourModelObject myObj)
{
    HttpContext.Request.Body.Position = 0;
    var reader = new StreamReader(HttpContext.Request.Body);
    var originalRequest = await reader.ReadToEndAsync().ConfigureAwait(false);
    [...]
}

Here, the originalRequest holds the actual request sent to my POST endpoint. Adding it here for my future reference.

-3

I know this is old, but just to help anyone that comes across this. You need to seek on the stream: context.Request.Body.Seek(0, SeekOrigin.Begin);

Mitch
  • 659
  • 1
  • 7
  • 18
  • 4
    Same as `Body.Position = 0`, this throws a `NotSupportedException` with the message `This stream does not support seek operations`. Also the `Body.CanSeek` property returns false. – Despertar Aug 23 '14 at 22:18