Class: Raptor::Http1

Inherits:
Object
  • Object
show all
Defined in:
lib/raptor/http1.rb,
sig/generated/raptor/http1.rbs

Overview

Parses HTTP/1.x requests and dispatches them to the Rack application. Coordinates with the Ractor pool for parsing and with the reactor for requests that need more data before they can be handled.

Constant Summary collapse

BODY_BUFFER_THRESHOLD =

Returns:

  • (Object)
256 * 1024
CHUNKED_WRITE_THRESHOLD =

Returns:

  • (Object)
512 * 1024
FILE_CHUNK_SIZE =

Returns:

  • (Object)
64 * 1024
MAX_CHUNK_OVERHEAD =

Returns:

  • (Object)
16 * 1024
READ_BUFFER_SIZE =

Returns:

  • (Object)
64 * 1024
RESPONSE_BUFFER_CAPACITY =

Returns:

  • (Object)
4 * 1024
KEEPALIVE_READ_TIMEOUT =

Returns:

  • (::Float)
0.001
MAX_KEEPALIVE_REQUESTS =

Returns:

  • (::Integer)
100
HTTP_10 =

Returns:

  • (::String)
"HTTP/1.0"
HTTP_11 =

Returns:

  • (::String)
"HTTP/1.1"
STATUS_LINE_CACHE_10 =

Returns:

  • (Object)
Hash.new do |h, status|
  reason = Rack::Utils::HTTP_STATUS_CODES[status]
  h[status] = "HTTP/1.0 #{status}#{reason ? " #{reason}" : ""}\r\n".freeze
end
STATUS_LINE_CACHE_11 =

Returns:

  • (Object)
Hash.new do |h, status|
  reason = Rack::Utils::HTTP_STATUS_CODES[status]
  h[status] = "HTTP/1.1 #{status}#{reason ? " #{reason}" : ""}\r\n".freeze
end
STATUS_WITH_NO_ENTITY_BODY =

Returns:

  • (Object)
[204, 304, *100..199].freeze
CONTINUE_RESPONSE =

Returns:

  • (::String)
"HTTP/1.1 100 Continue\r\n\r\n"
BAD_REQUEST_RESPONSE =

Returns:

  • (::String)
"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
CONTENT_TOO_LARGE_RESPONSE =

Returns:

  • (::String)
"HTTP/1.1 413 Content Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
INTERNAL_SERVER_ERROR_RESPONSE =

Returns:

  • (::String)
"HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
CONNECTION_CLOSE =

Returns:

  • (::String)
"close"
CONNECTION_KEEPALIVE =

Returns:

  • (::String)
"keep-alive"
EXPECT_100_CONTINUE =

Returns:

  • (::String)
"100-continue"
TRANSFER_ENCODING_CHUNKED =

Returns:

  • (::String)
"chunked"
HTTP_CONNECTION =

Returns:

  • (::String)
"HTTP_CONNECTION"
HTTP_EXPECT =

Returns:

  • (::String)
"HTTP_EXPECT"
HTTP_TRANSFER_ENCODING =

Returns:

  • (::String)
"HTTP_TRANSFER_ENCODING"
RACK_HEADER_PREFIX =

Returns:

  • (::String)
"rack."
RACK_HIJACKED =

Returns:

  • (::String)
"rack.hijacked"
RACK_HIJACK_IO =

Returns:

  • (::String)
"rack.hijack_io"
ILLEGAL_HEADER_KEY_REGEX =

Returns:

  • (::Regexp)
/[\x00-\x20\(\)<>@,;:\\"\/\[\]\?=\{\}\x7F]/
ILLEGAL_HEADER_VALUE_REGEX =

Returns:

  • (::Regexp)
/[\x00-\x08\x0A-\x1F]/

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, server_port, connection_options: {}, http1_options: {}, access_log_io: nil, on_error: nil) ⇒ Http1

Creates a new Http1 handler.

