Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Modify static file response in ASP.NET Core

I serve a bunch of static files in my app with app.UseStaticFiles(). I'd like to inject some additional markup into the response for a particular HTML file before it's sent. My first attempt was to add middleware like this before the static files middleware:

app.Use(async (context, next) => {
    await next();

    // Modify the response here
});

However, this doesn't work as I can't actually read the read the response stream - it's using Kestrel's FrameResponseStream under the hood, which is unreadable.

So, I figured I could replace the response body stream with a MemoryStream that I could write to:

app.Use(async (context, next) => {
    context.Response.Body = new MemoryStream();
    await next();

    // Modify the response here
});

But this just causes the request to never complete - it goes through all the pipeline stages, but it never even returns any headers to the browser.

So, is there any way I can modify the response that is produced by the StaticFileMiddleware?


Update

As the HTML file in question is tiny (765 bytes), memory consumption isn't a concern. However, any attempts to read/modify the response still causes the same problem as before (nothing is returned). More explicitly, here's what's being done:

app.Use(async (context, next) => {
    var originalStream = context.Response.Body;
    var bufferStream = new MemoryStream();
    context.Response.Body = bufferStream;
    await next();

    bufferStream.Seek(0, SeekOrigin.Begin);

    if (/* some condition */)
    {
        var reader = new StreamReader(bufferStream);
        var response = await reader.ReadToEndAsync();

        // The response string is modified here

        var writer = new StreamWriter(originalStream);
        await writer.WriteAsync(response);
    }
    else
    {
        await bufferStream.CopyToAsync(originalStream);
    }
});

The files hitting the else condition are returned just fine, but the particular file in the if condition causes trouble. Even if I don't modify the stream at all, it still hangs.

like image 429
Martin Wedvich Avatar asked Oct 30 '22 18:10

Martin Wedvich


1 Answers

Yes, the default stream provided is read only because the data is only buffered for a short moment and flushed to the client. Hence you can't rewind or read it.

Your second attempt doesn't work because the original stream is never processed. You replaced the response body stream entirely with MemoryStream and thrown away the original request, so there is never something written to it and the client waits forever.

You must not forget, that the stream to the client is within the original stream, you can't just replace it with something else.

After calling await next() you must read the data from the MemoryStream and then write it to the original stream.

app.Use(async (context, next) => {
    var originalStream = context.Response.Body;
    var memoryStream = new MemoryStream();
    context.Response.Body = memoryStream;
    await next();

    // Here you must read the MemoryStream, modify it, then write the 
    // result into "originalStream"
});

Remark

But be aware, that this solution will buffer the whole response into the servers memory, so if you send large files this will significantly degenerate the performance of your ASP.NET Core application, especially if you serve files which are several megabytes in size and cause the garbage collection to be triggered more often.

And this wouldn't only affect your static files, but also all your regular requests, because the MVC Middleware is called after the static files middleware.

If you really want to modify a single (or a list of files) on each request, I'd rather suggest you doing this inside a controller and route certain files there. Remember, if the given file is not found by the static files middleware, it will call the next one in chain until it comes to the mvc middleware.

Just setup a route there that will match a specific file or folder and route it to a controller. Read the file in the controller and write it to the response stream or just return the new steam (using return File(stream, contentType);

like image 53
Tseng Avatar answered Nov 15 '22 05:11

Tseng