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
READ_BUFFER_SIZE =

Returns:

  • (Object)
64 * 1024
RESPONSE_BUFFER_CAPACITY =

Returns:

  • (Object)
4 * 1024
MAX_KEEPALIVE_REQUESTS =

Returns:

  • (::Integer)
1000
HTTP_10 =

Returns:

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

Returns:

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

Returns:

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

Returns:

  • (Object)
Hash.new do |cache, status|
  reason = Rack::Utils::HTTP_STATUS_CODES[status]
  cache[status] = "#{HTTP_11} #{status}#{reason ? " #{reason}" : ""}\r\n".freeze
end
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"
CHUNKED_TERMINATOR =

Returns:

  • (::String)
"0\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"
HTTP_X_FORWARDED_PROTO =

Returns:

  • (::String)
"HTTP_X_FORWARDED_PROTO"
HTTP_X_FORWARDED_SCHEME =

Returns:

  • (::String)
"HTTP_X_FORWARDED_SCHEME"
HTTP_X_FORWARDED_SSL =

Returns:

  • (::String)
"HTTP_X_FORWARDED_SSL"
RACK_HEADER_PREFIX =

Returns:

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

Returns:

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

Returns:

  • (::String)
"rack.hijack_io"

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, server_port, connection_options: {}, http1_options: {}, access_log_io: nil, clean_thread_locals: true, clean_fiber_locals: false, 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

  • clean_thread_locals (Boolean) (defaults to: true) —

    whether to clear application thread locals after each request

  • clean_fiber_locals (Boolean) (defaults to: false) —

    whether to process each request in a fresh Fiber

  • 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)
  • clean_thread_locals: (Boolean) (defaults to: true)
  • clean_fiber_locals: (Boolean) (defaults to: false)
  • 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



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/raptor/http1.rb', line 241

def initialize(app, server_port, connection_options: {}, http1_options: {}, access_log_io: nil, clean_thread_locals: true, clean_fiber_locals: false, on_error: nil)
  @app = app
  @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
  @clean_thread_locals = clean_thread_locals
  @clean_fiber_locals = clean_fiber_locals
  @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

.encode_chunk(chunk) ⇒ String

Encodes one HTTP/1.1 response chunk.

Parameters:

  • chunk (String) —

    response body bytes

Returns:

  • (String) —

    the encoded chunk



122
123
124
# File 'lib/raptor/http1.rb', line 122

def self.encode_chunk(chunk)
  HttpParser.chunked_encode(String.new, chunk)
end

.encode_trailers(trailers) ⇒ String

Encodes the final HTTP/1.1 response chunk and its trailers.

Parameters:

  • trailers (Hash) —

    trailing response headers

Returns:

  • (String) —

    the final chunk followed by the trailer section



132
133
134
135
136
137
138
139
140
141
142
# File 'lib/raptor/http1.rb', line 132

def self.encode_trailers(trailers)
  normalized = trailers.each_with_object({}) do |(key, value), headers|
    name = key.match?(/[A-Z]/) ? key.downcase : key
    next if name.start_with?(RACK_HEADER_PREFIX) || name == "status"

    headers[name] = value
  end
  response = +"0\r\n"
  HttpParser.format_headers(response, normalized)
  response << "\r\n"
end

.invalid_host?(env) ⇒ Boolean

Returns true when an HTTP/1.1 request lacks a valid Host header per RFC 9112 section 3.2, where a valid value is a non-empty single-value line.

Parameters:

  • env (Hash) —

    the Rack environment after header parsing

Returns:

  • (Boolean)


71
72
73
74
75
76
# File 'lib/raptor/http1.rb', line 71

def self.invalid_host?(env)
  return false unless env[Rack::SERVER_PROTOCOL] == HTTP_11

  http_host = env[Rack::HTTP_HOST]
  !http_host || http_host.empty? || http_host.include?(",")
end

.parse(data, env_template, max_body_size) ⇒ Hash

Advances an HTTP/1.x request parse from the state hash's buffered bytes. A complete well-formed request returns with :complete set plus populated :env and :body, and records any bytes after it in the parse data's :pipelined; malformed or oversized input flips :malformed or :too_large; incomplete input returns the state so the reactor can wait for more.

