class Puma::Client

An instance of this class represents a unique request from a client. For example, this could be a web request from a browser or from CURL.

An instance of `Puma::Client` can be used as if it were an IO object by the reactor. The reactor is expected to call `#to_io` on any non-IO objects it polls. For example, nio4r internally calls `IO::try_convert` (which may call `#to_io`) when a new socket is registered.

Instances of this class are responsible for knowing if the header and body are fully buffered via the `try_to_finish` method. They can be used to “time out” a response via the `timeout_at` reader.

Constants

EmptyBody

The object used for a request with no body. All requests with no body share this one object since it has no state.

Attributes

body[R]
env[R]
hijacked[R]
io[R]
listener[RW]
peerip[W]
ready[R]
remote_addr_header[RW]
tempfile[R]
timeout_at[R]
to_io[R]

Public Class Methods

new(io, env=nil) click to toggle source
# File lib/puma/client.rb, line 46
def initialize(io, env=nil)
  @io = io
  @to_io = io.to_io
  @proto_env = env
  if !env
    @env = nil
  else
    @env = env.dup
  end

  @parser = HttpParser.new
  @parsed_bytes = 0
  @read_header = true
  @ready = false

  @body = nil
  @body_read_start = nil
  @buffer = nil
  @tempfile = nil

  @timeout_at = nil

  @requests_served = 0
  @hijacked = false

  @peerip = nil
  @listener = nil
  @remote_addr_header = nil

  @body_remain = 0

  @in_last_chunk = false
end

Public Instance Methods

call() click to toggle source

For the hijack protocol (allows us to just put the Client object into the env)

# File lib/puma/client.rb, line 102
def call
  @hijacked = true
  env[HIJACK_IO] ||= @io
end
can_close?() click to toggle source

Returns true if the persistent connection can be closed immediately without waiting for the configured idle/shutdown timeout. @version 5.0.0

# File lib/puma/client.rb, line 242
def can_close?
  # Allow connection to close if we're not in the middle of parsing a request.
  @parsed_bytes == 0
end
close() click to toggle source
# File lib/puma/client.rb, line 157
def close
  begin
    @io.close
  rescue IOError
    Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
  end
end
eagerly_finish() click to toggle source
# File lib/puma/client.rb, line 203
def eagerly_finish
  return true if @ready
  return false unless IO.select([@to_io], nil, nil, 0)
  try_to_finish
end
finish(timeout) click to toggle source
# File lib/puma/client.rb, line 209
def finish(timeout)
  return if @ready
  IO.select([@to_io], nil, nil, timeout) || timeout! until try_to_finish
end
in_data_phase() click to toggle source

@!attribute [r] in_data_phase

# File lib/puma/client.rb, line 108
def in_data_phase
  !@read_header
end
inspect() click to toggle source

@!attribute [r] inspect

# File lib/puma/client.rb, line 96
def inspect
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
end
io_ok?() click to toggle source

Test to see if io meets a bare minimum of functioning, @to_io needs to be used for MiniSSL::Socket

# File lib/puma/client.rb, line 91
def io_ok?
  @to_io.is_a?(::BasicSocket) && !closed?
end
peerip() click to toggle source
# File lib/puma/client.rb, line 226
def peerip
  return @peerip if @peerip

  if @remote_addr_header
    hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
    @peerip = hdr
    return hdr
  end

  @peerip ||= @io.peeraddr.last
end
reset(fast_check=true) click to toggle source
# File lib/puma/client.rb, line 121
def reset(fast_check=true)
  @parser.reset
  @read_header = true
  @env = @proto_env.dup
  @body = nil
  @tempfile = nil
  @parsed_bytes = 0
  @ready = false
  @body_remain = 0
  @peerip = nil if @remote_addr_header
  @in_last_chunk = false

  if @buffer
    @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)

    if @parser.finished?
      return setup_body
    elsif @parsed_bytes >= MAX_HEADER
      raise HttpParserError,
        "HEADER is longer than allowed, aborting client early."
    end

    return false
  else
    begin
      if fast_check &&
          IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
        return try_to_finish
      end
    rescue IOError
      # swallow it
    end

  end
end
set_timeout(val) click to toggle source
# File lib/puma/client.rb, line 112
def set_timeout(val)
  @timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
end
timeout() click to toggle source

Number of seconds until the timeout elapses.

# File lib/puma/client.rb, line 117
def timeout
  [@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
end
timeout!() click to toggle source
# File lib/puma/client.rb, line 214
def timeout!
  write_error(408) if in_data_phase
  raise ConnectionError
end
try_to_finish() click to toggle source
# File lib/puma/client.rb, line 165
def try_to_finish
  return read_body unless @read_header

  begin
    data = @io.read_nonblock(CHUNK_SIZE)
  rescue IO::WaitReadable
    return false
  rescue EOFError
    # Swallow error, don't log
  rescue SystemCallError, IOError
    raise ConnectionError, "Connection error detected during read"
  end

  # No data means a closed socket
  unless data
    @buffer = nil
    set_ready
    raise EOFError
  end

  if @buffer
    @buffer << data
  else
    @buffer = data
  end

  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)

  if @parser.finished?
    return setup_body
  elsif @parsed_bytes >= MAX_HEADER
    raise HttpParserError,
      "HEADER is longer than allowed, aborting client early."
  end

  false
end
write_error(status_code) click to toggle source
# File lib/puma/client.rb, line 219
def write_error(status_code)
  begin
    @io << ERROR_RESPONSE[status_code]
  rescue StandardError
  end
end

Private Instance Methods

