6

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.

1
  • Did you found a suitable solution for this ? I have the same use case I'm trying to resolve Commented Apr 1, 2021 at 17:23

1 Answer 1

3

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

Sign up to request clarification or add additional context in comments.

6 Comments

Could you provide an example of how you read/write the stream after the await next(); statement, though? Doing the following doesn't work: var sr = new StreamReader(buffer); var responseHtml = await sr.ReadToEndAsync(); // (modifying HTML response here) var sw = new StreamWriter(originalStream); await sw.WriteAsync(responseHtml); await sw.FlushAsync();
Use the ReadBytes method directly on the memory stream, read a chunk into a buffer, write the buffer to the original stream. Repeat until done. Otherwise you use double memory for reading the whole file twice in memory
Alternatively CopyTo method from Stream should be easier to use, to simply copy the content of a stream into a new one
The file in question is tiny - 765 bytes to be exact, so memory consumption isn't a concern here. Is there any way to tell the static files middleware to ignore that particular file, though? Because it does exist in the wwwroot directory, so it'll never hit the MVC middleware otherwise.
Well, put it in a different folder, like wwwroot/dynamic/file.html and set a route to /staticfiles/{*slug} and redirect it to DynamicFilesController. The {*slug} will contain the remaining part of the url. Now if the file is not found in staticfiles folder, it will call the controller. Check if the file exists in dynamic folder and modify it, otherwise return FileNotFound
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.