Parameters:

  • data (Hash) —

    the current parse state

  • env_template (Hash) —

    the Rack env template to seed the request with

  • max_body_size (Integer, nil) —

    byte limit for the request body, or nil for no limit

Returns:

  • (Hash) —

    the updated parse state, made shareable for cross-Ractor return



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/raptor/http1.rb', line 157

def self.parse(data, env_template, max_body_size)
  parser = HttpParser.new
  env = env_template.dup
  nread = begin
    parser.execute(env, data[:buffer], 0)
  rescue HttpParserError
    return 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 invalid_host?(env) || 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 parser.chunked?
        decoded_body, chunked_state, consumed = HttpParser.decode_chunked(body_buffer, max_body_size)

        case chunked_state
        when :complete
          env.delete(HTTP_TRANSFER_ENCODING)
          parse_data[:pipelined] = pipelined_bytes(body_buffer, consumed)
          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
        parse_data[:pipelined] = pipelined_bytes(body_buffer, parser.content_length)
        data.merge(env: env, body: body_buffer.byteslice(0, parser.content_length), parse_data: parse_data, complete: true)
      end
    else
      parse_data[:pipelined] = pipelined_bytes(data[:buffer], nread)
      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

.pipelined_bytes(buffer, offset) ⇒ String?

Returns any pipelined bytes following the request, or nil if none remain.

Parameters:

  • buffer (String) —

    the raw request bytes

  • offset (Integer) —

    the byte offset where the request ends

Returns:

  • (String, nil)


112
113
114
# File 'lib/raptor/http1.rb', line 112

def self.pipelined_bytes(buffer, offset)
  buffer.byteslice(offset..-1) if offset < buffer.bytesize
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)


88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/raptor/http1.rb', line 88

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

#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.

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) —

    the 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



823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
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
888
889
# File 'lib/raptor/http1.rb', line 823

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

  unless env.key?(Rack::PATH_INFO)
    request_uri = env[Http::REQUEST_URI]
    scheme_end = request_uri&.index("://")
    if scheme_end
      authority_end = request_uri.index("/", scheme_end + 3) || request_uri.bytesize
      path_and_query = request_uri.byteslice(authority_end..-1) || ""
      if (query_delim = path_and_query.index("?"))
        env[Rack::PATH_INFO] = query_delim.zero? ? "/" : path_and_query.byteslice(0, query_delim)
        env[Rack::QUERY_STRING] = path_and_query.byteslice(query_delim + 1..-1)
      else
        env[Rack::PATH_INFO] = path_and_query.empty? ? "/" : path_and_query
      end
    else
      env[Rack::PATH_INFO] = ""
    end
  end

  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



899
900
901
902
903
904
905
906
907
908
909
# File 'lib/raptor/http1.rb', line 899

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.

Parameters:

  • http_version (String) —

    "HTTP/1.1" or "HTTP/1.0"

  • status (Integer) —

    HTTP status code

Returns:

  • (String) —

    the status line including trailing CRLF



1102
1103
1104
1105
1106
1107
1108
# File 'lib/raptor/http1.rb', line 1102

def build_status_line(http_version, status)
  cache = http_version == HTTP_11 ? STATUS_LINE_CACHE_11 : STATUS_LINE_CACHE_10
  response = ThreadLocals.fetch(:raptor_response_buffer) { String.new(capacity: RESPONSE_BUFFER_CAPACITY) }
  response.clear
  response << cache[status]
  response
end

#calculate_content_length(body) ⇒ Integer?

Returns the byte length of the body when it can be determined upfront (array or file), otherwise nil.

Parameters:

  • body (Object) —

    the response body

Returns:

  • (Integer, nil) —

    the byte length, or nil if it cannot be determined



1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
# File 'lib/raptor/http1.rb', line 1210

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)
  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. Linux-only; a no-op elsewhere.

Parameters:

  • socket (TCPSocket) —

    the socket to cork



1415
1416
1417
# File 'lib/raptor/http1.rb', line 1415

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 connection identifier

  • reactor (Reactor) —

    the reactor for fallback registration

  • thread_pool (AtomicThreadPool) —

    thread pool for application processing

  • remote_addr (String) —

    the client IP address

  • url_scheme (String) —

    "http" or "https"



