Protocol::HTTP1SourceProtocolHTTP1Connection

class Connection

Represents a single HTTP/1.x connection, which may be used to send and receive multiple requests and responses.

Definitions

HTTP10 = "HTTP/1.0"

The HTTP/1.0 version string.

HTTP11 = "HTTP/1.1"

The HTTP/1.1 version string.

def initialize(stream, persistent: true, state: :idle, maximum_line_length: DEFAULT_MAXIMUM_LINE_LENGTH)

Initialize the connection with the given stream.

Signature

parameter stream IO

the stream to read and write data from.

parameter persistent Boolean

whether the connection is persistent.

parameter state Symbol

the initial state of the connection, typically idle.

Implementation

def initialize(stream, persistent: true, state: :idle, maximum_line_length: DEFAULT_MAXIMUM_LINE_LENGTH)
	@stream = stream
	
	@persistent = persistent
	@state = state
	
	@count = 0
	
	@maximum_line_length = maximum_line_length
end

attr :stream

The underlying IO stream.

attr_accessor :persistent

This determines what connection headers are sent in the response and whether the connection can be reused after the response is sent. This setting is automatically managed according to the nature of the request and response. Changing to false is safe. Changing to true from outside this class should generally be avoided and, depending on the response semantics, may be reset to false anyway.

Signature

attribute Boolean

true if the connection is persistent.

attr_accessor :state

The current state of the connection.

                         ┌────────┐
                         │        │
┌───────────────────────►│  idle  │
│                        │        │
│                        └───┬────┘
│                            │
│                            │ send request /
│                            │ receive request
│                            │
│                            ▼
│                        ┌────────┐
│                recv ES │        │ send ES
│           ┌────────────┤  open  ├────────────┐
│           │            │        │            │
│           ▼            └───┬────┘            ▼
│      ┌──────────┐          │           ┌──────────┐
│      │   half   │          │           │   half   │
│      │  closed  │          │ send R /  │  closed  │
│      │ (remote) │          │ recv R    │ (local)  │
│      └────┬─────┘          │           └─────┬────┘
│           │                │                 │
│           │ send ES /      │       recv ES / │
│           │ close          ▼           close │
│           │            ┌────────┐            │
│           └───────────►│        │◄───────────┘
│                        │ closed │
└────────────────────────┤        │
        persistent       └────────┘
  • ES: the body was fully received or sent (end of stream).
  • R: the connection was closed unexpectedly (reset).

State transition methods use a trailing "!".

def idle?

Implementation

def idle?
	@state == :idle
end

def open?

Implementation

def open?
	@state == :open
end

def half_closed_local?

Implementation

def half_closed_local?
	@state == :half_closed_local
end

def half_closed_remote?

Implementation

def half_closed_remote?
	@state == :half_closed_remote
end

def closed?

Implementation

def closed?
	@state == :closed
end

attr :count

Signature

attribute Integer

the number of requests and responses processed by this connection.

def persistent?(version, method, headers)

Indicates whether the connection is persistent given the version, method, and headers.

Signature

parameter version String

the HTTP version.

parameter method String

the HTTP method.

parameter headers Hash

the HTTP headers.

Implementation

def persistent?(version, method, headers)
	if method == HTTP::Methods::CONNECT
		return false
	end
	
	if version == HTTP10
		if connection = headers[CONNECTION]
			return connection.keep_alive?
		else
			return false
		end
	else # HTTP/1.1+
		if connection = headers[CONNECTION]
			return !connection.close?
		else
			return true
		end
	end
end

def write_connection_header(version)

Write the appropriate header for connection persistence.

Implementation

def write_connection_header(version)
	if version == HTTP10
		@stream.write("connection: keep-alive\r\n") if @persistent
	else
		@stream.write("connection: close\r\n") unless @persistent
	end
end

def write_upgrade_header(upgrade)

Write the appropriate header for connection upgrade.

Implementation

def write_upgrade_header(upgrade)
	@stream.write("connection: upgrade\r\nupgrade: #{upgrade}\r\n")
end

def hijacked?