Parameters:

  • app (#call)

    the Rack application to dispatch complete requests to

  • server_port (Integer)

    port number used to populate SERVER_PORT in the Rack env

  • connection_options (Hash) (defaults to: {})

    per-connection settings shared across protocols

  • http1_options (Hash) (defaults to: {})

    HTTP/1.1-specific settings

  • access_log_io (IO, nil) (defaults to: nil)

    IO to write Common Log Format access entries to, or nil to disable

  • on_error (#call, nil) (defaults to: nil)

    callback invoked with (env, exception) when the Rack app raises

  • connection_options: (Hash[Symbol, untyped]) (defaults to: {})
  • http1_options: (Hash[Symbol, untyped]) (defaults to: {})
  • access_log_io: (IO, nil) (defaults to: nil)
  • on_error: (^(Hash[String, untyped]?, Exception) -> void, nil) (defaults to: nil)

Options Hash (connection_options:):

  • :write_timeout (Integer)

    per-write socket timeout in seconds

  • :max_body_size (Integer, nil)

    maximum request body size in bytes

  • :body_spool_threshold (Integer, nil)

    spool bodies larger than this to a tempfile

Options Hash (http1_options:):

  • :max_keepalive_requests (Integer)

    maximum requests per HTTP/1.1 keep-alive connection



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/raptor/http1.rb', line 152

def initialize(app, server_port, connection_options: {}, http1_options: {}, access_log_io: nil, on_error: nil)
  @app = app
  @server_port = server_port
  @server_port_string = server_port.to_s.freeze
  @write_timeout = connection_options[:write_timeout] || Http::WRITE_TIMEOUT
  @max_body_size = connection_options[:max_body_size]
  @body_spool_threshold = connection_options[:body_spool_threshold]
  @max_keepalive_requests = http1_options[:max_keepalive_requests] || MAX_KEEPALIVE_REQUESTS
  @access_log_io = access_log_io
  @on_error = on_error
  @running = AtomicBoolean.new(true)
  @env_template = {
    Rack::RACK_VERSION => Rack::VERSION,
    Rack::RACK_IS_HIJACK => true,
    Rack::SCRIPT_NAME => "",
    Rack::QUERY_STRING => "",
    Http::SERVER_SOFTWARE => Http::SERVER_SOFTWARE_VALUE
  }.freeze
end

Class Method Details

.decode_chunked(buffer, max_size = nil) ⇒ Array(String, Symbol)

Decodes a chunked transfer-encoded body buffer.

Returns the decoded bytes and a state symbol: :complete when the terminating zero-length chunk was found, :too_large when the decoded size would exceed max_size, :malformed when chunk framing overhead exceeds MAX_CHUNK_OVERHEAD, or :incomplete otherwise.

Parameters:

  • buffer (String)

    the raw body buffer to decode

  • max_size (Integer, nil) (defaults to: nil)

    maximum decoded body size, or nil for unlimited

Returns:

  • (Array(String, Symbol))

    decoded body and completion state



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/raptor/http1.rb', line 101

def self.decode_chunked(buffer, max_size = nil)
  decoded = String.new
  offset = 0
  overhead = 0

  while offset < buffer.bytesize
    crlf = buffer.index("\r\n", offset)
    return [decoded, :incomplete] unless crlf

    chunk_size = buffer.byteslice(offset, crlf - offset).to_i(16)
    return [decoded, :complete] if chunk_size == 0
    return [decoded, :too_large] if max_size && (decoded.bytesize + chunk_size) > max_size

    overhead += (crlf - offset) + 4
    return [decoded, :malformed] if overhead > (decoded.bytesize + chunk_size + MAX_CHUNK_OVERHEAD)

    offset = crlf + 2
    decoded << buffer.byteslice(offset, chunk_size)
    offset += chunk_size + 2
  end

  [decoded, :incomplete]
end

.request_smuggling?(env) ⇒ Boolean

Returns true when the message framing shows a request-smuggling vector per RFC 9112 section 6.3: a Transfer-Encoding where chunked is missing, not the final encoding, or duplicated; a Transfer-Encoding paired with a Content-Length; or a Content-Length containing any non-digit character.

Parameters:

  • env (Hash)

    the Rack environment after header parsing

Returns:

  • (Boolean)


72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/raptor/http1.rb', line 72

def self.request_smuggling?(env)
  transfer_encoding = env[HTTP_TRANSFER_ENCODING]
  content_length = env[Http::CONTENT_LENGTH]

  if transfer_encoding
    return true if content_length

    encodings = transfer_encoding.downcase.split(",").map(&:strip)
    return true if encodings.last != TRANSFER_ENCODING_CHUNKED
    return true if encodings.count(TRANSFER_ENCODING_CHUNKED) > 1
  elsif content_length
    return true if content_length.match?(/[^\d]/)
  end

  false
end

Instance Method Details

#append_header_value(result, name, value) ⇒ void

This method returns an undefined value.

Appends one or more name: value header lines to result. Newline- separated values are emitted as separate lines; empty values and values with illegal characters are skipped silently.

Parameters:

  • result (String)

    the buffer to append to

  • name (String)

    the header name

  • value (Object)

    the header value (any object responding to to_s)



1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
# File 'lib/raptor/http1.rb', line 1281

def append_header_value(result, name, value)
  string_value = value.is_a?(String) ? value : value.to_s
  return if string_value.empty?

  if string_value.include?("\n")
    string_value.split("\n").each do |line|
      next if line.empty? || illegal_header_value?(line)

      result << name << ": " << line << "\r\n"
    end
  else
    return if illegal_header_value?(string_value)

    result << name << ": " << string_value << "\r\n"
  end
end

#build_rack_env(env, parse_data, body, socket, remote_addr: Server::DEFAULT_REMOTE_ADDR, url_scheme: Server::HTTP_SCHEME) ⇒ Hash

Builds a Rack environment hash from parsed HTTP request data.

Populates all required Rack env keys including rack.* keys, REMOTE_ADDR, SERVER_NAME, SERVER_PORT, and hijack support.

Parameters:

  • env (Hash)

    partial env hash from the HTTP parser

  • parse_data (Hash)

    metadata from the parsing pass, including content_length

  • body (String, nil)

    decoded request body, or nil if no body

  • socket (TCPSocket)

    the client socket, used for hijack support

  • remote_addr (String) (defaults to: Server::DEFAULT_REMOTE_ADDR)

    client IP address

  • url_scheme (String) (defaults to: Server::HTTP_SCHEME)

    "http" or "https"

  • remote_addr: (String) (defaults to: Server::DEFAULT_REMOTE_ADDR)
  • url_scheme: (String) (defaults to: Server::HTTP_SCHEME)

Returns:

  • (Hash)

    fully populated Rack environment hash



708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
# File 'lib/raptor/http1.rb', line 708

def build_rack_env(env, parse_data, body, socket, remote_addr: Server::DEFAULT_REMOTE_ADDR, url_scheme: Server::HTTP_SCHEME)
  env[Rack::RACK_INPUT] = build_rack_input(body)
  env[Rack::RACK_ERRORS] = $stderr
  env[Rack::RACK_RESPONSE_FINISHED] = []
  env[Rack::RACK_HIJACK] = proc do
    env[RACK_HIJACKED] = true
    env[RACK_HIJACK_IO] = socket
    socket
  end
  env[Rack::RACK_EARLY_HINTS] = proc do |hints|
    send_early_hints(socket, hints) rescue nil
  end

  env[Rack::PATH_INFO] = env.delete(Rack::REQUEST_PATH) if env.key?(Rack::REQUEST_PATH)
  env[Rack::PATH_INFO] = "" unless env.key?(Rack::PATH_INFO)
  if (content_length = parse_data[:content_length]).positive?
    env[Http::CONTENT_LENGTH] = content_length.to_s
  end

  env[Http::REMOTE_ADDR] = remote_addr
  env[Http::HTTP_VERSION] = env[Rack::SERVER_PROTOCOL]

  behind_tls_proxy = (url_scheme == Server::HTTP_SCHEME) && forwarded_https?(env)
  env[Rack::RACK_URL_SCHEME] = behind_tls_proxy ? Server::HTTPS_SCHEME : url_scheme
  default_port = behind_tls_proxy ? "443" : @server_port_string

  http_host = env[Rack::HTTP_HOST]
  host = nil
  port = nil
  if http_host && !http_host.empty?
    if http_host.start_with?("[")
      bracket_end = http_host.index("]")
      if bracket_end
        host = http_host.byteslice(1, bracket_end - 1)
        port_colon = http_host.index(":", bracket_end + 1)
        port = port_colon && http_host.byteslice(port_colon + 1, http_host.bytesize - port_colon - 1)
      end
    else
      colon = http_host.index(":")
      if colon
        host = http_host.byteslice(0, colon)
        port = http_host.byteslice(colon + 1, http_host.bytesize - colon - 1)
      else
        host = http_host
      end
    end
  end
  env[Rack::SERVER_NAME] ||= host || Server::DEFAULT_SERVER_NAME
  env[Rack::SERVER_PORT] ||= port || default_port

  env
end

#build_rack_input(body) ⇒ IO

Builds the rack.input IO object for the request body. Returns an in-memory StringIO for bodies up to the spool threshold, or a Tempfile for larger bodies to bound per-worker memory.

Parameters:

  • body (String, nil)

    decoded request body

Returns:

  • (IO)

    an IO-like object positioned at the start of the body



769
770
771
772
773
774
775
776
777
778
779
# File 'lib/raptor/http1.rb', line 769

def build_rack_input(body)
  if body && @body_spool_threshold && body.bytesize > @body_spool_threshold
    tempfile = Tempfile.new("raptor-body")
    tempfile.binmode
    tempfile.write(body)
    tempfile.rewind
    tempfile
  else
    (body ? StringIO.new(body) : StringIO.new).set_encoding(Encoding::ASCII_8BIT)
  end
end

#build_status_line(http_version, status) ⇒ String

Returns the HTTP status line for status. Callers must not retain the returned string across further calls on the same thread.

Parameters:

  • http_version (String)

    "HTTP/1.1" or "HTTP/1.0"

  • status (Integer)

    HTTP status code

Returns:

  • (String)

    the status line including trailing CRLF



958
959
960
961
962
963
964
# File 'lib/raptor/http1.rb', line 958

def build_status_line(http_version, status)
  cache = http_version == HTTP_11 ? STATUS_LINE_CACHE_11 : STATUS_LINE_CACHE_10
  response = (Thread.current[:raptor_response_buffer] ||= String.new(capacity: RESPONSE_BUFFER_CAPACITY))
  response.clear
  response << cache[status]
  response
end

#calculate_content_length(body) ⇒ Integer?

Calculates content length from an array or file body without consuming it.

Returns nil for enumerable bodies whose length cannot be determined upfront.

Parameters:

  • body (Object)

    the response body

Returns:

  • (Integer, nil)

    the byte length, or nil if it cannot be determined



1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
# File 'lib/raptor/http1.rb', line 1074

def calculate_content_length(body)
  if body.respond_to?(:to_ary)
    array = body.to_ary
    return unless array.is_a?(Array)

    array.sum { |chunk| chunk.is_a?(String) ? chunk.bytesize : 0 }
  elsif body.respond_to?(:to_path) && (path = body.to_path) && File.readable?(path)
    File.size(path)
  else
    nil
  end
end

#call_response_finished(env, status, headers, error) ⇒ void

This method returns an undefined value.

Calls all rack.response_finished callbacks registered in the environment.

Callbacks are called in reverse registration order. Individual callback failures are rescued so all callbacks are always attempted.

Parameters:

  • env (Hash, nil)

    the Rack environment

  • status (Integer, nil)

    the response status code

  • headers (Hash, nil)

    the response headers

  • error (Exception, nil)

    any error raised during processing, or nil on success



1310
1311
1312
1313
1314
1315
1316
# File 'lib/raptor/http1.rb', line 1310

def call_response_finished(env, status, headers, error)
  return unless env && env[Rack::RACK_RESPONSE_FINISHED].is_a?(Array)

  env[Rack::RACK_RESPONSE_FINISHED].reverse_each do |callable|
    callable.call(env, status, headers, error) rescue nil
  end
end

#cork_socket(socket) ⇒ void

This method returns an undefined value.

Enables TCP_CORK on the socket to batch outgoing packets into fewer segments.

Only applies to TCP sockets. No-op on non-TCP sockets. Available on Linux only; this method is not defined on other platforms.

Parameters:

  • socket (TCPSocket)

    the socket to cork



1355
1356
1357
# File 'lib/raptor/http1.rb', line 1355

def cork_socket(socket)
  socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_CORK, 1) if socket.is_a?(TCPSocket)
end

#eager_accept(socket, id, reactor, thread_pool, remote_addr, url_scheme) ⇒ void

This method returns an undefined value.

Eagerly reads and parses the first request on a freshly accepted connection on the server thread, dispatching directly to the thread pool when complete. Falls back to the reactor when more data is needed.

Parameters:

  • socket (TCPSocket)

    the freshly accepted client socket

  • id (Integer)

    unique client identifier

  • reactor (Reactor)

    the reactor for fallback registration

  • thread_pool (AtomicThreadPool)

    thread pool for application processing

  • remote_addr (String)

    client IP address

  • url_scheme (String)

    "http" or "https"



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/raptor/http1.rb', line 221

def eager_accept(socket, id, reactor, thread_pool, remote_addr, url_scheme)
  buffer = (Thread.current[:raptor_read_buffer] ||= String.new(capacity: READ_BUFFER_SIZE))

  begin
    socket.read_nonblock(READ_BUFFER_SIZE, buffer)
  rescue IO::WaitReadable
    reactor.add(
      id: id,
      socket: socket,
      remote_addr: remote_addr,
      url_scheme: url_scheme
    )
    return
  rescue EOFError, IOError
    socket.close rescue nil
    return
  end

  while socket.respond_to?(:pending) && socket.pending > 0
    buffer << socket.read_nonblock(socket.pending)
  end

  parser = HttpParser.new
  env = @env_template.dup
  nread = begin
    parser.execute(env, buffer, 0)
  rescue HttpParserError
    reject_malformed(socket)
    return
  end
  parse_data = { parse_count: 1, content_length: parser.content_length }

  body = nil
  if !parser.finished?
    fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
    return
  elsif Http1.request_smuggling?(env)
    reject_malformed(socket)
    return
  elsif parser.has_body?
    if @max_body_size && parser.content_length > @max_body_size
      reject_oversized(socket)
      return
    end

    body = buffer.byteslice(nread..-1) || ""

    if env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
      body, chunked_state = Http1.decode_chunked(body, @max_body_size)
      case chunked_state
      when :complete
        env.delete(HTTP_TRANSFER_ENCODING)
      when :too_large
        reject_oversized(socket)
        return
      when :malformed
        reject_malformed(socket)
        return
      else
        fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
        return
      end
    elsif parser.content_length > body.bytesize
      fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
      return
    end
  end

  thread_pool << proc do
    process_client(socket, id, env, parse_data, body, reactor, thread_pool, 1, remote_addr, url_scheme)
  end
end

#eager_keepalive(socket, id, reactor, thread_pool, request_count, remote_addr, url_scheme) ⇒ void

This method returns an undefined value.

Attempts to read and process subsequent requests inline on a kept-alive connection. Blocks briefly for the next request to avoid a full reactor round-trip. Falls back to the reactor when no data arrives within the timeout, when the thread pool has queued work (deprioritization), or when the request is incomplete.

Parameters:

  • socket (TCPSocket)

    the client socket

  • id (Integer)

    unique client identifier

  • reactor (Reactor)

    the reactor for fallback registration

  • thread_pool (AtomicThreadPool)

    thread pool for deprioritization

  • request_count (Integer)

    number of requests handled on this connection

  • remote_addr (String)

    client IP address

  • url_scheme (String)

    "http" or "https"



546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
# File 'lib/raptor/http1.rb', line 546

def eager_keepalive(socket, id, reactor, thread_pool, request_count, remote_addr, url_scheme)
  loop do
    unless @running.true?
      socket.close rescue nil
      return
    end

    unless socket.wait_readable(KEEPALIVE_READ_TIMEOUT)
      reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
      return
    end

    buffer = (Thread.current[:raptor_read_buffer] ||= String.new(capacity: READ_BUFFER_SIZE))

    begin
      socket.read_nonblock(READ_BUFFER_SIZE, buffer)
    rescue IO::WaitReadable
      reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
      return
    rescue EOFError
      socket.close rescue nil
      return
    end

    while socket.respond_to?(:pending) && socket.pending > 0
      buffer << socket.read_nonblock(socket.pending)
    end

    parser = HttpParser.new
    env = @env_template.dup
    nread = begin
      parser.execute(env, buffer, 0)
    rescue HttpParserError
      reject_malformed(socket)
      return
    end
    parse_data = { parse_count: 1, content_length: parser.content_length }

    body = nil
    if !parser.finished?
      fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme)
      return
    elsif parser.has_body?
      body = buffer.byteslice(nread..-1) || ""

      chunked = env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
      if chunked || parser.content_length > body.bytesize
        fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme)
        return
      end
    end

    request_count += 1

    if thread_pool.queue_size > 0
      thread_pool << proc do
        process_client(
          socket,
          id,
          env,
          parse_data,
          body,
          reactor,
          thread_pool,
          request_count,
          remote_addr,
          url_scheme
        )
      end
      return
    end

    keep_alive = process_request(
      socket,
      env,
      parse_data,
      body,
      request_count,
      remote_addr,
      url_scheme
    )
    return unless keep_alive
  end