294
295
296
297
298
299
300
301
302
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
# File 'lib/raptor/http1.rb', line 294

def eager_accept(socket, id, reactor, thread_pool, remote_addr, url_scheme)
  begin
    buffer = read_into_thread_buffer(socket)
  rescue IO::WaitReadable
    reactor.add(id: id, socket: socket, remote_addr: remote_addr, url_scheme: url_scheme)
    return
  rescue EOFError, IOError, SystemCallError
    socket.close rescue nil
    return
  end

  env, parse_data, nread, parser = begin
    parse_next_request(buffer)
  rescue HttpParserError
    reject_malformed(socket)
    return
  end

  unless parser.finished?
    fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
    return
  end

  body = extract_body(buffer, env, parse_data, parser, nread)
  case body
  when :incomplete
    fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
    return
  when :too_large
    reject_oversized(socket)
    return
  when :malformed
    reject_malformed(socket)
    return
  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, resumed: false, pipelined: nil) ⇒ void

This method returns an undefined value.

Processes kept-alive requests on application threads, yielding to queued work and returning idle or incomplete connections to the reactor.

Parameters:

  • socket (TCPSocket) —

    the client socket

  • id (Integer) —

    unique connection identifier

  • reactor (Reactor) —

    the reactor for fallback registration

  • thread_pool (AtomicThreadPool) —

    thread pool for application processing

  • request_count (Integer) —

    number of requests handled on this connection

  • remote_addr (String) —

    the client IP address

  • url_scheme (String) —

    "http" or "https"

  • resumed (Boolean) (defaults to: false) —

    whether the reactor queued the connection once its next request became readable

  • pipelined (String, nil) (defaults to: nil) —

    bytes the client sent after the previous request

  • resumed: (Boolean) (defaults to: false)
  • pipelined: (String, nil) (defaults to: nil)


398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
# File 'lib/raptor/http1.rb', line 398

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

    if pipelined
      buffer = pipelined
    else
      unless resumed || socket.wait_readable(0)
        reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
        return
      end

      begin
        buffer = read_into_thread_buffer(socket)
      rescue IO::WaitReadable
        reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
        return
      rescue EOFError, IOError, SystemCallError
        socket.close rescue nil
        return
      end
    end

    env, parse_data, nread, parser = begin
      parse_next_request(buffer)
    rescue HttpParserError
      reject_malformed(socket)
      return
    end

    unless parser.finished?
      fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme)
      return
    end

    body = extract_body(buffer, env, parse_data, parser, nread)
    case body
    when :incomplete
      fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme)
      return
    when :too_large
      reject_oversized(socket)
      return
    when :malformed
      reject_malformed(socket)
      return
    end

    request_count += 1

    if !resumed && thread_pool.queue_size.positive?
      thread_pool << proc do
        process_client(
          socket,
          id,
          env,
          parse_data,
          body,
          reactor,
          thread_pool,
          request_count,
          remote_addr,
          url_scheme
        )
      end
      return
    end

    resumed = false
    pipelined = parse_data[:pipelined]
    keep_alive = process_request(
      socket,
      id,
      env,
      parse_data,
      body,
      reactor,
      request_count,
      remote_addr,
      url_scheme
    )
    return unless keep_alive
  end
end

#expects_100_continue?(env) ⇒ Boolean

Returns true when the request expects a 100 Continue response per RFC 9110 section 10.1.1.

Parameters:

  • env (Hash) —

    the parsed Rack environment (possibly incomplete)

Returns:

  • (Boolean)


568
569
570
571
572
# File 'lib/raptor/http1.rb', line 568

def expects_100_continue?(env)
  return false unless env[Rack::SERVER_PROTOCOL] == HTTP_11

  env[HTTP_EXPECT]&.casecmp?(EXPECT_100_CONTINUE) || false
end

#extract_body(buffer, env, parse_data, parser, nread) ⇒ String, ...

Validates request framing and extracts the body, preserving pipelined bytes.

