Vlad Faust
Vlad Faust

Reputation: 550

Read request body without deleting it

So I have a MyHandler which has to know what's inside the request body:

class MyHandler
  include HTTP::Handler

  def call(context)
    p "MyHandler got body: " + context.request.body.not_nil!.gets_to_end
    call_next(context)
  end
end

server = HTTP::Server.new(42, [MyHandler.new]) do |context|
  p "Server got body: " + context.request.body.not_nil!.gets_to_end
end

As expected, after MyHandler has read, server receives a empty body. How can copy the body without modifying original context?

Upvotes: 3

Views: 689

Answers (1)

Stephie
Stephie

Reputation: 3175

Crystal supports streaming request bodies, which means that once you stream in the request, the IO is EOF and the second handler can't read any data.

A simple way to solve this would be to retrieve the entire content using body_string = context.request.body.try(&.gets_to_end), then set the request body to the returned string using context.request.body = body_string. This buffers the entire body to memory then sets the body to the buffer stored in memory. The downside to this approach is that an attacker can send an infinitely sized request body and eat all the memory on your server, causing a DOS attack. Another disadvantage is that if you're working with binary data, you would then need to convert the string into a slice using #to_slice to work with it.

One way to solve the DOS attack problem - if you have a maximum body size in mind - is to fail the request if the body is too large:

if body = context.request.body
  body_io = IO::Memory.new
  bytes_read = IO.copy(body, body_io, limit: 1_048_576) # 1GiB limit
  body_io.rewind
  if bytes_read == 1_048_576
    # Fail request
  end

  # use body_io

  body_io.rewind # Reset body_io to start
  context.request.body = body_io
end

If you need to accept an infinitely sized body, and not buffer it to memory, you should create a custom IO implementation which wraps the existing body IO and runs the required transform inside IO#read(Bytes). This method is quite complex, and the previous method covers almost all situations, so I won't provide a code sample for this option.

Upvotes: 9

Related Questions