Indicates whether the connection has been hijacked meaning its IO has been handed over and is not usable anymore.

Signature

returns Boolean

hijack status

Implementation

def hijacked?
	@stream.nil?
end

def hijack!

Hijack the connection - that is, take over the underlying IO and close the connection.

Signature

returns IO | Nil

the underlying non-blocking IO.

Implementation

def hijack!
	@persistent = false
	
	if stream = @stream
		@stream = nil
		stream.flush
		
		@state = :hijacked
		self.closed
		
		return stream
	end
end

def close_read

Close the read end of the connection and transition to the half-closed remote state (or closed if already in the half-closed local state).

Implementation

def close_read
	@persistent = false
	@stream&.close_read
	self.receive_end_stream!
end

def close(error = nil)

Close the connection and underlying stream and transition to the closed state.

Implementation

def close(error = nil)
	@persistent = false
	
	if stream = @stream
		@stream = nil
		stream.close
	end
	
	unless closed?
		@state = :closed
		self.closed(error)
	end
end

def open!

Force a transition to the open state.

Signature

raises ProtocolError

if the connection is not in the idle state.

Implementation

def open!
	unless @state == :idle
		raise ProtocolError, "Cannot open connection in state: #{@state}!"
	end
	
	@state = :open
	
	return self
end

def write_request(authority, method, target, version, headers)

Write a request to the connection. It is expected you will write the body after this method.

Transitions to the open state.

Signature

parameter authority String

the authority of the request.

parameter method String

the HTTP method.

parameter target String

the request target.

parameter version String

the HTTP version.

parameter headers Hash

the HTTP headers.

raises ProtocolError

if the connection is not in the idle state.

Implementation

def write_request(authority, method, target, version, headers)
	open!
	
	@stream.write("#{method} #{target} #{version}\r\n")
	@stream.write("host: #{authority}\r\n") if authority
	
	write_headers(headers)
end

def write_response(version, status, headers, reason = Reason::DESCRIPTIONS[status])

Write a response to the connection. It is expected you will write the body after this method.

Signature

parameter version String

the HTTP version.

parameter status Integer

the HTTP status code.

parameter headers Hash

the HTTP headers.

parameter reason String

the reason phrase, defaults to the standard reason phrase for the status code.

Implementation

def write_response(version, status, headers, reason = Reason::DESCRIPTIONS[status])
	unless @state == :open or @state == :half_closed_remote
		raise ProtocolError, "Cannot write response in state: #{@state}!"
	end
	
	# Safari WebSockets break if no reason is given:
	@stream.write("#{version} #{status} #{reason}\r\n")
	
	write_headers(headers)
end

def write_interim_response(version, status, headers, reason = Reason::DESCRIPTIONS[status])

Write an interim response to the connection. It is expected you will eventually write the final response after this method.

Signature

parameter version String

the HTTP version.

parameter status Integer

the HTTP status code.

parameter headers Hash

the HTTP headers.

parameter reason String

the reason phrase, defaults to the standard reason phrase for the status code.

raises ProtocolError

if the connection is not in the open or half-closed remote state.

Implementation

def write_interim_response(version, status, headers, reason = Reason::DESCRIPTIONS[status])
	unless @state == :open or @state == :half_closed_remote
		raise ProtocolError, "Cannot write interim response in state: #{@state}!"
	end
	
	@stream.write("#{version} #{status} #{reason}\r\n")
	
	write_headers(headers)
	
	@stream.write("\r\n")
	@stream.flush
end

def write_headers(headers)

Write headers to the connection.

Signature

parameter headers Hash

the headers to write.

raises BadHeader

if the header name or value is invalid.

Implementation

def write_headers(headers)
	headers.each do |name, value|
		# Convert it to a string:
		name = name.to_s
		value = value.to_s
		
		# Validate it:
		unless name.match?(VALID_FIELD_NAME)
			raise BadHeader, "Invalid header name: #{name.inspect}"
		end
		
		unless value.match?(VALID_FIELD_VALUE)
			raise BadHeader, "Invalid header value for #{name}: #{value.inspect}"
		end
		
		# Write it:
		@stream.write("#{name}: #{value}\r\n")
	end