Parameters:

  • buffer (String) —

    the raw request bytes

  • env (Hash) —

    the Rack environment being built

  • parse_data (Hash) —

    metadata from the parsing pass

  • parser (HttpParser) —

    the parser holding the finished parse state

  • nread (Integer) —

    the byte offset where the body begins in buffer

Returns:

  • (String, nil, Symbol) —

    body, nil when absent, or :incomplete, :too_large, or :malformed



536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
# File 'lib/raptor/http1.rb', line 536

def extract_body(buffer, env, parse_data, parser, nread)
  return :malformed if Http1.invalid_host?(env) || Http1.request_smuggling?(env)

  unless parser.has_body?
    parse_data[:pipelined] = Http1.pipelined_bytes(buffer, nread)
    return
  end

  return :too_large if @max_body_size && parser.content_length > @max_body_size

  if parser.chunked?
    body, chunked_state, consumed = HttpParser.decode_chunked(buffer.byteslice(nread..-1), @max_body_size)
    return chunked_state unless chunked_state == :complete

    env.delete(HTTP_TRANSFER_ENCODING)
    parse_data[:pipelined] = Http1.pipelined_bytes(buffer, nread + consumed)
    body
  elsif nread + parser.content_length > buffer.bytesize
    :incomplete
  else
    parse_data[:pipelined] = Http1.pipelined_bytes(buffer, nread + parser.content_length)
    buffer.byteslice(nread, parser.content_length)
  end
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 on the fast path.

Parameters:

  • socket (TCPSocket) —

    the client socket

  • id (Integer) —

    unique connection 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) —

    the 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)


771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
# File 'lib/raptor/http1.rb', line 771

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

#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)


919
920
921
922
923
924
# File 'lib/raptor/http1.rb', line 919

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_app_error(socket, rack_env, status, headers, error, response_started:, hijacked:) ⇒ void

This method returns an undefined value.

Handles an exception raised while processing a request. Fires the rack.response_finished callbacks with the error, writes a 500 response when no bytes have gone to the socket yet, and routes the exception through the configured on_error handler (or re-raises).

Parameters:

  • socket (TCPSocket) —

    the client socket

  • rack_env (Hash, nil) —

    the Rack environment, if it was built

  • status (Object, nil) —

    the status returned by the app, if any

  • headers (Hash, nil) —

    the headers returned by the app, if any

  • error (Exception) —

    the exception raised

  • response_started (Boolean) —

    whether any response bytes have been written

  • hijacked (Boolean) —

    whether the app took over the socket

  • response_started: (Boolean)
  • hijacked: (Boolean)


744
745
746
747
748
749
750
751
752
753
# File 'lib/raptor/http1.rb', line 744

def handle_app_error(socket, rack_env, status, headers, error, response_started:, hijacked:)
  Http.call_response_finished(rack_env, status, headers, error)
  socket.write(INTERNAL_SERVER_ERROR_RESPONSE) rescue nil unless response_started || hijacked

  if @on_error
    @on_error.call(rack_env, error) rescue nil
  else
    raise error
  end
end

#handle_parsed_request(parsed_request, reactor, thread_pool) ⇒ void

This method returns an undefined value.

Dispatches a parsed HTTP request to the thread pool when complete, or hands it back to the reactor for more I/O when incomplete.

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



344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# File 'lib/raptor/http1.rb', line 344

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

  if parsed_request[:complete]
    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
  else
    parsed_request = send_continue_if_expected(parsed_request, reactor)
    reactor.update_state(parsed_request)
  end
end

#keep_alive?(env, request_count) ⇒ Boolean

Returns true when the connection should be kept alive after the current response.

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



934
935
936
937
938
939
940
941
942
943
944
# File 'lib/raptor/http1.rb', line 934

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

Returns a normalized copy of the response headers with lowercased keys and illegal/rack.*/status entries dropped.

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



1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
# File 'lib/raptor/http1.rb', line 1060

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 HttpParser.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

#parse_next_request(buffer) ⇒ Array(Hash, Hash, Integer, HttpParser)

Runs a fresh HTTP/1.x parse against buffer.

Parameters:

  • buffer (String) —

    the raw request bytes

Returns:

  • (Array(Hash, Hash, Integer, HttpParser)) —

    the env, parse data, bytes read, and parser

Raises:



