Sam Eaton
Sam Eaton

Reputation: 1835

How to modify the HTTP::Response after it has been written to

I'm trying to write some tooling for Crystal (specifically Kemal) where I can see if the response content type is text/html and modify the response body thats has already been written to the HTTP::Response before it is sent to the client by injecting an HTML element into the existing html response body.

I've noticed that HTTP::Server::Response is write-only, but things like Gzip::Writer are able to modify the body.

How can I modify the HTTP::Server::Response html body before it is sent to the client?

Upvotes: 1

Views: 289

Answers (1)

Jonne Haß
Jonne Haß

Reputation: 4857

It's written in Crystal, so let's just take a look at the source on how others do this.

Taking the CompressHandler as an example, the basic idea is to replace the response's IO with something that allows the desired control:

context.response.output = Gzip::Writer.new(context.response.output, sync_close: true)
# ...
call_next(context)

So how can we make use of that to modify the response that's being written?

A naive (and slow) example would be to just keep hold of the original output and provide a IO::Memory instead:

client = context.response.output
io = IO::Memory.new
context.response.output = io

call_next(context)

body = io.to_s
new_body = inject_html(body)
client.print new_body

Of course that would only work when this handler comes before any handler that turns the response into non-plaintext (like the above CompressHandler).

A smarter solution would provide a custom IO implementation that just wraps the original IO, watching what's written to it and inject whatever it wants to inject at the right point. Examples of such wrapping IOs can be found at IO::Delimited, IO::Sized and IO::MultieWriter among others, the pattern is really common to prevent unnecessary allocations.

Upvotes: 1

Related Questions