end

#expects_100_continue?(env) ⇒ Boolean

Returns true if the request expects a 100 Continue response per RFC 7231 section 5.1.1.

Parameters:

  • env (Hash)

    the parsed Rack environment (possibly incomplete)

Returns:

  • (Boolean)


420
421
422
# File 'lib/raptor/http1.rb', line 420

def expects_100_continue?(env)
  (env[Rack::SERVER_PROTOCOL] == HTTP_11) && env[HTTP_EXPECT]&.casecmp?(EXPECT_100_CONTINUE)
end

#fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme, persisted: true) ⇒ void

This method returns an undefined value.

Re-registers a socket with the reactor for further processing when an incomplete request is received during eager accept or eager keep-alive.

The persisted flag selects between persistent_data_timeout (for kept-alive connections awaiting the next request) and chunk_data_timeout (for fresh connections awaiting the rest of the first request).

Parameters:

  • socket (TCPSocket)

    the client socket

  • id (Integer)

    unique client identifier

  • buffer (String)

    the partial request data already read

  • env (Hash)

    partial env hash from the HTTP parser

  • parse_data (Hash)

    metadata from the parsing pass

  • reactor (Reactor)

    the reactor to re-register with

  • request_count (Integer)

    number of requests handled on this connection

  • remote_addr (String)

    client IP address

  • url_scheme (String)

    "http" or "https"

  • persisted (Boolean) (defaults to: true)

    whether the connection has already completed at least one request

  • persisted: (Boolean) (defaults to: true)