516
517
518
519
520
521
522
523
# File 'lib/raptor/http1.rb', line 516

def parse_next_request(buffer)
  parser = ThreadLocals.fetch(:raptor_http_parser) { HttpParser.new }
  parser.reset
  env = @env_template.dup
  nread = parser.execute(env, buffer, 0)
  parse_data = { parse_count: 1, content_length: parser.content_length }
  [env, parse_data, nread, parser]
end

#parser_worker ⇒ Proc

Returns a Ractor-safe proc that parses HTTP/1.x requests.

Returns:

  • (Proc)


267
268
269
# File 'lib/raptor/http1.rb', line 267

def parser_worker
  Http.parser_worker(@env_template, @max_body_size)
end

#perform_request(socket, id, env, parse_data, body, reactor, request_count, remote_addr, url_scheme) ⇒ Boolean

Calls the Rack app and writes its response for one request.

Parameters:

  • socket (TCPSocket) —

    the client socket

  • id (Integer) —

    unique connection 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

  • request_count (Integer) —

    number of requests handled on this connection

  • remote_addr (String) —

    the client IP address

  • url_scheme (String) —

    "http" or "https"

Returns:

  • (Boolean) —

    true if the connection should be kept alive



657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
# File 'lib/raptor/http1.rb', line 657

def perform_request(socket, id, env, parse_data, body, reactor, request_count, remote_addr, url_scheme)
  rack_env = nil
  status = nil
  headers = nil
  hijacked = false
  keep_alive = false
  detached = 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]
      no_body = rack_env[Rack::REQUEST_METHOD] == "HEAD" || Http.no_entity_body_status?(status)
      detached = body.is_a?(DetachedBody) && !no_body
      if detached
        chunked = rack_env[Rack::SERVER_PROTOCOL] == HTTP_11
        keep_alive = chunked && keep_alive?(rack_env, request_count)
        response_started = true
        write_detached_response(socket, status, headers, chunked: chunked, keep_alive: keep_alive)
        finished = proc do |reason|
          error = IOError.new("detached response closed") unless reason == :closed
          write_access_log(rack_env, status, "-", remote_addr) if @access_log_io
          Http.call_response_finished(rack_env, status, headers, error)
        end
        reactor.attach_http1_body(
          socket,
          id,
          body,
          {
            chunked: chunked,
            keep_alive: keep_alive,
            request_count: request_count,
            remote_addr: remote_addr,
            url_scheme: url_scheme,
            pipelined: parse_data[:pipelined]
          },
          finished
        )
      else
        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) if @access_log_io && !hijacked
        response_started = true
        write_response(socket, rack_env, status, headers, body, keep_alive: keep_alive)
        body.finish(:closed) if body.is_a?(DetachedBody)
      end
    end

    unless detached
      write_access_log(rack_env, status, response_size, remote_addr) if @access_log_io && !hijacked
      Http.call_response_finished(rack_env, status, headers, nil)
    end
    keep_alive && !hijacked && !detached
  rescue => error
    keep_alive = false
    handle_app_error(socket, rack_env, status, headers, error, response_started: response_started, hijacked: hijacked)
  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 || detached
      socket.close rescue nil
    end
  end
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 connection 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) —

    the client IP address

  • url_scheme (String) —

    "http" or "https"



612
613
614
615
# File 'lib/raptor/http1.rb', line 612

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

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

Processes a single request, isolating fiber and thread locals as configured, and returns whether the connection stays open for another request.

Parameters:

  • socket (TCPSocket) —

    the client socket

  • id (Integer) —

    unique connection 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

  • request_count (Integer) —

    number of requests handled on this connection

  • remote_addr (String) —

    the client IP address

  • url_scheme (String) —

    "http" or "https"

Returns:

  • (Boolean) —

    true if the connection should be kept alive



633
634
635
636
637
638
639
640
641
# File 'lib/raptor/http1.rb', line 633

def process_request(socket, id, env, parse_data, body, reactor, request_count, remote_addr, url_scheme)
  if @clean_fiber_locals
    Fiber.new { perform_request(socket, id, env, parse_data, body, reactor, request_count, remote_addr, url_scheme) }.resume
  else
    perform_request(socket, id, env, parse_data, body, reactor, request_count, remote_addr, url_scheme)
  end