end

def readpartial(length)

Read some data from the connection.

Signature

parameter length Integer

the maximum number of bytes to read.

Implementation

def readpartial(length)
	@stream.readpartial(length)
end

def read(length)

Read some data from the connection.

Signature

parameter length Integer

the number of bytes to read.

Implementation

def read(length)
	@stream.read(length)
end

def read_line?

Read a line from the connection.

Signature

returns String | Nil

the line read, or nil if the connection is closed.

raises EOFError

if the connection is closed.

raises LineLengthError

if the line is too long.

Implementation

def read_line?
	if line = @stream.gets(CRLF, @maximum_line_length)
		unless line.chomp!(CRLF)
			# This basically means that the request line, response line, header, or chunked length line is too long.
			raise LineLengthError, "Line too long!"
		end
	end
	
	return line
end

def read_line

Read a line from the connection.

Signature

raises EOFError

if a line could not be read.

raises LineLengthError

if the line is too long.

Implementation

def read_line
	read_line? or raise EOFError
end

def read_request_line

Read a request line from the connection.

Signature

returns Tuple(String, String, String) | Nil

the method, path, and version of the request, or nil if the connection is closed.

Implementation

def read_request_line
	return unless line = read_line?
	
	if match = line.match(REQUEST_LINE)
		_, method, path, version = *match
	else
		raise InvalidRequest, line.inspect
	end
	
	return method, path, version
end

def read_request

Read a request from the connection, including the request line and request headers, and prepares to read the request body.

Transitions to the open state.

Signature

yields {|host, method, path, version, headers, body| ...}

if a block is given.

returns Tuple(String, String, String, String, HTTP::Headers, Protocol::HTTP1::Body) | Nil

the host, method, path, version, headers, and body of the request, or nil if the connection is closed.

raises ProtocolError

if the connection is not in the idle state.

Implementation

def read_request
	open!
	
	method, path, version = read_request_line
	return unless method
	
	headers = read_headers
	
	# If we are not persistent, we can't become persistent even if the request might allow it:
	if @persistent
		# In other words, `@persistent` can only transition from true to false.
		@persistent = persistent?(version, method, headers)
	end
	
	body = read_request_body(method, headers)
	
	unless body
		self.receive_end_stream!
	end
	
	@count += 1
	
	if block_given?
		yield headers.delete(HOST), method, path, version, headers, body
	else
		return headers.delete(HOST), method, path, version, headers, body
	end
end

def read_response_line

Read a response line from the connection.

Signature

returns Tuple(String, Integer, String)

the version, status, and reason of the response.

raises EOFError

if the connection is closed.

Implementation

def read_response_line
	version, status, reason = read_line.split(/\s+/, 3)
	
	status = Integer(status)
	
	return version, status, reason
end

def interim_status?(status)

Indicates whether the status code is an interim status code.

Signature

parameter status Integer

the status code.

returns Boolean

whether the status code is an interim status code.

Implementation

def interim_status?(status)
	status != 101 and status >= 100 and status < 200
end

def read_response(method)

Read a response from the connection.

Signature

parameter method String

the HTTP method.

yields {|version, status, reason, headers, body| ...}

if a block is given.

returns Tuple(String, Integer, String, HTTP::Headers, Protocol::HTTP1::Body)

the version, status, reason, headers, and body of the response.

raises ProtocolError

if the connection is not in the open or half-closed local state.

raises EOFError

if the connection is closed.

Implementation

def read_response(method)
	unless @state == :open or @state == :half_closed_local
		raise ProtocolError, "Cannot read response in state: #{@state}!"
	end
	
	version, status, reason = read_response_line
	
	headers = read_headers
	
	if @persistent
		@persistent = persistent?(version, method, headers)
	end
	
	unless interim_status?(status)
		body = read_response_body(method, status, headers)
		
		unless body
			self.receive_end_stream!
		end
		
		@count += 1
	end
	
	if block_given?
		yield version, status, reason, headers, body
	else
		return version, status, reason, headers, body
	end