651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
# File 'lib/raptor/http1.rb', line 651

def fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme, persisted: true)
  continued = expects_100_continue?(env)
  socket_write(socket, CONTINUE_RESPONSE) rescue nil if continued

  reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
  state = {
    id: id,
    buffer: buffer.dup,
    env: env,
    request_count: request_count,
    parse_data: parse_data,
    remote_addr: remote_addr,
    url_scheme: url_scheme
  }
  state[:persisted] = true if persisted
  state[:continued] = true if continued
  reactor.update_state(Ractor.make_shareable(state))
end

#format_headers(headers) ⇒ String

Formats a headers hash into an HTTP header string.

Skips entries with illegal keys or values. Array values are written as separate header lines.

Parameters:

  • headers (Hash)

    normalized response headers

Returns:

  • (String)

    formatted header lines, each ending with CRLF



1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
# File 'lib/raptor/http1.rb', line 1257

def format_headers(headers)
  result = +""
  headers.each do |name, value|
    next if illegal_header_key?(name)

    if value.is_a?(Array)
      value.each { |entry| append_header_value(result, name, entry) }
    else
      append_header_value(result, name, value)
    end
  end
  result
end

#forwarded_https?(env) ⇒ Boolean

Returns true when an upstream proxy signals that it terminated TLS for this request via X-Forwarded-Proto, X-Forwarded-Scheme, or X-Forwarded-Ssl. Only the first comma-separated value is consulted.

Parameters:

  • env (Hash)

    the Rack environment

Returns:

  • (Boolean)


789
790
791
792
793
794
# File 'lib/raptor/http1.rb', line 789

def forwarded_https?(env)
  proto = env["HTTP_X_FORWARDED_PROTO"] || env["HTTP_X_FORWARDED_SCHEME"]
  return true if proto && proto.split(",").first&.strip&.casecmp?(Server::HTTPS_SCHEME)

  env["HTTP_X_FORWARDED_SSL"]&.casecmp?("on") || false
end

#handle_parsed_request(parsed_request, reactor, thread_pool) ⇒ void

This method returns an undefined value.

Handles a parsed HTTP request by either continuing parsing or dispatching to the Rack app.

For incomplete requests, updates reactor state and re-registers for more I/O. For complete requests, removes from reactor, builds Rack env, and dispatches to thread pool.

Parameters:

  • parsed_request (Hash)

    the parsed request state from the ractor pool

  • reactor (Reactor)

    the reactor managing the client connection

  • thread_pool (AtomicThreadPool)

    thread pool for application processing



372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# File 'lib/raptor/http1.rb', line 372

def handle_parsed_request(parsed_request, reactor, thread_pool)
  if parsed_request[:too_large]
    socket = reactor.remove(parsed_request[:id])
    reject_oversized(socket) if socket
    return
  end

  if parsed_request[:malformed]
    socket = reactor.remove(parsed_request[:id])
    reject_malformed(socket) if socket
    return
  end

  unless parsed_request[:complete]
    parsed_request = send_continue_if_expected(parsed_request, reactor)
    reactor.update_state(parsed_request)
  else
    socket = reactor.remove(parsed_request[:id])
    request_count = (parsed_request[:request_count] || 0) + 1
    remote_addr = parsed_request[:remote_addr] || Server::DEFAULT_REMOTE_ADDR
    url_scheme = parsed_request[:url_scheme] || Server::HTTP_SCHEME

    thread_pool << proc do
      process_client(
        socket,
        parsed_request[:id],
        parsed_request[:env].dup,
        parsed_request[:parse_data],
        parsed_request[:body],
        reactor,
        thread_pool,
        request_count,
        remote_addr,
        url_scheme
      )
    end
  end
