'How to directly set response body to a file stream in ASP.NET Core middleware?

Sample code below to write a file stream to Response.Body in an ASP.NET Core middleware doesn't work (emits empty response):

public Task Invoke(HttpContext context)
{
    context.Response.ContentType = "text/plain";

    using (var fs = new FileStream("/valid-path-to-file-on-server.txt", FileMode.Open)
    using (var sr = new StreamReader(fs))
    {
        context.Response.Body = sr.BaseStream;
    }

    return Task.CompletedTask;
}

Any ideas what could be wrong with this approach of directly setting the context.Response.Body?

Note: any next middleware in the pipeline is skipped for no further processing.

Update (another example): a simple MemoryStream assignment doesn't work either (empty response):

context.Response.Body = new MemoryStream(Encoding.UTF8.GetBytes(DateTime.Now.ToString()));


Solution 1:[1]

  1. No. You can never do that directly.

    Note that context.Response.Body is a reference to an object (HttpResponseStream) that is initialized before it becomes available in HttpContext. It is assumed that all bytes are written into this original Stream. If you change the Body to reference (point to) a new stream object by context.Response.Body = a_new_Stream, the original Stream is not changed at all.

    Also, if you look into the source code of ASP.NET Core, you'll find the Team always copy the wrapper stream to the original body stream at the end rather than with a simple replacement(unless they're unit-testing with a mocked stream). For example, the SPA Prerendering middleware source code:

        finally
        {
            context.Response.Body = originalResponseStream;
            ...
    

    And the ResponseCachingMiddleware source code:

        public async Task Invoke(HttpContext httpContext)
        {
            ...
            finally
            {
                UnshimResponseStream(context);
            }
            ...
        }
    
        internal static void UnshimResponseStream(ResponseCachingContext context)
        {
            // Unshim response stream
            context.HttpContext.Response.Body = context.OriginalResponseStream;
    
            // Remove IResponseCachingFeature
            RemoveResponseCachingFeature(context.HttpContext);
        }
    
  2. As a walkaround, you can copy the bytes to the raw stream as below:

    public async Task Invoke(HttpContext context)
    {
        context.Response.ContentType = "text/plain";
        using (var fs = new FileStream("valid-path-to-file-on-server.txt", FileMode.Open))
        {
            await fs.CopyToAsync(context.Response.Body);
        }
    }
    

    Or if you like to hijack the raw HttpResponseStream with your own stream wrapper:

        var originalBody = HttpContext.Response.Body;
        var ms = new MemoryStream();
        HttpContext.Response.Body = ms;
        try
        {
            await next();
            HttpContext.Response.Body = originalBody;
            ms.Seek(0, SeekOrigin.Begin);
            await ms.CopyToAsync(HttpContext.Response.Body);
        }
        finally
        {
            response.Body = originalBody;
        }
    

Solution 2:[2]

The using statements in the question causes your stream and stream reader to be rather ephemeral, so they will both be disposed. The extra reference to the steam in "body" wont prevent the dispose.

The framework disposes of the stream after sending the response. (The medium is the message).

Solution 3:[3]

In net 6 I found I was getting console errors when I tried to do this e.g.:

System.InvalidOperationException: Response Content-Length mismatch: too many bytes written (25247 of 8863).

The solution was to remove the relevant header:

context.Response.Headers.Remove("Content-Length");
await context.Response.SendFileAsync(filename);

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 dodbrian
Solution 2 benhorgen
Solution 3 drk-mtr