-
-
Notifications
You must be signed in to change notification settings - Fork 4
Description
Protocol::Rack::Input#rewind returns true even when the underlying body cannot actually be rewound. After calling rewind, subsequent reads return empty data. This silently breaks any middleware or gem that relies on the read → rewind → re-read pattern (e.g. rails-reverse-proxy).
Reproduction
Minimal Rack app served by Falcon that reads the body, rewinds, and reads again:
# test_rewind.rb
require "bundler/setup"
require "async"
require "async/http/server"
require "async/http/client"
require "async/http/endpoint"
require "protocol/rack"
app = lambda do |env|
input = env["rack.input"]
first_read = input.read
rewind_result = input.rewind
second_read = input.read
body = [
"input_class: #{input.class}",
"first_read_size: #{first_read&.bytesize || 0}",
"rewind_result: #{rewind_result.inspect}",
"second_read_size: #{second_read&.bytesize || 0}",
"body_preserved: #{first_read == second_read && first_read.to_s.bytesize > 0}",
].join("\n") + "\n"
[200, {"content-type" => "text/plain"}, [body]]
end
Async do |task|
endpoint = Async::HTTP::Endpoint.parse("http://localhost:9292")
server = Async::HTTP::Server.new(Protocol::Rack::Adapter.new(app), endpoint)
server_task = task.async { server.run }
sleep 0.1
client = Async::HTTP::Client.new(endpoint)
body = Protocol::HTTP::Body::Buffered.new(['{"key":"value"}'])
response = client.call(Protocol::HTTP::Request["POST", "/test", {"content-type" => "application/json"}, body])
puts response.read
client.close
server_task.stop
endOutput
input_class: Protocol::Rack::Input
first_read_size: 15
rewind_result: true
second_read_size: 0
body_preserved: false
rewind returns true, but the second read is empty.
Expected behavior
rewind should return false when the body cannot be rewound, so callers can detect the failure and handle it (e.g. by bufferin
g the input themselves). The internal state (@finished, @closed) should not be reset when the rewind didn't actually work.
Root cause
In lib/protocol/rack/input.rb lines 62-74:
def rewind
if @body and @body.respond_to?(:rewind)
@body.rewind # ← returns false for non-rewindable bodies
@finished = false # ← resets state anyway
@closed = false
return true # ← claims success
end
return false
endThe method checks respond_to?(:rewind) but not rewindable?. The base class Protocol::HTTP::Body::Readable defines both:
def rewindable?
false # "I am NOT rewindable"
end
def rewind
false # "rewind failed"
endSince Readable responds to :rewind, Input#rewind calls it, ignores the false return value, resets internal state, and returns true.
Suggested fix
Check rewindable? before attempting to rewind:
def rewind
if @body and @body.respond_to?(:rewind)
if @body.respond_to?(:rewindable?) and !@body.rewindable?
return false
end
@body.rewind
@finished = false
@closed = false
return true
end
return false
endImpact
This affects any library that uses the read → rewind → re-read pattern on rack.input. In our case, rails-reverse-proxy calls source_request.body.rewind before forwarding the body to the proxy target (client.rb:75). Because rewind claims success, the gem proceeds to forward an empty body. The target server then hangs waiting for the promised Content-Length bytes that never arrive, resulting in timeouts and 503 errors. Also see axsuul/rails-reverse-proxy#82
Workaround
We're using Rack::RewindableInput::Middleware (or wrapping rack.input manually with Rack::RewindableInput) to ensure the input is truly rewindable before it reaches the proxy code.
Versions
protocol-rack0.21.0protocol-http0.58.1falcon0.54.0rack3.2.4