Marcin Zablocki
Marcin Zablocki

Reputation: 10683

Replacing response stream in ASP.NET Core 1.0 middleware

I want to write Custom Middleware in my ASP.NET Core 1.0 project which will replace original framework's Http Response Stream to my own, so I will be able to perform read / seek / write operations on it (first 2 are not possible on the original stream) in the further code i.e. in Actions or Filters.

I've started with the following code:

public class ReplaceStreamMiddleware
{
    protected RequestDelegate NextMiddleware;

    public ReplaceStreamMiddleware(RequestDelegate next)
    {
        NextMiddleware = next;
    }

    public async Task Invoke(HttpContext httpContext)
    {       
        using (var responseStream = new MemoryStream())
        {
            var fullResponse = httpContext.Response.Body;
            httpContext.Response.Body = responseStream;
            await NextMiddleware.Invoke(httpContext);
            responseStream.Seek(0, SeekOrigin.Begin);
            await responseStream.CopyToAsync(fullResponse);
        }   
    }
}

The problem with the following code is that sometimes the fullResponse stream is already closed at the time of invoking await responseStream.CopyToAsync(fullResponse); so it throws an exception Cannot access a closed Stream.

This weird behaviour is easy to observe when I load the page in the browser and then refresh, before it loads completely.

I would like to know:

  1. why this happens?
  2. how to prevent it?
  3. is my solution a good idea or there is another way to replace response stream?

Upvotes: 3

Views: 3300

Answers (1)

Victor Hurdugaci
Victor Hurdugaci

Reputation: 28425

The exception doesn't come from your CopyToAsync. It's from one of your code's callers:

You're not restoring the original response stream in HttpContext. Therefore, whoever calls your middleware will get back a closed MemoryStream.

Here's some working code:

app.Use(async (httpContext, next) =>
{
    using (var memoryResponse = new MemoryStream())
    {
        var originalResponse = httpContext.Response.Body;
        try
        {
            httpContext.Response.Body = memoryResponse;

            await next.Invoke();

            memoryResponse.Seek(0, SeekOrigin.Begin);
            await memoryResponse.CopyToAsync(originalResponse);
        }
        finally
        {
            // This is what you're missing
            httpContext.Response.Body = originalResponse;
        }
    }
});

app.Run(async (context) =>
{
    context.Response.ContentType = "text/other";
    await context.Response.WriteAsync("Hello World!");
});

Upvotes: 8

Related Questions