end

#http_parser_workerProc

Returns a Proc for HTTP parsing work in Ractor context.

The returned Proc processes raw socket data through the appropriate HTTP parser and returns either a complete request state (ready for app processing) or incomplete request state (needs more data).

Returns:

  • (Proc)

    a Ractor-safe proc that accepts a state hash and returns an updated state hash



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/raptor/http1.rb', line 303

def http_parser_worker
  max_body_size = @max_body_size
  env_template = @env_template

  proc do |data|
    next Raptor::Http2.process_frames(data) if data[:protocol] == :http2

    parser = Raptor::HttpParser.new
    env = env_template.dup
    nread = begin
      parser.execute(env, data[:buffer], 0)
    rescue Raptor::HttpParserError
      next Ractor.make_shareable(data.merge(complete: true, malformed: true))
    end
    parse_data = if data[:parse_data]
      data[:parse_data].dup
    else
      { parse_count: 0, content_length: parser.content_length }
    end
    parse_data[:parse_count] += 1

    message = if parser.finished?
      if Raptor::Http1.request_smuggling?(env)
        data.merge(env: env, body: nil, parse_data: parse_data, complete: true, malformed: true)
      elsif parser.has_body?
        body_buffer = data[:buffer].byteslice(nread..-1) || ""

        if max_body_size && parser.content_length > max_body_size
          data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
        elsif env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
          decoded_body, chunked_state = Raptor::Http1.decode_chunked(body_buffer, max_body_size)

          case chunked_state
          when :complete
            env.delete(HTTP_TRANSFER_ENCODING)
            data.merge(env: env, body: decoded_body, parse_data: parse_data, complete: true)
          when :too_large
            data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
          when :malformed
            data.merge(env: env, body: nil, parse_data: parse_data, complete: true, malformed: true)
          else
            data.merge(env: env, parse_data: parse_data)
          end
        elsif parser.content_length > body_buffer.bytesize
          data.merge(env: env, parse_data: parse_data)
        else
          data.merge(env: env, body: body_buffer, parse_data: parse_data, complete: true)
        end
      else
        data.merge(env: env, body: nil, parse_data: parse_data, complete: true)
      end
    else
      data.merge(env: env, parse_data: parse_data)
    end
    Ractor.make_shareable(message)
  end
end

#illegal_header_key?(key) ⇒ Boolean

Returns true if the header key contains characters illegal in HTTP headers.

Parameters:

  • key (String)

    the header key to check

Returns:

  • (Boolean)

    true if the key is illegal



1234
1235
1236
# File 'lib/raptor/http1.rb', line 1234

def illegal_header_key?(key)
  key.match?(ILLEGAL_HEADER_KEY_REGEX)
end

#illegal_header_value?(value) ⇒ Boolean

Returns true if the header value contains characters illegal in HTTP headers.

Parameters:

  • value (String)

    the header value to check

Returns:

  • (Boolean)

    true if the value is illegal



1244
1245
1246
# File 'lib/raptor/http1.rb', line 1244

def illegal_header_value?(value)
  value.match?(ILLEGAL_HEADER_VALUE_REGEX)
end

#keep_alive?(env, request_count) ⇒ Boolean

Determines whether the connection should be kept alive after the response.

Returns false if the request limit has been reached. For HTTP/1.1, keep-alive is the default unless the client sent Connection: close. For HTTP/1.0, keep-alive must be explicitly requested.

Parameters:

  • env (Hash)

    the Rack environment

  • request_count (Integer)

    number of requests handled on this connection

Returns:

  • (Boolean)

    true if the connection should be kept alive



807
808
809
810
811
812
813
814
815
816
817
# File 'lib/raptor/http1.rb', line 807

def keep_alive?(env, request_count)
  return false if request_count >= @max_keepalive_requests

  connection_header = env[HTTP_CONNECTION]

  if env[Rack::SERVER_PROTOCOL] == HTTP_11
    !connection_header&.casecmp?(CONNECTION_CLOSE)
  else
    connection_header&.casecmp?(CONNECTION_KEEPALIVE) || false
  end
end

#normalize_headers(headers) ⇒ Hash

Normalizes response headers by downcasing keys and filtering invalid entries.

Removes headers with illegal keys, rack.* prefixed headers, and "status" headers. Raises if headers is not a Hash or contains non-String keys.

Parameters:

  • headers (Hash)

    raw headers from the Rack application

Returns:

  • (Hash)

    normalized headers with lowercased string keys

Raises:

  • (TypeError)

    if headers is not a Hash or a key is not a String



913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
# File 'lib/raptor/http1.rb', line 913

def normalize_headers(headers)
  raise TypeError, "headers must be a Hash" unless headers.is_a?(Hash)

  normalized = {}
  headers.each do |key, value|
    raise TypeError, "header keys must be Strings" unless key.is_a?(String)

    next if illegal_header_key?(key)

    normalized_key = key.match?(/[A-Z]/) ? key.downcase : key
    next if normalized_key.start_with?(RACK_HEADER_PREFIX)
    next if normalized_key == "status"

    normalized[normalized_key] = value
  end
  normalized
end

#process_client(socket, id, env, parse_data, body, reactor, thread_pool, request_count, remote_addr, url_scheme) ⇒ void

This method returns an undefined value.

Processes a client connection by handling the current request and, if keep-alive, eagerly reading subsequent requests inline.

Parameters:

  • socket (TCPSocket)

    the client socket

  • id (Integer)

    unique client identifier

  • env (Hash)

    partial env hash from the HTTP parser

  • parse_data (Hash)

    metadata from the parsing pass

  • body (String, nil)

    decoded request body

  • reactor (Reactor)

    the reactor managing the client connection

  • thread_pool (AtomicThreadPool)

    thread pool for application processing

  • request_count (Integer)

    number of requests handled on this connection

  • remote_addr (String)

    client IP address

  • url_scheme (String)

    "http" or "https"



464
465
466
467
# File 'lib/raptor/http1.rb', line 464

def process_client(socket, id, env, parse_data, body, reactor, thread_pool, request_count, remote_addr, url_scheme)
  keep_alive = process_request(socket, env, parse_data, body, request_count, remote_addr, url_scheme)
  eager_keepalive(socket, id, reactor, thread_pool, request_count, remote_addr, url_scheme) if keep_alive
end

#process_request(socket, env, parse_data, body, request_count, remote_addr, url_scheme) ⇒ Boolean