ensure
  ThreadLocals.clear if @clean_thread_locals
end

#read_into_thread_buffer(socket) ⇒ String

Reads pending bytes off socket into the thread-local read buffer, draining any additional SSL-buffered bytes so pending is empty on return.

Parameters:

  • socket (TCPSocket) —

    the socket to read from

Returns:

  • (String) —

    the thread-local buffer, freshly populated

Raises:

  • (IO::WaitReadable) —

    if no bytes are ready to read

  • (EOFError, IOError, SystemCallError) —

    if the connection is closed or fails



498
499
500
501
502
503
504
505
506
507
# File 'lib/raptor/http1.rb', line 498

def read_into_thread_buffer(socket)
  buffer = ThreadLocals.fetch(:raptor_read_buffer) { String.new(capacity: READ_BUFFER_SIZE) }
  socket.read_nonblock(READ_BUFFER_SIZE, buffer)

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

  buffer
end

#reject_malformed(socket) ⇒ void

This method returns an undefined value.

Writes a 400 response and closes the socket.

Parameters:

  • socket (TCPSocket) —

    the client socket



807
808
809
810
# File 'lib/raptor/http1.rb', line 807

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.

Parameters:

  • socket (TCPSocket) —

    the client socket



796
797
798
799
# File 'lib/raptor/http1.rb', line 796

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)


1403
1404
1405
# File 'lib/raptor/http1.rb', line 1403

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 the client requested Expect: 100-continue, returning the state hash with :continued set once 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



583
584
585
586
587
588
589
590
591
592
593
594
# File 'lib/raptor/http1.rb', line 583

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, skipping any entries with illegal header keys or values. No-ops when hints is empty.

Parameters:

  • socket (TCPSocket) —

    the client socket to write to

  • hints (Hash) —

    header name to value (or array of values) pairs



954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
# File 'lib/raptor/http1.rb', line 954

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 HttpParser.illegal_header_key?(key)

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

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

  socket_write(socket, response)
end

#shutdown ⇒ void

This method returns an undefined value.

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



277
278
279
# File 'lib/raptor/http1.rb', line 277

def shutdown
  @running.make_false
end

#socket_write(socket, string) ⇒ void

This method returns an undefined value.

Writes response bytes within 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



1365
1366
1367
# File 'lib/raptor/http1.rb', line 1365

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.

Writes response buffers in order within 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



1377
1378
1379
# File 'lib/raptor/http1.rb', line 1377

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. Linux-only; a no-op elsewhere.

Parameters:

  • socket (TCPSocket) —

    the socket to uncork



1426
1427
1428
# File 'lib/raptor/http1.rb', line 1426

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

#validate_headers(headers, status, no_entity_body) ⇒ void

This method returns an undefined value.

Raises when the headers include entries forbidden for the response status (content-type or content-length on a 204, 304, or 1xx).

Parameters:

  • headers (Hash) —

    normalized response headers

  • status (Integer) —

    HTTP status code

  • no_entity_body (Boolean) —

    whether the status forbids an entity body

Raises:

  • (ArgumentError) —

    if a forbidden header is present for the status



1088
1089
1090
1091
1092
1093
# File 'lib/raptor/http1.rb', line 1088

def validate_headers(headers, status, no_entity_body)
  if no_entity_body
    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



1047
1048
1049
1050
# File 'lib/raptor/http1.rb', line 1047

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.

Writes a completed request to the access log.

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



1390
1391
1392
# File 'lib/raptor/http1.rb', line 1390

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.

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



1263
1264
1265
1266
1267
1268
1269
# File 'lib/raptor/http1.rb', line 1263

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_detached_response(socket, status, headers, chunked:, keep_alive:) ⇒ void

This method returns an undefined value.

Starts a detached response, chunked on HTTP/1.1 and ended by closing the connection on HTTP/1.0.

Parameters:

  • socket (TCPSocket) —

    the client socket to write to

  • status (Integer) —

    HTTP status code

  • headers (Hash) —

    response headers from the Rack application

  • chunked (Boolean) —

    whether to use chunked transfer encoding

  • keep_alive (Boolean) —

    whether to send a keep-alive connection header

  • chunked: (Boolean)
  • keep_alive: (Boolean)


