Class: Rack::HttpStreamingResponse

Inherits:
Object
  • Object
show all
Defined in:
lib/rack/http_streaming_response.rb

Overview

A lazy Rack body that streams a backend Net::HTTP response without buffering it.

The request runs inside a Fiber, using only the public block form of Net::HTTP#request: the Fiber pauses (Fiber.yield res) the moment the status and headers are available, and #each resumes it to pull body chunks as the server consumes them. This replaced the 2010-era monkey-patch of private net/http internals (net_http_hacked.rb, deleted in 1.0) and inherits upstream's handling of 1xx interim responses, keep-alive negotiation, and transport errors.

A Fiber can only be resumed from the thread that created it. The request Fiber is created lazily on first use (#code/#headers/#each), so the normal Rack flow — one thread calls the app and then iterates the body — is fine. If #close is called from a different thread (some servers do this on client abort), the Fiber unwind is skipped and the connection is hard-closed instead, which releases the socket either way.

Defined Under Namespace

Classes: ResponseTooLarge, StreamAborted

Constant Summary collapse

STATUSES_WITH_NO_ENTITY_BODY =
{
  204 => true,
  205 => true,
  304 => true
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(request, host, port = nil, &configure) ⇒ HttpStreamingResponse

An optional block receives the Net::HTTP instance for configuration before it connects — this is the single source of truth used by Rack::Proxy (see Rack::Proxy#configure_backend_connection). When no block is given, the public accessors above are applied instead (backward-compatible path for direct users of this class).



50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/rack/http_streaming_response.rb', line 50

def initialize(request, host, port = nil, &configure)
  @request, @host, @port, @configure = request, host, port, configure

  # Forward the backend body verbatim. Without this, Net::HTTP inflates
  # gzip/deflate bodies for requests that opted in (the default for e.g.
  # Net::HTTP::Get), leaving the already-forwarded Content-Length and
  # Content-Encoding describing bytes the client never receives. The old
  # patched read path never decoded; keep that contract.
  if request.instance_variable_defined?(:@decode_content)
    request.instance_variable_set(:@decode_content, false)
  end
end

Instance Attribute Details

#certObject

Returns the value of attribute cert.



42
43
44
# File 'lib/rack/http_streaming_response.rb', line 42

def cert
  @cert
end

#keyObject

Returns the value of attribute key.



42
43
44
# File 'lib/rack/http_streaming_response.rb', line 42

def key
  @key
end

#loggerObject

Returns the value of attribute logger.



42
43
44
# File 'lib/rack/http_streaming_response.rb', line 42

def logger
  @logger
end

#max_response_lengthObject

Returns the value of attribute max_response_length.



43
44
45
# File 'lib/rack/http_streaming_response.rb', line 43

def max_response_length
  @max_response_length
end

#read_timeoutObject

Returns the value of attribute read_timeout.



42
43
44
# File 'lib/rack/http_streaming_response.rb', line 42

def read_timeout
  @read_timeout
end

#ssl_versionObject

Returns the value of attribute ssl_version.



42
43
44
# File 'lib/rack/http_streaming_response.rb', line 42

def ssl_version
  @ssl_version
end

#use_sslObject

Returns the value of attribute use_ssl.



42
43
44
# File 'lib/rack/http_streaming_response.rb', line 42

def use_ssl
  @use_ssl
end

#verify_modeObject

Returns the value of attribute verify_mode.



42
43
44
# File 'lib/rack/http_streaming_response.rb', line 42

def verify_mode
  @verify_mode
end

Instance Method Details

#bodyObject



63
64
65
# File 'lib/rack/http_streaming_response.rb', line 63

def body
  self
end

#closeObject

Rack calls #close on the response body when it is done with it, including when it bails out early (HEAD, 304, a client disconnect). Without this, a body that is never iterated leaks the backend TCP/TLS connection until GC.



116
117
118
# File 'lib/rack/http_streaming_response.rb', line 116

def close
  close_connection
end

#codeObject Also known as: status



67
68
69
70
71
# File 'lib/rack/http_streaming_response.rb', line 67

def code
  response.code.to_i.tap do |response_code|
    STATUSES_WITH_NO_ENTITY_BODY[response_code] && close_connection
  end
end

#each(&block) ⇒ Object

Can be called only once!



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/rack/http_streaming_response.rb', line 80

def each(&block)
  return if connection_closed

  response # make sure the request has started and the headers are in

  bytes = 0
  while @fiber.alive?
    chunk = @fiber.resume
    # The last resume returns the Fiber's terminal value, not a body chunk.
    next unless chunk.is_a?(String)

    if max_response_length
      bytes += chunk.bytesize
      if bytes > max_response_length
        raise ResponseTooLarge, "backend response exceeded max_response_length=#{max_response_length}"
      end
    end
    block.call(chunk)
  end
rescue => e
  # The status/headers are already on the wire, so we can't turn a mid-stream
  # backend failure into a 502. Log it and re-raise so the server aborts the
  # transfer (the client sees a truncated response, not a false "complete").
  logger << "rack-proxy: streaming backend read failed: #{e.class}: #{e.message}\n" if logger.respond_to?(:<<)
  raise
ensure
  close_connection
end

#headersObject



75
76
77
# File 'lib/rack/http_streaming_response.rb', line 75

def headers
  Rack::Proxy.build_header_hash(response.to_hash)
end

#to_sObject



109
110
111
# File 'lib/rack/http_streaming_response.rb', line 109

def to_s
  @to_s ||= StringIO.new.tap { |io| each { |line| io << line } }.string
end