decode_chunk(chunk) click to toggle source
# File lib/puma/client.rb, line 407
def decode_chunk(chunk)
  if @partial_part_left > 0
    if @partial_part_left <= chunk.size
      if @partial_part_left > 2
        write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
      end
      chunk = chunk[@partial_part_left..-1]
      @partial_part_left = 0
    else
      if @partial_part_left > 2
        if @partial_part_left == chunk.size + 1
          # Don't include the last \r
          write_chunk(chunk[0..(@partial_part_left-3)])
        else
          # don't include the last \r\n
          write_chunk(chunk)
        end
      end
      @partial_part_left -= chunk.size
      return false
    end
  end

  if @prev_chunk.empty?
    io = StringIO.new(chunk)
  else
    io = StringIO.new(@prev_chunk+chunk)
    @prev_chunk = ""
  end

  while !io.eof?
    line = io.gets
    if line.end_with?("\r\n")
      len = line.strip.to_i(16)
      if len == 0
        @in_last_chunk = true
        @body.rewind
        rest = io.read
        last_crlf_size = "\r\n".bytesize
        if rest.bytesize < last_crlf_size
          @buffer = nil
          @partial_part_left = last_crlf_size - rest.bytesize
          return false
        else
          @buffer = rest[last_crlf_size..-1]
          @buffer = nil if @buffer.empty?
          set_ready
          return true
        end
      end

      len += 2

      part = io.read(len)

      unless part
        @partial_part_left = len
        next
      end

      got = part.size

      case
      when got == len
        write_chunk(part[0..-3]) # to skip the ending \r\n
      when got <= len - 2
        write_chunk(part)
        @partial_part_left = len - part.size
      when got == len - 1 # edge where we get just \r but not \n
        write_chunk(part[0..-2])
        @partial_part_left = len - part.size
      end
    else
      @prev_chunk = line
      return false
    end
  end

  if @in_last_chunk
    set_ready
    true
  else
    false
  end
end
read_body() click to toggle source
# File lib/puma/client.rb, line 315
def read_body
  if @chunked_body
    return read_chunked_body
  end

  # Read an odd sized chunk so we can read even sized ones
  # after this
  remain = @body_remain

  if remain > CHUNK_SIZE
    want = CHUNK_SIZE
  else
    want = remain
  end

  begin
    chunk = @io.read_nonblock(want)
  rescue IO::WaitReadable
    return false
  rescue SystemCallError, IOError
    raise ConnectionError, "Connection error detected during read"
  end

  # No chunk means a closed socket
  unless chunk
    @body.close
    @buffer = nil
    set_ready
    raise EOFError
  end

  remain -= @body.write(chunk)

  if remain <= 0
    @body.rewind
    @buffer = nil
    set_ready
    return true
  end

  @body_remain = remain

  false
end
read_chunked_body() click to toggle source
# File lib/puma/client.rb, line 360
def read_chunked_body
  while true
    begin
      chunk = @io.read_nonblock(4096)
    rescue IO::WaitReadable
      return false
    rescue SystemCallError, IOError
      raise ConnectionError, "Connection error detected during read"
    end

    # No chunk means a closed socket
    unless chunk
      @body.close
      @buffer = nil
      set_ready
      raise EOFError
    end

    if decode_chunk(chunk)
      @env[CONTENT_LENGTH] = @chunked_content_length.to_s
      return true
    end
  end
end
set_ready() click to toggle source
# File lib/puma/client.rb, line 493
def set_ready
  if @body_read_start
    @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
  end
  @requests_served += 1
  @ready = true
end
setup_body() click to toggle source
# File lib/puma/client.rb, line 249
def setup_body
  @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)

  if @env[HTTP_EXPECT] == CONTINUE
    # TODO allow a hook here to check the headers before
    # going forward
    @io << HTTP_11_100
    @io.flush
  end

  @read_header = false

  body = @parser.body

  te = @env[TRANSFER_ENCODING2]

  if te
    if te.include?(",")
      te.split(",").each do |part|
        if CHUNKED.casecmp(part.strip) == 0
          return setup_chunked_body(body)
        end
      end
    elsif CHUNKED.casecmp(te) == 0
      return setup_chunked_body(body)
    end
  end

  @chunked_body = false

  cl = @env[CONTENT_LENGTH]

  unless cl
    @buffer = body.empty? ? nil : body
    @body = EmptyBody
    set_ready
    return true
  end

  remain = cl.to_i - body.bytesize

  if remain <= 0
    @body = StringIO.new(body)
    @buffer = nil
    set_ready
    return true
  end

  if remain > MAX_BODY
    @body = Tempfile.new(Const::PUMA_TMP_BASE)
    @body.unlink
    @body.binmode
    @tempfile = @body
  else
    # The body[0,0] trick is to get an empty string in the same
    # encoding as body.
    @body = StringIO.new body[0,0]
  end

  @body.write body

  @body_remain = remain

  return false
end
setup_chunked_body(body) click to toggle source
# File lib/puma/client.rb, line 385
def setup_chunked_body(body)
  @chunked_body = true
  @partial_part_left = 0
  @prev_chunk = ""

  @body = Tempfile.new(Const::PUMA_TMP_BASE)
  @body.unlink
  @body.binmode
  @tempfile = @body
  @chunked_content_length = 0

  if decode_chunk(body)
    @env[CONTENT_LENGTH] = @chunked_content_length.to_s
    return true
  end
end
write_chunk(str) click to toggle source

@version 5.0.0

# File lib/puma/client.rb, line 403
def write_chunk(str)
  @chunked_content_length += @body.write(str)
end