The Crystal Programming Language Forum

How to pipe context.response.body = HTTP::Client.get(url).body?

My pseudo-code would look like this:

get "/something/:seed" do |env|
  res = HTTP::Client.get(url)
  env.response.status_code = res.status_code
  env.response.headers.merge! res.headers
  env.response.body = res.body_io?
end

I’m basically implementing a reverse proxy for a single API endpoint. I want to pass the body from HTTP::Client with minimal CPU/memory/GC impact. Basically piping it as-is. Currently I am getting:

 12 | env.response.body = res.body_io?
                   ^---
Error: undefined method 'body=' for HTTP::Server::Response

HTTP::Server::Response trace:

  src/routes/img.cr:12

      env.response.body = res.body_io?

and am not really sure the best way to proceed. Using something external to crystal like nginx is not an option for reasons. Using kemal if that matters. The data being passed through is going to be binary and sometimes XML.

Try like

get "/something/:seed" do |env|
  HTTP::Client.get(url) do |res|
    env.response.status_code = res.status_code
    env.response.headers.merge! res.headers
    IO.copy res.body_io, env.response
  end
end

this didn’t work,

  IO.copy res.body_io, env.response.body

resulted in:

Exception: HTTP::Client::Response#body_io cannot be nil (NilAssertionError)
  from /usr/share/crystal/src/http/client/response.cr:9:3 in 'body_io'
  from src/routes/img.cr:17:11 in '->'
  [...]

But I looked further into the IO class and found that these 2 lines seem both seem to work, I’m assuming that at least the second one actually does the efficient piping.

  env.response.print res.body
  env.response << res.body

Thanks! (I’ll worry about if it’s actually maximally efficient later).

Those two would work, but you’re not streaming at that point. That would load the contents of the request into memory, just to write it back to the response IO.

It seems the body_io was nil in this case. Try this:

get "/something/:seed" do |env|
  HTTP::Client.get(url) do |res|
    env.response.status_code = res.status_code
    env.response.headers.merge! res.headers

    if b_io = res.body_io?
      IO.copy b_io, env.response
    end
  end
end

That ended up sending empty responses.

body_io and body are effectively mutually exclusive in an HTTP::Client::Response.

You get the body_io when you pass a block to get (or whichever HTTP method) and you get body when you don’t.

require "uri"
require "http"

uri = URI.parse("http://localhost:8080/")

HTTP::Client.get(uri).body
HTTP::Client.get(uri) do |response|
  response.body_io.gets_to_end
end

If you call body from inside the block, it’ll be an empty string. If you call body_io without the block you get the exception.

I guess we could always introduce two types for this: StreamingResponse and NonStreamingResponse, both would inherit from a Response (just for reuse purposes). Then there would be no confusion… maybe?