984
985
986
987
988
989
990
991
992
993
994
995
996
# File 'lib/raptor/http1.rb', line 984

def write_detached_response(socket, status, headers, chunked:, keep_alive:)
  validate_status(status)
  headers = normalize_headers(headers)
  validate_headers(headers, status, false)
  headers.delete(Rack::CONTENT_LENGTH)
  headers[Rack::TRANSFER_ENCODING] = TRANSFER_ENCODING_CHUNKED if chunked
  headers["connection"] = keep_alive ? CONNECTION_KEEPALIVE : CONNECTION_CLOSE

  response = build_status_line(chunked ? HTTP_11 : HTTP_10, status)
  HttpParser.format_headers(response, headers)
  response << "\r\n"
  socket_write(socket, response)
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



1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
# File 'lib/raptor/http1.rb', line 1335

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

      buffer.clear
      HttpParser.chunked_encode(buffer, chunk)
      socket_write(socket, buffer) unless buffer.empty?
    end
    socket_write(socket, CHUNKED_TERMINATOR)
  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.

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



1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
# File 'lib/raptor/http1.rb', line 1231

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))
        HttpParser.chunked_encode(buffer, chunk)
        if buffer.bytesize >= CHUNKED_WRITE_THRESHOLD
          socket_write(socket, buffer)
          buffer = +""
        end
      end
      buffer << CHUNKED_TERMINATOR
      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. Emits a Content-Length when the total size is known upfront, otherwise chunked encoding on HTTP/1.1.

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"

Raises:

  • (TypeError) —

    if the body responds to none of each, to_ary, or to_path



1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
# File 'lib/raptor/http1.rb', line 1161

def write_full_response(socket, response, headers, body, http_version)
  if body.respond_to?(:call)
    HttpParser.format_headers(response, 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.zero?
    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

  HttpParser.format_headers(response, 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 the response headers and hands the raw socket to the hijack callback.

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



1120
1121
1122
1123
1124
1125
# File 'lib/raptor/http1.rb', line 1120

def write_hijacked_response(socket, response, headers, response_hijack)
  HttpParser.format_headers(response, headers)
  response << "\r\n"
  socket_write(socket, response)
  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



1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
# File 'lib/raptor/http1.rb', line 1303

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)

      HttpParser.chunked_encode(buffer, chunk)
      if buffer.bytesize >= CHUNKED_WRITE_THRESHOLD
        socket_write(socket, buffer)
        buffer = +""
      end
    end
    buffer << CHUNKED_TERMINATOR
    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, no_entity_body) ⇒ void

This method returns an undefined value.

Writes a response with no entity body, adding a zero Content-Length when the status may carry a body but none was supplied.

Parameters:

  • socket (TCPSocket) —

    the client socket

  • response (String) —

    the status line accumulated so far

  • headers (Hash) —

    normalized response headers

  • no_entity_body (Boolean) —

    whether the response status forbids an entity body



1138
1139
1140
1141
1142
1143
1144
1145
1146
# File 'lib/raptor/http1.rb', line 1138

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

  HttpParser.format_headers(response, 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.

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)


1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
# File 'lib/raptor/http1.rb', line 1009

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)
  no_entity_body = Http.no_entity_body_status?(status)
  validate_headers(headers, status, no_entity_body)

  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" || no_entity_body

  response = build_status_line(http_version, status)

  corked = !keep_alive && !body.respond_to?(:to_ary) && !no_body && !response_hijack
  cork_socket(socket) if corked

  if response_hijack
    write_hijacked_response(socket, response, headers, response_hijack)
  elsif no_body
    write_no_body_response(socket, response, headers, no_entity_body)
  else
    write_full_response(socket, response, headers, body, http_version)
  end
ensure
  body.close if body.respond_to?(:close)
  uncork_socket(socket) if corked
  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



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

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

  if use_chunked
    HttpParser.chunked_encode(response, chunk)
    response << CHUNKED_TERMINATOR
    socket_write(socket, response)
  else
    socket_writev(socket, [response, chunk])
  end
end