Skip to content

Input#rewind returns true when the underlying body is not rewindable #33

@gsar

Description

@gsar

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
end

Output

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
end

The 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"
end

Since 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
end

Impact

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-rack 0.21.0
  • protocol-http 0.58.1
  • falcon 0.54.0
  • rack 3.2.4

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions