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
BLOCK_SIZE = 1024 * 64
The maximum amount of body data returned by a single read.
def initialize(connection, headers)
Initialize the chunked body.
Signature
-
parameter
connectionProtocol::HTTP1::Connection the connection to read the body from.
-
parameter
headersProtocol::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
@remaining = nil
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#readwill returnnil.
Implementation
def empty?
@connection.nil?
end
def close(error = nil)
Close the connection and mark the body as finished.
Signature
-
parameter
errorException | 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
nilif the body is finished.-
raises
EOFError if the connection is closed before the expected length is read.
Implementation
def read
while !@finished
unless @connection
raise EOFError, "Connection closed before expected length was read!"
end
if @remaining
if @remaining > 0
chunk = @connection.readpartial([@remaining, BLOCK_SIZE].min)
@remaining -= chunk.bytesize
@length += chunk.bytesize
return chunk
end
terminator = @connection.read(CRLF.bytesize)
unless terminator&.bytesize == CRLF.bytesize
raise EOFError, "Connection closed before expected length was read!"
end
unless terminator == CRLF
raise BadRequest, "Invalid chunk terminator: #{terminator.inspect}"
end
@remaining = nil
@count += 1
else
length, _extensions = @connection.read_line.split(";", 2)
unless length =~ VALID_CHUNK_LENGTH
raise BadRequest, "Invalid chunk length: #{length.inspect}"
end
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
@remaining = length
end
end
rescue EOFError
if connection = @connection
@connection = nil
connection.close_read
end
raise
end
def inspect
Signature
-
returns
String a human-readable representation of the body.
Implementation
def inspect
"\#<#{self.class} #{@length} bytes read in #{@count} chunks, #{@finished ? 'finished' : 'reading'}>"
end
def as_json(...)
Signature
-
returns
Hash JSON representation for tracing and debugging.
Implementation
def as_json(...)
super.merge(
count: @count,
finished: @finished,
state: @connection ? "open" : "closed"
)
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], trailer: true)
else
raise BadHeader, "Could not parse header: #{line.inspect}"
end
end
end