Builds the Rack env, calls the application, and writes the response. Returns true if the connection should be kept alive for further requests, false otherwise (including hijack and error cases).

Parameters:

  • socket (TCPSocket)

    the client socket

  • env (Hash)

    partial env hash from the HTTP parser

  • parse_data (Hash)

    metadata from the parsing pass

  • body (String, nil)

    decoded request body

  • request_count (Integer)

    number of requests handled on this connection

  • remote_addr (String)

    client IP address

  • url_scheme (String)

    "http" or "https"

Returns:

  • (Boolean)

    true if the connection should be kept alive



483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
# File 'lib/raptor/http1.rb', line 483

def process_request(socket, env, parse_data, body, request_count, remote_addr, url_scheme)
  rack_env = nil
  status = nil
  headers = nil
  hijacked = false
  keep_alive = false
  response_started = false

  begin
    rack_env = build_rack_env(env, parse_data, body, socket, remote_addr: remote_addr, url_scheme: url_scheme)
    status, headers, body = @app.call(rack_env)

    if rack_env[RACK_HIJACKED]
      hijacked = true
      body.close if body.respond_to?(:close)
    else
      hijacked = headers.is_a?(Hash) && !!headers[Rack::RACK_HIJACK]
      streaming = body.respond_to?(:call) && !body.respond_to?(:each)
      keep_alive = (hijacked || streaming) ? false : keep_alive?(rack_env, request_count)
      response_size = response_size(headers, body) unless hijacked
      response_started = true
      write_response(socket, rack_env, status, headers, body, keep_alive: keep_alive)
    end

    write_access_log(rack_env, status, response_size, remote_addr) if @access_log_io && !hijacked
    call_response_finished(rack_env, status, headers, nil)
    keep_alive && !hijacked
  rescue => error
    call_response_finished(rack_env, status, headers, error) if rack_env
    socket.write(INTERNAL_SERVER_ERROR_RESPONSE) rescue nil unless response_started || hijacked
    keep_alive = false

    if @on_error
      @on_error.call(rack_env, error) rescue nil
    else
      raise
    end
  ensure
    rack_input = rack_env && rack_env[Rack::RACK_INPUT]
    rack_input.close! rescue nil if rack_input.respond_to?(:close!)

    unless hijacked || keep_alive
      socket.close rescue nil
    end
  end
end

#reject_malformed(socket) ⇒ void

This method returns an undefined value.

Writes a 400 response and closes the socket. Used when the HTTP parser rejects the request line or headers.

Parameters:

  • socket (TCPSocket)

    the client socket



689
690
691
692
# File 'lib/raptor/http1.rb', line 689

def reject_malformed(socket)
  socket.write(BAD_REQUEST_RESPONSE) rescue nil
  socket.close rescue nil
end

#reject_oversized(socket) ⇒ void

This method returns an undefined value.

Writes a 413 response and closes the socket. Used when a request body exceeds the configured maximum size.

Parameters:

  • socket (TCPSocket)

    the client socket



677
678
679
680
# File 'lib/raptor/http1.rb', line 677

def reject_oversized(socket)
  socket.write(CONTENT_TOO_LARGE_RESPONSE) rescue nil
  socket.close rescue nil
end

#response_size(headers, body) ⇒ String

Returns the response body size as a String for the access log, taken from the content-length header when set, computed from the body otherwise, or - when the size cannot be determined upfront.

Parameters:

  • headers (Hash)

    the response headers

  • body (Object)

    the response body

Returns:

  • (String)


1341
1342
1343
# File 'lib/raptor/http1.rb', line 1341

def response_size(headers, body)
  headers[Rack::CONTENT_LENGTH] || calculate_content_length(body)&.to_s || "-"
end

#send_continue_if_expected(state, reactor) ⇒ Hash

Sends an HTTP 100 Continue response when an HTTP/1.1 client requested Expect: 100-continue and the request body has not yet been received.

Returns the state hash with :continued set when the response has been written. A write failure is silently ignored.

Parameters:

  • state (Hash)

    the partially-parsed connection state

  • reactor (Reactor)

    the reactor holding the connection's socket

Returns:

  • (Hash)

    the state, with :continued set if 100 was written



435
436
437
438
439
440
441
442
443
444
445
446
# File 'lib/raptor/http1.rb', line 435

def send_continue_if_expected(state, reactor)
  return state if state[:continued]

  env = state[:env]
  return state unless env && expects_100_continue?(env)

  socket = reactor.socket_for(state[:id])
  return state unless socket

  socket_write(socket, CONTINUE_RESPONSE) rescue nil
  state.merge(continued: true)
end

#send_early_hints(socket, hints) ⇒ void

This method returns an undefined value.

Sends an HTTP 103 Early Hints response to the client.

Skips any hints with illegal header keys or values. No-ops if hints is empty.

Parameters:

  • socket (TCPSocket)

    the client socket to write to

  • hints (Hash)

    header name to value (or array of values) pairs



828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
# File 'lib/raptor/http1.rb', line 828

def send_early_hints(socket, hints)
  return if hints.empty?

  response = +"#{HTTP_11} 103 Early Hints\r\n"
  hints.each do |key, value|
    next if illegal_header_key?(key)

    values = value.is_a?(Array) ? value : [value]
    values.each do |hint_value|
      next if illegal_header_value?(hint_value.to_s)

      response << "#{key.downcase}: #{hint_value}\r\n"
    end
  end
  response << "\r\n"

  socket_write(socket, response)
end

#shutdownvoid

This method returns an undefined value.

Signals eager keep-alive loops to stop processing further requests on their connections. In-flight requests complete normally.



204
205
206
# File 'lib/raptor/http1.rb', line 204

def shutdown
  @running.make_false
end

#socket_write(socket, string) ⇒ void

This method returns an undefined value.

Instance-level wrapper around Raptor::Http.socket_write that applies the configured write_timeout.

Parameters:

  • socket (TCPSocket)

    the socket to write to

  • string (String)

    the data to write

Raises:

  • (Http::WriteError)

    if the socket is not writable within the timeout or raises IOError



181
182
183
# File 'lib/raptor/http1.rb', line 181

def socket_write(socket, string)
  Http.socket_write(socket, string, timeout: @write_timeout)
end

#socket_writev(socket, strings) ⇒ void

This method returns an undefined value.

Instance-level wrapper around Raptor::Http.socket_writev that applies the configured write_timeout.

Parameters:

  • socket (TCPSocket)

    the socket to write to

  • strings (Array<String>)

    the buffers to write in order

Raises:

  • (Http::WriteError)

    if the socket is not writable within the timeout or raises IOError



194
195
196
# File 'lib/raptor/http1.rb', line 194

def socket_writev(socket, strings)
  Http.socket_writev(socket, strings, timeout: @write_timeout)
