Protocol::HTTP1SourceProtocolHTTP1BodyChunked

class Chunked

Represents a chunked body, which is a series of chunks, each with a length prefix.

See https://tools.ietf.org/html/rfc7230#section-4.1 for more details on the chunked transfer encoding.

Definitions

def initialize(connection, headers)

Initialize the chunked body.

Signature

parameter connection Protocol::HTTP1::Connection

the connection to read the body from.

parameter headers Protocol::HTTP::Headers

the headers to read the trailer into, if any.

Implementation

def initialize(connection, headers)
	@connection = connection
	@finished = false
	
	@headers = headers
	
	@length = 0
	@count = 0
end

attr :count

Signature

attribute Integer

the number of chunks read so far.

def length

Signature

attribute Integer

the length of the body if known.

Implementation

def length
	# We only know the length once we've read the final chunk:
	if @finished
		@length
	end
end

def empty?

Signature

returns Boolean

true if the body is empty, in other words Protocol::HTTP1::Body::Chunked#read will return nil.

Implementation

def empty?
	@connection.nil?
end

def close(error = nil)

Close the connection and mark the body as finished.

Signature

parameter error Exception | Nil

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

Implementation

def close(error = nil)
	if connection = @connection
		@connection = nil
		
		unless @finished
			connection.close_read
		end
	end
	
	super
end

def read

Read a chunk of data.

Follows the procedure outlined in https://tools.ietf.org/html/rfc7230#section-4.1.3

Signature

returns String | Nil

the next chunk of data, or nil if the body is finished.

raises EOFError

if the connection is closed before the expected length is read.

Implementation

def read
	if !@finished
		if @connection
			length, _extensions = @connection.read_line.split(";", 2)
			
			unless length =~ VALID_CHUNK_LENGTH
				raise BadRequest, "Invalid chunk length: #{length.inspect}"
			end
			
			# It is possible this line contains chunk extension, so we use `to_i` to only consider the initial integral part:
			length = Integer(length, 16)
			
			if length == 0
				read_trailer
				
				# The final chunk has been read and the connection is now closed:
				@connection.receive_end_stream!
				@connection = nil
				@finished = true
				
				return nil
			end
			
			# Read trailing CRLF:
			chunk = @connection.read(length + 2)
			
			if chunk.bytesize == length + 2
				# ...and chomp it off:
				chunk.chomp!(CRLF)
				
				@length += length
				@count += 1
				
				return chunk
			else
				# The connection has been closed before we have read the requested length:
				@connection.close_read
				@connection = nil
			end
		end
		
		# If the connection has been closed before we have read the final chunk, raise an error:
		raise EOFError, "connection closed before expected length was read!"
	end
end

def inspect

Signature

returns String

a human-readable representation of the body.

Implementation

def inspect
	"\#<#{self.class} #{@length} bytes read in #{@count} chunks>"
end

def read_trailer

Read the trailer from the connection, and add any headers to the trailer.

Implementation

def read_trailer
	while line = @connection.read_line?
		# Empty line indicates end of trailer:
		break if line.empty?
		
		if match = line.match(HEADER)
			@headers.add(match[1], match[2])
		else
			raise BadHeader, "Could not parse header: #{line.inspect}"
		end
	end
end