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.

Martin Wedvich
  • 2,158
  • 2
  • 21
  • 37

1 Answers1

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

Tseng
  • 61,549
  • 15
  • 193
  • 205
  • 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();``` – Martin Wedvich Oct 13 '16 at 11:09
  • 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 – Tseng Oct 13 '16 at 11:14
  • Alternatively `CopyTo` method from Stream should be easier to use, to simply copy the content of a stream into a new one – Tseng Oct 13 '16 at 11:15
  • 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. – Martin Wedvich Oct 13 '16 at 11:23
  • 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 – Tseng Oct 13 '16 at 11:27
  • That's still not satisfying, though. I keep it in the wwwroot folder as I run a dev server in parallel for the client-side part of this during development, so the index.html file *needs* to be there, while on the live version it's all served by the ASP.NET Core app. – Martin Wedvich Oct 13 '16 at 11:34