end

#uncork_socket(socket) ⇒ void

This method returns an undefined value.

Disables TCP_CORK on the socket, flushing any buffered packets.

Only applies to TCP sockets. No-op on non-TCP sockets. Available on Linux only; this method is not defined on other platforms.

Parameters:

  • socket (TCPSocket)

    the socket to uncork



1368
1369
1370
# File 'lib/raptor/http1.rb', line 1368

def uncork_socket(socket)
  socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_CORK, 0) if socket.is_a?(TCPSocket)
end

#validate_headers(headers, status) ⇒ void

This method returns an undefined value.

Validates that headers are appropriate for the given status code.

Raises if content-type or content-length are present for status codes that must not have an entity body (204, 304, 1xx).

Parameters:

  • headers (Hash)

    normalized response headers

  • status (Integer)

    HTTP status code

Raises:

  • (ArgumentError)

    if a forbidden header is present for the status



942
943
944
945
946
947
948
# File 'lib/raptor/http1.rb', line 942

def validate_headers(headers, status)
  if STATUS_WITH_NO_ENTITY_BODY.include?(status)
    raise ArgumentError, "content-type must not be present for status #{status}" if headers.key?(Rack::CONTENT_TYPE)

    raise ArgumentError, "content-length must not be present for status #{status}" if headers.key?(Rack::CONTENT_LENGTH)
  end
end

#validate_status(status) ⇒ void

This method returns an undefined value.

Validates that the status code is a valid integer.

Parameters:

  • status (Object)

    the status value to validate

Raises:

  • (TypeError)

    if status is not an Integer

  • (ArgumentError)

    if status is less than 100



897
898
899
900
901
# File 'lib/raptor/http1.rb', line 897

def validate_status(status)
  raise TypeError, "status must be an Integer" unless status.is_a?(Integer)

  raise ArgumentError, "status must be >= 100" unless status >= 100
end

#write_access_log(env, status, size, remote_addr) ⇒ void

This method returns an undefined value.

Instance-level wrapper around Raptor::Http.write_access_log that routes to the configured @access_log_io.

Parameters:

  • env (Hash)

    the Rack environment

  • status (Integer)

    the response status code

  • size (String)

    the response body size in bytes, or - if unknown

  • remote_addr (String)

    the client IP address



1328
1329
1330
# File 'lib/raptor/http1.rb', line 1328

def write_access_log(env, status, size, remote_addr)
  Http.write_access_log(@access_log_io, env, status, size, remote_addr)
end

#write_array_body(socket, response, body_array, use_chunked) ⇒ void

This method returns an undefined value.

Writes an array body to the socket.

Dispatches to the single-chunk or multi-chunk path based on array length.

Parameters:

  • socket (TCPSocket)

    the client socket

  • response (String)

    headers already serialized, to be written before the body

  • body_array (Array<String>)

    the response body chunks

  • use_chunked (Boolean)

    whether to use chunked transfer encoding



1134
1135
1136
1137
1138
1139
1140
# File 'lib/raptor/http1.rb', line 1134

def write_array_body(socket, response, body_array, use_chunked)
  if body_array.length == 1
    write_single_chunk(socket, response, body_array.first, use_chunked)
  else
    write_multiple_chunks(socket, response, body_array, use_chunked)
  end
end

#write_enumerable_body(socket, response, body, use_chunked) ⇒ void

This method returns an undefined value.

Writes a generic enumerable body to the socket.

Parameters:

  • socket (TCPSocket)

    the client socket

  • response (String)

    headers already serialized, to be written before the body

  • body (Object)

    any object responding to each

  • use_chunked (Boolean)

    whether to use chunked transfer encoding

Raises:

  • (TypeError)

    if any yielded chunk is not a String



1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
# File 'lib/raptor/http1.rb', line 1207

def write_enumerable_body(socket, response, body, use_chunked)
  if use_chunked
    socket_write(socket, response)
    body.each do |chunk|
      raise TypeError, "body must yield String values" unless chunk.is_a?(String)

      next if chunk.empty?

      socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
    end
    socket_write(socket, "0\r\n\r\n")
  else
    body.each do |chunk|
      raise TypeError, "body must yield String values" unless chunk.is_a?(String)

      response << chunk
    end
    socket_write(socket, response)
  end
end

#write_file_body(socket, response, path, content_length, use_chunked) ⇒ void

This method returns an undefined value.

Writes a file body to the socket.

Uses zero-copy IO.copy_stream for large files, direct buffering for small ones, and chunked encoding when required.

Parameters:

  • socket (TCPSocket)

    the client socket

  • response (String)

    headers already serialized, to be written before the body

  • path (String)

    filesystem path of the file to send

  • content_length (Integer, nil)

    pre-calculated file size

  • use_chunked (Boolean)

    whether to use chunked transfer encoding



1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
# File 'lib/raptor/http1.rb', line 1100

def write_file_body(socket, response, path, content_length, use_chunked)
  File.open(path, "rb") do |file|
    if use_chunked
      buffer = response
      while (chunk = file.read(FILE_CHUNK_SIZE))
        buffer << chunk.bytesize.to_s(16) << "\r\n" << chunk << "\r\n"
        if buffer.bytesize >= CHUNKED_WRITE_THRESHOLD
          socket_write(socket, buffer)
          buffer = +""
        end
      end
      buffer << "0\r\n\r\n"
      socket_write(socket, buffer)
    elsif content_length && content_length < BODY_BUFFER_THRESHOLD
      response << file.read(content_length)
      socket_write(socket, response)
    else
      socket_write(socket, response)
      IO.copy_stream(file, socket)
    end
  end
end

#write_full_response(socket, response, headers, body, http_version) ⇒ void

This method returns an undefined value.

Writes a complete response with a body.

Selects the appropriate write strategy based on body type: callable (streaming), file (zero-copy), array, or generic enumerable. Automatically determines content-length where possible, falling back to chunked transfer encoding for HTTP/1.1 when the length cannot be determined upfront.

Parameters:

  • socket (TCPSocket)

    the client socket

  • response (String)

    the status line accumulated so far

  • headers (Hash)

    normalized response headers

  • body (Object)

    the response body

  • http_version (String)

    "HTTP/1.1" or "HTTP/1.0"



1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
# File 'lib/raptor/http1.rb', line 1024