end

def read_headers

Read headers from the connection until an empty line is encountered.

Signature

returns HTTP::Headers

the headers read.

raises EOFError

if the connection is closed.

raises BadHeader

if a header could not be parsed.

Implementation

def read_headers
	fields = []
	
	while line = read_line
		# Empty line indicates end of headers:
		break if line.empty?
		
		if match = line.match(HEADER)
			fields << [match[1], match[2] || ""]
		else
			raise BadHeader, "Could not parse header: #{line.inspect}"
		end
	end
	
	return HTTP::Headers.new(fields)
end

def send_end_stream!

Transition to the half-closed local state, in other words, the connection is closed for writing.

If the connection is already in the half-closed remote state, it will transition to the closed state.

Signature

raises ProtocolError

if the connection is not in the open state.

Implementation

def send_end_stream!
	if @state == :open
		@state = :half_closed_local
	elsif @state == :half_closed_remote
		self.close!
	else
		raise ProtocolError, "Cannot send end stream in state: #{@state}!"
	end
end

def write_upgrade_body(protocol, body = nil)

Write an upgrade body to the connection.

This writes the upgrade header and the body to the connection. If the body is nil, you should coordinate writing to the stream.

The connection will not be persistent after this method is called.

Signature

parameter protocol String

the protocol to upgrade to.

parameter body Object | Nil

the body to write.

returns IO

the underlying IO stream.

Implementation

def write_upgrade_body(protocol, body = nil)
	# Once we upgrade the connection, it can no longer handle other requests:
	@persistent = false
	
	write_upgrade_header(protocol)
	
	@stream.write("\r\n")
	@stream.flush # Don't remove me!
	
	if body
		body.each do |chunk|
			@stream.write(chunk)
			@stream.flush
		end
		
		@stream.close_write
	end
	
	return @stream
ensure
	self.send_end_stream!
end

def write_tunnel_body(version, body = nil)

Write a tunnel body to the connection.

This writes the connection header and the body to the connection. If the body is nil, you should coordinate writing to the stream.

The connection will not be persistent after this method is called.

Signature

parameter version String

the HTTP version.

parameter body Object | Nil

the body to write.

returns IO

the underlying IO stream.

Implementation

def write_tunnel_body(version, body = nil)
	@persistent = false
	
	write_connection_header(version)
	
	@stream.write("\r\n")
	@stream.flush # Don't remove me!
	
	if body
		body.each do |chunk|
			@stream.write(chunk)
			@stream.flush
		end
		
		@stream.close_write
	end
	
	return @stream
ensure
	self.send_end_stream!
end

def write_empty_body(body = nil)

Write an empty body to the connection.

If given, the body will be closed.

Signature

parameter body Object | Nil

the body to write.

Implementation

def write_empty_body(body = nil)
	@stream.write("content-length: 0\r\n\r\n")
	@stream.flush
	
	body&.close
ensure
	self.send_end_stream!
end

def write_fixed_length_body(body, length, head)

Write a fixed length body to the connection.

If the request was a HEAD request, the body will be closed, and no data will be written.

Signature

parameter body Object

the body to write.

parameter length Integer

the length of the body.

parameter head Boolean

whether the request was a HEAD request.

raises ContentLengthError

if the body length does not match the content length specified.

Implementation

def write_fixed_length_body(body, length, head)
	@stream.write("content-length: #{length}\r\n\r\n")
	
	if head
		@stream.flush
		
		body.close
		
		return
	end
	
	@stream.flush unless body.ready?
	
	chunk_length = 0
	body.each do |chunk|
		chunk_length += chunk.bytesize
		
		if chunk_length > length
			raise ContentLengthError, "Trying to write #{chunk_length} bytes, but content length was #{length} bytes!"
		end
		
		@stream.write(chunk)
		@stream.flush unless body.ready?
	end
	
	@stream.flush
	
	if chunk_length != length
		raise ContentLengthError, "Wrote #{chunk_length} bytes, but content length was #{length} bytes!"
	end
ensure
	self.send_end_stream!
end

def write_chunked_body(body, head, trailer = nil)

