class Protocol::HTTP1::Body::Chunked
See tools.ietf.org/html/rfc7230#section-4.1 for more details on the chunked transfer encoding.
Represents a chunked body, which is a series of chunks, each with a length prefix.
def as_json(...)
def as_json(...) super.merge( count: @count, finished: @finished, state: @connection ? "open" : "closed" ) end
def close(error = nil)
Close the connection and mark the body as finished.
def close(error = nil) if connection = @connection @connection = nil unless @finished connection.close_read end end super end
def empty?
def empty? @connection.nil? end
def initialize(connection, headers)
@parameter connection [Protocol::HTTP1::Connection] the connection to read the body from.
Initialize the chunked body.
def initialize(connection, headers) @connection = connection @finished = false @headers = headers @length = 0 @count = 0 end
def inspect
def inspect "\#<#{self.class} #{@length} bytes read in #{@count} chunks, #{@finished ? 'finished' : 'reading'}>" end
def length
def length # We only know the length once we've read the final chunk: if @finished @length end end
def read
@returns [String | Nil] the next chunk of data, or `nil` if the body is finished.
Follows the procedure outlined in https://tools.ietf.org/html/rfc7230#section-4.1.3
Read a chunk of data.
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 read_trailer
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