def write_full_response(socket, response, headers, body, http_version)
  if body.respond_to?(:call)
    response << format_headers(headers)
    response << "\r\n"
    socket_write(socket, response)
    uncork_socket(socket)
    body.call(socket)
    return
  end

  content_length = headers[Rack::CONTENT_LENGTH]&.to_i
  use_chunked = false

  if !content_length || content_length == 0
    calculated_length = calculate_content_length(body)
    if calculated_length
      content_length = calculated_length
    elsif http_version == HTTP_11 && !headers.key?(Rack::TRANSFER_ENCODING)
      use_chunked = true
    end
  end

  if content_length && content_length >= 0
    headers[Rack::CONTENT_LENGTH] = content_length.to_s
  elsif use_chunked
    headers[Rack::TRANSFER_ENCODING] = TRANSFER_ENCODING_CHUNKED
  end

  response << format_headers(headers)
  response << "\r\n"

  if body.respond_to?(:to_path) && (path = body.to_path) && File.readable?(path)
    write_file_body(socket, response, path, content_length, use_chunked)
  elsif body.respond_to?(:to_ary)
    write_array_body(socket, response, body.to_ary, use_chunked)
  elsif body.respond_to?(:each)
    write_enumerable_body(socket, response, body, use_chunked)
  else
    raise TypeError, "body must respond to each, to_ary, or to_path"
  end
end

#write_hijacked_response(socket, response, headers, response_hijack) ⇒ void

This method returns an undefined value.

Writes response headers and delegates body writing to the hijack callback.

Uncorks the socket before calling the hijack so the app has full control of the raw connection.

Parameters:

  • socket (TCPSocket)

    the client socket

  • response (String)

    the status line accumulated so far

  • headers (Hash)

    normalized response headers

  • response_hijack (Proc)

    callable that receives the socket and writes the body



978
979
980
981
982
983
984
# File 'lib/raptor/http1.rb', line 978

def write_hijacked_response(socket, response, headers, response_hijack)
  response << format_headers(headers)
  response << "\r\n"
  socket_write(socket, response)
  uncork_socket(socket)
  response_hijack.call(socket)
end

#write_multiple_chunks(socket, response, body_array, use_chunked) ⇒ void

This method returns an undefined value.

Writes a multi-element array body to the socket.

Parameters:

  • socket (TCPSocket)

    the client socket

  • response (String)

    headers already serialized, to be written before the body

  • body_array (Array<String>)

    the response body chunks

  • use_chunked (Boolean)

    whether to use chunked transfer encoding

Raises:

  • (TypeError)

    if any chunk is not a String



1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
# File 'lib/raptor/http1.rb', line 1173

def write_multiple_chunks(socket, response, body_array, use_chunked)
  if use_chunked
    buffer = response
    body_array.each do |chunk|
      raise TypeError, "body must yield String values" unless chunk.is_a?(String)

      next if chunk.empty?

      buffer << chunk.bytesize.to_s(16) << "\r\n" << chunk << "\r\n"
      if buffer.bytesize >= CHUNKED_WRITE_THRESHOLD
        socket_write(socket, buffer)
        buffer = +""
      end
    end
    buffer << "0\r\n\r\n"
    socket_write(socket, buffer)
  else
    body_array.each do |chunk|
      raise TypeError, "body must yield String values" unless chunk.is_a?(String)
    end
    socket_writev(socket, [response, *body_array])
  end
end

#write_no_body_response(socket, response, headers, status) ⇒ void

This method returns an undefined value.

Writes a response with no entity body.

Used for HEAD requests and status codes that must not carry a body (204, 304, 1xx). Adds a zero content-length for non-no-body statuses that did not supply one.

Parameters:

  • socket (TCPSocket)

    the client socket

  • response (String)

    the status line accumulated so far

  • headers (Hash)

    normalized response headers

  • status (Integer)

    HTTP status code



999
1000
1001
1002
1003
1004
1005
1006
1007
# File 'lib/raptor/http1.rb', line 999

def write_no_body_response(socket, response, headers, status)
  unless STATUS_WITH_NO_ENTITY_BODY.include?(status)
    headers[Rack::CONTENT_LENGTH] = "0" unless headers.key?(Rack::CONTENT_LENGTH) || headers.key?(Rack::TRANSFER_ENCODING)
  end

  response << format_headers(headers)
  response << "\r\n"
  socket_write(socket, response)
end

#write_response(socket, env, status, headers, body, keep_alive: false) ⇒ void

This method returns an undefined value.

Writes a complete HTTP response to the socket.

Handles header normalization, validation, connection management, TCP corking, and dispatches to the appropriate body write strategy.

Parameters:

  • socket (TCPSocket)

    the client socket to write to

  • env (Hash)

    the Rack environment

  • status (Integer)

    HTTP status code

  • headers (Hash)

    response headers from the Rack application

  • body (Object)

    response body (array, enumerable, file, or callable)

  • keep_alive (Boolean) (defaults to: false)

    whether to send a keep-alive connection header

  • keep_alive: (Boolean) (defaults to: false)


861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
# File 'lib/raptor/http1.rb', line 861

def write_response(socket, env, status, headers, body, keep_alive: false)
  validate_status(status)
  response_hijack = headers.is_a?(Hash) ? headers.delete(Rack::RACK_HIJACK) : nil
  headers = normalize_headers(headers)
  validate_headers(headers, status)

  headers["connection"] = keep_alive ? CONNECTION_KEEPALIVE : CONNECTION_CLOSE

  http_version = env[Rack::SERVER_PROTOCOL] == HTTP_11 ? HTTP_11 : HTTP_10
  no_body = env[Rack::REQUEST_METHOD] == "HEAD" || STATUS_WITH_NO_ENTITY_BODY.include?(status)

  response = build_status_line(http_version, status)

  cork_socket(socket)

  if response_hijack
    write_hijacked_response(socket, response, headers, response_hijack)
  elsif no_body
    write_no_body_response(socket, response, headers, status)
  else
    write_full_response(socket, response, headers, body, http_version)
  end
ensure
  body.close if body.respond_to?(:close)
  uncork_socket(socket)
  socket.flush rescue nil
end

#write_single_chunk(socket, response, chunk, use_chunked) ⇒ void

This method returns an undefined value.

Writes a single-element array body to the socket.

Parameters:

  • socket (TCPSocket)

    the client socket

  • response (String)

    headers already serialized, to be written before the body

  • chunk (String)

    the single body chunk

  • use_chunked (Boolean)

    whether to use chunked transfer encoding

Raises:

  • (TypeError)

    if the chunk is not a String



1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
# File 'lib/raptor/http1.rb', line 1152

def write_single_chunk(socket, response, chunk, use_chunked)
  raise TypeError, "body must yield String values" unless chunk.is_a?(String)

  if use_chunked
    response << chunk.bytesize.to_s(16) << "\r\n" << chunk << "\r\n0\r\n\r\n"
    socket_write(socket, response)
  else
    socket_writev(socket, [response, chunk])
  end
end