Write a chunked body to the connection.

If the request was a HEAD request, the body will be closed, and no data will be written.

If trailers are given, they will be written after the body.

Signature

parameter body Object

the body to write.

parameter head Boolean

whether the request was a HEAD request.

parameter trailer Hash | Nil

the trailers to write.

Implementation

def write_chunked_body(body, head, trailer = nil)
	@stream.write("transfer-encoding: chunked\r\n\r\n")
	
	if head
		@stream.flush
		
		body.close
		
		return
	end
	
	@stream.flush unless body.ready?
	
	body.each do |chunk|
		next if chunk.size == 0
		
		@stream.write("#{chunk.bytesize.to_s(16).upcase}\r\n")
		@stream.write(chunk)
		@stream.write(CRLF)
		
		@stream.flush unless body.ready?
	end
	
	if trailer&.any?
		@stream.write("0\r\n")
		write_headers(trailer)
		@stream.write("\r\n")
	else
		@stream.write("0\r\n\r\n")
	end
	
	@stream.flush
ensure
	self.send_end_stream!
end

def write_body_and_close(body, head)

Write the body to the connection and close the connection.

Signature

parameter body Object

the body to write.

parameter head Boolean

whether the request was a HEAD request.

Implementation

def write_body_and_close(body, head)
	# We can't be persistent because we don't know the data length:
	@persistent = false
	
	@stream.write("\r\n")
	@stream.flush unless body.ready?
	
	if head
		body.close
	else
		body.each do |chunk|
			@stream.write(chunk)
			
			@stream.flush unless body.ready?
		end
	end
	
	@stream.flush
	@stream.close_write
ensure
	self.send_end_stream!
end

def closed(error = nil)

The connection (stream) was closed. It may now be in the idle state.

Sub-classes may override this method to perform additional cleanup.

Signature

parameter error Exception | Nil

the error that caused the connection to be closed, if any.

Implementation

def closed(error = nil)
end

def close!(error = nil)

Transition to the closed state.

If no error occurred, and the connection is persistent, this will immediately transition to the idle state.

Signature

parameter error Exxception

the error that caused the connection to close.

Implementation

def close!(error = nil)
	if @persistent and !error
		# If there was no error, and the connection is persistent, we can reuse it:
		@state = :idle
	else
		@state = :closed
	end
	
	self.closed(error)
end

def write_body(version, body, head = false, trailer = nil)

Write a body to the connection.

The behavior of this method is determined by the HTTP version, the body, and the request method. We try to choose the best approach possible, given the constraints, connection persistence, whether the length is known, etc.

Signature

parameter version String

the HTTP version.

parameter body Object

the body to write.

parameter head Boolean

whether the request was a HEAD request.

parameter trailer Hash | Nil

the trailers to write.

Implementation

def write_body(version, body, head = false, trailer = nil)
	# HTTP/1.0 cannot in any case handle trailers.
	if version == HTTP10 # or te: trailers was not present (strictly speaking not required.)
		trailer = nil
	end
	
	# While writing the body, we don't know if trailers will be added. We must choose a different body format depending on whether there is the chance of trailers, even if trailer.any? is currently false.
	#
	# Below you notice `and trailer.nil?`. I tried this but content-length is more important than trailers.
	
	if body.nil?
		write_connection_header(version)
		write_empty_body(body)
	elsif length = body.length # and trailer.nil?
		write_connection_header(version)
		write_fixed_length_body(body, length, head)
	elsif body.empty?
		# Even thought this code is the same as the first clause `body.nil?`, HEAD responses have an empty body but still carry a content length. `write_fixed_length_body` takes care of this appropriately.
		write_connection_header(version)
		write_empty_body(body)
	elsif version == HTTP11
		write_connection_header(version)
		# We specifically ensure that non-persistent connections do not use chunked response, so that hijacking works as expected.
		write_chunked_body(body, head, trailer)
	else
		@persistent = false
		write_connection_header(version)
		write_body_and_close(body, head)
	end
end

def receive_end_stream!

Indicate that the end of the stream (body) has been received.

