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

3

11

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()));
Undershrub answered 1/11, 2019 at 2:56 Comment(0)
B
15
  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;
        }
    
Bronny answered 4/11, 2019 at 6:6 Comment(2)
Thank you for the explanation. For file content response, I think httpContext.Response.SendFileAsync will work as well, eliminating the need for FileStream altogether.Undershrub
@Undershrub Yes, absolutely. The above CopyToAsync is just a way that illustrates how to deal with a plain Stream. If you're dealing with a File, feel free to use an easier way :)Bronny
D
0

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).

Dikdik answered 1/11, 2021 at 15:11 Comment(0)
A
0

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);
Almond answered 9/5, 2022 at 18:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.