This will transition to the half-closed remote state if the connection is open, or the closed state if the connection is half-closed local.

Signature

raises ProtocolError

if the connection is not in the open or half-closed remote state.

Implementation

def receive_end_stream!
	if @state == :open
		@state = :half_closed_remote
	elsif @state == :half_closed_local
		self.close!
	else
		raise ProtocolError, "Cannot receive end stream in state: #{@state}!"
	end
end

def read_chunked_body(headers)

Read the body, assuming it is using the chunked transfer encoding.

Signature

returns Protocol::HTTP1::Body::Chunked

the body.

Implementation

def read_chunked_body(headers)
	Body::Chunked.new(self, headers)
end

def read_fixed_body(length)

Read the body, assuming it has a fixed length.

Signature

returns Protocol::HTTP1::Body::Fixed

the body.

Implementation

def read_fixed_body(length)
	Body::Fixed.new(self, length)
end

def read_remainder_body

Read the body, assuming that we read until the connection is closed.

Signature

returns Protocol::HTTP1::Body::Remainder

the body.

Implementation

def read_remainder_body
	@persistent = false
	Body::Remainder.new(self)
end

def read_head_body(length)

Read the body, assuming that we are not receiving any actual data, but just the length.

Signature

returns Protocol::HTTP::Body::Head

the body.

Implementation

def read_head_body(length)
	# We are not receiving any body:
	self.receive_end_stream!
	
	Protocol::HTTP::Body::Head.new(length)
end

def read_tunnel_body

Read the body, assuming it is a tunnel.

Invokes Protocol::HTTP1::Connection#read_remainder_body.

Signature

returns Protocol::HTTP::Body::Remainder

the body.

Implementation

def read_tunnel_body
	read_remainder_body
end

def read_upgrade_body

Read the body, assuming it is an upgrade.

Invokes Protocol::HTTP1::Connection#read_remainder_body.

Signature

returns Protocol::HTTP::Body::Remainder

the body.

Implementation

def read_upgrade_body
	# When you have an incoming upgrade request body, we must be extremely careful not to start reading it until the upgrade has been confirmed, otherwise if the upgrade was rejected and we started forwarding the incoming request body, it would desynchronize the connection (potential security issue).
	# We mitigate this issue by setting @persistent to false, which will prevent the connection from being reused, even if the upgrade fails (potential performance issue).
	read_remainder_body
end

HEAD = "HEAD"

The HTTP HEAD method.

CONNECT = "CONNECT"

The HTTP CONNECT method.

VALID_CONTENT_LENGTH = /\A\d+\z/

The pattern for valid content length values.

def extract_content_length(headers)

Extract the content length from the headers, if possible.

Signature

parameter headers Hash

the headers.

yields {|length| ...}

if a content length is found.

parameter length Integer

the content length.

raises BadRequest

if the content length is invalid.

Implementation

def extract_content_length(headers)
	if content_length = headers.delete(CONTENT_LENGTH)
		if content_length =~ VALID_CONTENT_LENGTH
			yield Integer(content_length, 10)
		else
			raise BadRequest, "Invalid content length: #{content_length.inspect}"
		end
	end
end

def read_response_body(method, status, headers)

Read the body of the response.

Signature

parameter method String

the HTTP method.

parameter status Integer

the HTTP status code.

parameter headers Hash

the headers of the response.

Implementation

def read_response_body(method, status, headers)
	# RFC 7230 3.3.3
	# 1.  Any response to a HEAD request and any response with a 1xx
	# (Informational), 204 (No Content), or 304 (Not Modified) status
	# code is always terminated by the first empty line after the
	# header fields, regardless of the header fields present in the
	# message, and thus cannot contain a message body.
	if method == HTTP::Methods::HEAD
		extract_content_length(headers) do |length|
			if length > 0
				return read_head_body(length)
			else
				return nil
			end
		end
		
		# There is no body for a HEAD request if there is no content length:
		return nil
	end
	
	if status == 101
		return read_upgrade_body
	end
	
	if (status >= 100 and status < 200) or status == 204 or status == 304
		return nil
	end
	
	# 2.  Any 2xx (Successful) response to a CONNECT request implies that
	# the connection will become a tunnel immediately after the empty
	# line that concludes the header fields.  A client MUST ignore any
	# Content-Length or Transfer-Encoding header fields received in
	# such a message.
	if method == HTTP::Methods::CONNECT and status == 200
		return read_tunnel_body
	end
	
	return read_body(headers, true)
end

def read_request_body(method, headers)

Read the body of the request.

  • The CONNECT method is used to establish a tunnel, so the body is read until the connection is closed.
  • The UPGRADE method is used to upgrade the connection to a different protocol (typically WebSockets), so the body is read until the connection is closed.
  • Otherwise, the body is read according to Protocol::HTTP1::Connection#read_body.

Signature

parameter method String

the HTTP method.

parameter headers Hash

the headers of the request.

Implementation

def read_request_body(method, headers)
	# 2.  Any 2xx (Successful) response to a CONNECT request implies that
	# the connection will become a tunnel immediately after the empty
	# line that concludes the header fields.  A client MUST ignore any
	# Content-Length or Transfer-Encoding header fields received in
	# such a message.
	if method == HTTP::Methods::CONNECT
		return read_tunnel_body
	end
	
	# A successful upgrade response implies that the connection will become a tunnel immediately after the empty line that concludes the header fields.
	if headers[UPGRADE]
		return read_upgrade_body
	end
	
	# 6.  If this is a request message and none of the above are true, then
	# the message body length is zero (no message body is present).
	return read_body(headers)
end

def read_body(headers, remainder = false)

Read the body of the message.

  • The transfer-encoding header is used to determine if the body is chunked.
  • Otherwise, if the content-length is present, the body is read until the content length is reached.
  • Otherwise, if remainder is true, the body is read until the connection is closed.

Signature

parameter headers Hash

the headers of the message.

parameter remainder Boolean

whether to read the remainder of the body.

returns Object

the body.

Implementation

def read_body(headers, remainder = false)
	# 3.  If a Transfer-Encoding header field is present and the chunked
	# transfer coding (Section 4.1) is the final encoding, the message
	# body length is determined by reading and decoding the chunked
	# data until the transfer coding indicates the data is complete.
	if transfer_encoding = headers.delete(TRANSFER_ENCODING)
		# If a message is received with both a Transfer-Encoding and a
		# Content-Length header field, the Transfer-Encoding overrides the
		# Content-Length.  Such a message might indicate an attempt to
		# perform request smuggling (Section 9.5) or response splitting
		# (Section 9.4) and ought to be handled as an error.  A sender MUST
		# remove the received Content-Length field prior to forwarding such
		# a message downstream.
		if headers[CONTENT_LENGTH]
			raise BadRequest, "Message contains both transfer encoding and content length!"
		end
		
		if transfer_encoding.last == CHUNKED
			return read_chunked_body(headers)
		else
			# If a Transfer-Encoding header field is present in a response and
			# the chunked transfer coding is not the final encoding, the
			# message body length is determined by reading the connection until
			# it is closed by the server.  If a Transfer-Encoding header field
			# is present in a request and the chunked transfer coding is not
			# the final encoding, the message body length cannot be determined
			# reliably; the server MUST respond with the 400 (Bad Request)
			# status code and then close the connection.
			return read_remainder_body
		end
	end
	
	# 5.  If a valid Content-Length header field is present without
	# Transfer-Encoding, its decimal value defines the expected message
	# body length in octets.  If the sender closes the connection or
	# the recipient times out before the indicated number of octets are
	# received, the recipient MUST consider the message to be
	# incomplete and close the connection.
	extract_content_length(headers) do |length|
		if length > 0
			return read_fixed_body(length)
		else
			return nil
		end
	end
	
	# http://tools.ietf.org/html/rfc2068#section-19.7.1.1
	if remainder
		# 7.  Otherwise, this is a response message without a declared message
		# body length, so the message body length is determined by the
		# number of octets received prior to the server closing the
		# connection.
		return read_remainder_body
	end
end