raptor 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/raptor/http1.rb CHANGED
@@ -155,6 +155,69 @@ module Raptor
155
155
  [decoded, :incomplete]
156
156
  end
157
157
 
158
+ # Advances an HTTP/1.x request parse from the state hash's buffered
159
+ # bytes. A complete well-formed request returns with `:complete` set
160
+ # plus populated `:env` and `:body`; malformed or oversized input flips
161
+ # `:malformed` or `:too_large`; incomplete input returns the state
162
+ # so the reactor can wait for more.
163
+ #
164
+ # @param data [Hash] the current parse state
165
+ # @param env_template [Hash] the Rack env template to seed the request with
166
+ # @param max_body_size [Integer, nil] byte limit for the request body, or nil for no limit
167
+ # @return [Hash] the updated parse state, made shareable for cross-Ractor return
168
+ #
169
+ # @rbs (Hash[Symbol, untyped] data, Hash[String, untyped] env_template, Integer? max_body_size) -> Hash[Symbol, untyped]
170
+ def self.parse(data, env_template, max_body_size)
171
+ parser = Raptor::HttpParser.new
172
+ env = env_template.dup
173
+ nread = begin
174
+ parser.execute(env, data[:buffer], 0)
175
+ rescue Raptor::HttpParserError
176
+ return Ractor.make_shareable(data.merge(complete: true, malformed: true))
177
+ end
178
+ parse_data = if data[:parse_data]
179
+ data[:parse_data].dup
180
+ else
181
+ { parse_count: 0, content_length: parser.content_length }
182
+ end
183
+ parse_data[:parse_count] += 1
184
+
185
+ message = if parser.finished?
186
+ if invalid_host?(env) || request_smuggling?(env)
187
+ data.merge(env: env, body: nil, parse_data: parse_data, complete: true, malformed: true)
188
+ elsif parser.has_body?
189
+ body_buffer = data[:buffer].byteslice(nread..-1) || ""
190
+
191
+ if max_body_size && parser.content_length > max_body_size
192
+ data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
193
+ elsif parser.chunked?
194
+ decoded_body, chunked_state = decode_chunked(body_buffer, max_body_size)
195
+
196
+ case chunked_state
197
+ when :complete
198
+ env.delete(HTTP_TRANSFER_ENCODING)
199
+ data.merge(env: env, body: decoded_body, parse_data: parse_data, complete: true)
200
+ when :too_large
201
+ data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
202
+ when :malformed
203
+ data.merge(env: env, body: nil, parse_data: parse_data, complete: true, malformed: true)
204
+ else
205
+ data.merge(env: env, parse_data: parse_data)
206
+ end
207
+ elsif parser.content_length > body_buffer.bytesize
208
+ data.merge(env: env, parse_data: parse_data)
209
+ else
210
+ data.merge(env: env, body: body_buffer, parse_data: parse_data, complete: true)
211
+ end
212
+ else
213
+ data.merge(env: env, body: nil, parse_data: parse_data, complete: true)
214
+ end
215
+ else
216
+ data.merge(env: env, parse_data: parse_data)
217
+ end
218
+ Ractor.make_shareable(message)
219
+ end
220
+
158
221
  # @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
159
222
  # @rbs @server_port: Integer
160
223
  # @rbs @server_port_string: String
@@ -202,6 +265,16 @@ module Raptor
202
265
  }.freeze
203
266
  end
204
267
 
268
+ # Instance-level wrapper around {Http.parser_worker} that binds this
269
+ # handler's env template and body-size limit into the worker proc.
270
+ #
271
+ # @return [Proc]
272
+ #
273
+ # @rbs () -> ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
274
+ def parser_worker
275
+ Http.parser_worker(@env_template, @max_body_size)
276
+ end
277
+
205
278
  # Instance-level wrapper around {Http.socket_write} that applies the
206
279
  # configured `write_timeout`.
207
280
  #
@@ -252,72 +325,45 @@ module Raptor
252
325
  #
253
326
  # @rbs (TCPSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
254
327
  def eager_accept(socket, id, reactor, thread_pool, remote_addr, url_scheme)
255
- buffer = (Thread.current[:raptor_read_buffer] ||= String.new(capacity: READ_BUFFER_SIZE))
256
-
257
328
  begin
258
- socket.read_nonblock(READ_BUFFER_SIZE, buffer)
329
+ buffer = read_into_thread_buffer(socket)
259
330
  rescue IO::WaitReadable
260
- reactor.add(
261
- id: id,
262
- socket: socket,
263
- remote_addr: remote_addr,
264
- url_scheme: url_scheme
265
- )
331
+ reactor.add(id: id, socket: socket, remote_addr: remote_addr, url_scheme: url_scheme)
266
332
  return
267
333
  rescue EOFError, IOError
268
334
  socket.close rescue nil
269
335
  return
270
336
  end
271
337
 
272
- while socket.respond_to?(:pending) && socket.pending > 0
273
- buffer << socket.read_nonblock(socket.pending)
274
- end
275
-
276
- parser = (Thread.current[:raptor_http_parser] ||= HttpParser.new)
277
- parser.reset
278
- env = @env_template.dup
279
- nread = begin
280
- parser.execute(env, buffer, 0)
338
+ env, parse_data, nread, parser = begin
339
+ parse_next_request(buffer)
281
340
  rescue HttpParserError
282
341
  reject_malformed(socket)
283
342
  return
284
343
  end
285
- parse_data = { parse_count: 1, content_length: parser.content_length }
286
344
 
287
- body = nil
288
345
  if !parser.finished?
289
346
  fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
290
347
  return
291
348
  elsif Http1.invalid_host?(env) || Http1.request_smuggling?(env)
292
349
  reject_malformed(socket)
293
350
  return
294
- elsif parser.has_body?
295
- if @max_body_size && parser.content_length > @max_body_size
296
- reject_oversized(socket)
297
- return
298
- end
351
+ elsif parser.has_body? && @max_body_size && parser.content_length > @max_body_size
352
+ reject_oversized(socket)
353
+ return
354
+ end
299
355
 
300
- body = buffer.byteslice(nread..-1) || ""
301
-
302
- if parser.chunked?
303
- body, chunked_state = Http1.decode_chunked(body, @max_body_size)
304
- case chunked_state
305
- when :complete
306
- env.delete(HTTP_TRANSFER_ENCODING)
307
- when :too_large
308
- reject_oversized(socket)
309
- return
310
- when :malformed
311
- reject_malformed(socket)
312
- return
313
- else
314
- fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
315
- return
316
- end
317
- elsif parser.content_length > body.bytesize
318
- fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
319
- return
320
- end
356
+ body = extract_body(buffer, env, parser, nread, decode_chunked: true)
357
+ case body
358
+ when :incomplete
359
+ fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
360
+ return
361
+ when :too_large
362
+ reject_oversized(socket)
363
+ return
364
+ when :malformed
365
+ reject_malformed(socket)
366
+ return
321
367
  end
322
368
 
323
369
  thread_pool << proc do
@@ -325,77 +371,8 @@ module Raptor
325
371
  end
326
372
  end
327
373
 
328
- # Returns a Proc for HTTP parsing work in Ractor context.
329
- #
330
- # The returned Proc processes raw socket data through the appropriate
331
- # HTTP parser and returns either a complete request state (ready for
332
- # app processing) or incomplete request state (needs more data).
333
- #
334
- # @return [Proc] a Ractor-safe proc that accepts a state hash and returns an updated state hash
335
- #
336
- # @rbs () -> ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
337
- def http_parser_worker
338
- max_body_size = @max_body_size
339
- env_template = @env_template
340
-
341
- proc do |data|
342
- next Raptor::Http2.process_frames(data) if data[:protocol] == :http2
343
-
344
- parser = Raptor::HttpParser.new
345
- env = env_template.dup
346
- nread = begin
347
- parser.execute(env, data[:buffer], 0)
348
- rescue Raptor::HttpParserError
349
- next Ractor.make_shareable(data.merge(complete: true, malformed: true))
350
- end
351
- parse_data = if data[:parse_data]
352
- data[:parse_data].dup
353
- else
354
- { parse_count: 0, content_length: parser.content_length }
355
- end
356
- parse_data[:parse_count] += 1
357
-
358
- message = if parser.finished?
359
- if Raptor::Http1.invalid_host?(env) || Raptor::Http1.request_smuggling?(env)
360
- data.merge(env: env, body: nil, parse_data: parse_data, complete: true, malformed: true)
361
- elsif parser.has_body?
362
- body_buffer = data[:buffer].byteslice(nread..-1) || ""
363
-
364
- if max_body_size && parser.content_length > max_body_size
365
- data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
366
- elsif parser.chunked?
367
- decoded_body, chunked_state = Raptor::Http1.decode_chunked(body_buffer, max_body_size)
368
-
369
- case chunked_state
370
- when :complete
371
- env.delete(HTTP_TRANSFER_ENCODING)
372
- data.merge(env: env, body: decoded_body, parse_data: parse_data, complete: true)
373
- when :too_large
374
- data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
375
- when :malformed
376
- data.merge(env: env, body: nil, parse_data: parse_data, complete: true, malformed: true)
377
- else
378
- data.merge(env: env, parse_data: parse_data)
379
- end
380
- elsif parser.content_length > body_buffer.bytesize
381
- data.merge(env: env, parse_data: parse_data)
382
- else
383
- data.merge(env: env, body: body_buffer, parse_data: parse_data, complete: true)
384
- end
385
- else
386
- data.merge(env: env, body: nil, parse_data: parse_data, complete: true)
387
- end
388
- else
389
- data.merge(env: env, parse_data: parse_data)
390
- end
391
- Ractor.make_shareable(message)
392
- end
393
- end
394
-
395
- # Handles a parsed HTTP request by either continuing parsing or dispatching to the Rack app.
396
- #
397
- # For incomplete requests, updates reactor state and re-registers for more I/O.
398
- # For complete requests, removes from reactor, builds Rack env, and dispatches to thread pool.
374
+ # Dispatches a parsed HTTP request to the thread pool when complete,
375
+ # or hands it back to the reactor for more I/O when incomplete.
399
376
  #
400
377
  # @param parsed_request [Hash] the parsed request state from the ractor pool
401
378
  # @param reactor [Reactor] the reactor managing the client connection
@@ -444,6 +421,79 @@ module Raptor
444
421
 
445
422
  private
446
423
 
424
+ # Reads pending bytes off `socket` into the thread-local read buffer,
425
+ # draining any additional SSL-buffered bytes so `pending` is empty on
426
+ # return. Raises `IO::WaitReadable`, `EOFError`, or `IOError` like the
427
+ # underlying `read_nonblock` does.
428
+ #
429
+ # @param socket [TCPSocket] the socket to read from
430
+ # @return [String] the thread-local buffer, freshly populated
431
+ #
432
+ # @rbs (TCPSocket socket) -> String
433
+ def read_into_thread_buffer(socket)
434
+ buffer = (Thread.current[:raptor_read_buffer] ||= String.new(capacity: READ_BUFFER_SIZE))
435
+ socket.read_nonblock(READ_BUFFER_SIZE, buffer)
436
+
437
+ while socket.respond_to?(:pending) && socket.pending > 0
438
+ buffer << socket.read_nonblock(socket.pending)
439
+ end
440
+
441
+ buffer
442
+ end
443
+
444
+ # Runs a fresh HTTP/1.x parse against `buffer`, returning
445
+ # `[env, parse_data, nread, parser]`. Raises `HttpParserError` on
446
+ # malformed input.
447
+ #
448
+ # @param buffer [String] the raw request bytes
449
+ # @return [Array(Hash, Hash, Integer, HttpParser)]
450
+ #
451
+ # @rbs (String buffer) -> [Hash[String, untyped], Hash[Symbol, untyped], Integer, HttpParser]
452
+ def parse_next_request(buffer)
453
+ parser = (Thread.current[:raptor_http_parser] ||= HttpParser.new)
454
+ parser.reset
455
+ env = @env_template.dup
456
+ nread = parser.execute(env, buffer, 0)
457
+ parse_data = { parse_count: 1, content_length: parser.content_length }
458
+ [env, parse_data, nread, parser]
459
+ end
460
+
461
+ # Resolves the request body for a finished parse. Returns the body
462
+ # `String` (or `nil` when the request has no body), or one of
463
+ # `:incomplete`, `:too_large`, `:malformed` when the caller must
464
+ # fall back or reject.
465
+ #
466
+ # @param buffer [String] the raw request bytes
467
+ # @param env [Hash] the Rack environment being built
468
+ # @param parser [HttpParser] the parser holding the finished parse state
469
+ # @param nread [Integer] the byte offset where the body begins in `buffer`
470
+ # @param decode_chunked [Boolean] whether to decode chunked bodies inline; when false chunked bodies signal `:incomplete`
471
+ # @return [String, nil, Symbol]
472
+ #
473
+ # @rbs (String buffer, Hash[String, untyped] env, HttpParser parser, Integer nread, decode_chunked: bool) -> (String | Symbol)?
474
+ def extract_body(buffer, env, parser, nread, decode_chunked:)
475
+ return nil unless parser.has_body?
476
+
477
+ body = buffer.byteslice(nread..-1) || ""
478
+
479
+ if parser.chunked?
480
+ return :incomplete unless decode_chunked
481
+
482
+ body, chunked_state = Http1.decode_chunked(body, @max_body_size)
483
+ case chunked_state
484
+ when :complete
485
+ env.delete(HTTP_TRANSFER_ENCODING)
486
+ body
487
+ else
488
+ chunked_state
489
+ end
490
+ elsif parser.content_length > body.bytesize
491
+ :incomplete
492
+ else
493
+ body
494
+ end
495
+ end
496
+
447
497
  # Returns true if the request expects a 100 Continue response per
448
498
  # RFC 7231 section 5.1.1.
449
499
  #
@@ -455,11 +505,9 @@ module Raptor
455
505
  (env[Rack::SERVER_PROTOCOL] == HTTP_11) && env[HTTP_EXPECT]&.casecmp?(EXPECT_100_CONTINUE)
456
506
  end
457
507
 
458
- # Sends an HTTP 100 Continue response when an HTTP/1.1 client requested
459
- # `Expect: 100-continue` and the request body has not yet been received.
460
- #
461
- # Returns the state hash with `:continued` set when the response has been
462
- # written. A write failure is silently ignored.
508
+ # Sends an HTTP 100 Continue response when the client requested
509
+ # `Expect: 100-continue`, returning the state hash with `:continued`
510
+ # set once written. A write failure is silently ignored.
463
511
  #
464
512
  # @param state [Hash] the partially-parsed connection state
465
513
  # @param reactor [Reactor] the reactor holding the connection's socket
@@ -500,9 +548,9 @@ module Raptor
500
548
  eager_keepalive(socket, id, reactor, thread_pool, request_count, remote_addr, url_scheme) if keep_alive
501
549
  end
502
550
 
503
- # Builds the Rack env, calls the application, and writes the response.
504
- # Returns true if the connection should be kept alive for further
505
- # requests, false otherwise (including hijack and error cases).
551
+ # Processes a single request. Builds the Rack env, calls the app,
552
+ # writes the response, and returns whether the connection stays open
553
+ # for another request.
506
554
  #
507
555
  # @param socket [TCPSocket] the client socket
508
556
  # @param env [Hash] partial env hash from the HTTP parser
@@ -542,15 +590,8 @@ module Raptor
542
590
  call_response_finished(rack_env, status, headers, nil)
543
591
  keep_alive && !hijacked
544
592
  rescue => error
545
- call_response_finished(rack_env, status, headers, error) if rack_env
546
- socket.write(INTERNAL_SERVER_ERROR_RESPONSE) rescue nil unless response_started || hijacked
547
593
  keep_alive = false
548
-
549
- if @on_error
550
- @on_error.call(rack_env, error) rescue nil
551
- else
552
- raise
553
- end
594
+ handle_app_error(socket, rack_env, status, headers, error, response_started: response_started, hijacked: hijacked)
554
595
  ensure
555
596
  rack_input = rack_env && rack_env[Rack::RACK_INPUT]
556
597
  rack_input.close! rescue nil if rack_input.respond_to?(:close!)
@@ -561,11 +602,35 @@ module Raptor
561
602
  end
562
603
  end
563
604
 
564
- # Attempts to read and process subsequent requests inline on a
565
- # kept-alive connection. Blocks briefly for the next request to avoid
566
- # a full reactor round-trip. Falls back to the reactor when no data
567
- # arrives within the timeout, when the thread pool has queued work
568
- # (deprioritization), or when the request is incomplete.
605
+ # Handles an exception raised while processing a request. Fires the
606
+ # `rack.response_finished` callbacks with the error, writes a 500
607
+ # response when no bytes have gone to the socket yet, and routes the
608
+ # exception through the configured `on_error` handler (or re-raises).
609
+ #
610
+ # @param socket [TCPSocket] the client socket
611
+ # @param rack_env [Hash, nil] the Rack environment, if it was built
612
+ # @param status [Integer, nil] the status returned by the app, if any
613
+ # @param headers [Hash, nil] the headers returned by the app, if any
614
+ # @param error [Exception] the exception raised
615
+ # @param response_started [Boolean] whether any response bytes have been written
616
+ # @param hijacked [Boolean] whether the app took over the socket
617
+ # @return [void]
618
+ #
619
+ # @rbs (TCPSocket socket, Hash[String, untyped]? rack_env, Integer? status, Hash[String, String | Array[String]]? headers, Exception error, response_started: bool, hijacked: bool) -> void
620
+ def handle_app_error(socket, rack_env, status, headers, error, response_started:, hijacked:)
621
+ call_response_finished(rack_env, status, headers, error) if rack_env
622
+ socket.write(INTERNAL_SERVER_ERROR_RESPONSE) rescue nil unless response_started || hijacked
623
+
624
+ if @on_error
625
+ @on_error.call(rack_env, error) rescue nil
626
+ else
627
+ raise error
628
+ end
629
+ end
630
+
631
+ # Reads and processes subsequent requests inline on a kept-alive
632
+ # connection. Falls back to the reactor when no data arrives within the
633
+ # timeout, the thread pool is saturated, or the request is incomplete.
569
634
  #
570
635
  # @param socket [TCPSocket] the client socket
571
636
  # @param id [Integer] unique client identifier
@@ -589,10 +654,8 @@ module Raptor
589
654
  return
590
655
  end
591
656
 
592
- buffer = (Thread.current[:raptor_read_buffer] ||= String.new(capacity: READ_BUFFER_SIZE))
593
-
594
657
  begin
595
- socket.read_nonblock(READ_BUFFER_SIZE, buffer)
658
+ buffer = read_into_thread_buffer(socket)
596
659
  rescue IO::WaitReadable
597
660
  reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
598
661
  return
@@ -601,32 +664,22 @@ module Raptor
601
664
  return
602
665
  end
603
666
 
604
- while socket.respond_to?(:pending) && socket.pending > 0
605
- buffer << socket.read_nonblock(socket.pending)
606
- end
607
-
608
- parser = (Thread.current[:raptor_http_parser] ||= HttpParser.new)
609
- parser.reset
610
- env = @env_template.dup
611
- nread = begin
612
- parser.execute(env, buffer, 0)
667
+ env, parse_data, nread, parser = begin
668
+ parse_next_request(buffer)
613
669
  rescue HttpParserError
614
670
  reject_malformed(socket)
615
671
  return
616
672
  end
617
- parse_data = { parse_count: 1, content_length: parser.content_length }
618
673
 
619
- body = nil
620
674
  if !parser.finished?
621
675
  fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme)
622
676
  return
623
- elsif parser.has_body?
624
- body = buffer.byteslice(nread..-1) || ""
677
+ end
625
678
 
626
- if parser.chunked? || parser.content_length > body.bytesize
627
- fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme)
628
- return
629
- end
679
+ body = extract_body(buffer, env, parser, nread, decode_chunked: false)
680
+ if body == :incomplete
681
+ fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme)
682
+ return
630
683
  end
631
684
 
632
685
  request_count += 1
@@ -663,11 +716,7 @@ module Raptor
663
716
  end
664
717
 
665
718
  # Re-registers a socket with the reactor for further processing when
666
- # an incomplete request is received during eager accept or eager keep-alive.
667
- #
668
- # The persisted flag selects between persistent_data_timeout (for
669
- # kept-alive connections awaiting the next request) and chunk_data_timeout
670
- # (for fresh connections awaiting the rest of the first request).
719
+ # an incomplete request is received on the fast path.
671
720
  #
672
721
  # @param socket [TCPSocket] the client socket
673
722
  # @param id [Integer] unique client identifier
@@ -701,8 +750,7 @@ module Raptor
701
750
  reactor.update_state(Ractor.make_shareable(state))
702
751
  end
703
752
 
704
- # Writes a 413 response and closes the socket. Used when a request body
705
- # exceeds the configured maximum size.
753
+ # Writes a 413 response and closes the socket.
706
754
  #
707
755
  # @param socket [TCPSocket] the client socket
708
756
  # @return [void]
@@ -713,8 +761,7 @@ module Raptor
713
761
  socket.close rescue nil
714
762
  end
715
763
 
716
- # Writes a 400 response and closes the socket. Used when the HTTP parser
717
- # rejects the request line or headers.
764
+ # Writes a 400 response and closes the socket.
718
765
  #
719
766
  # @param socket [TCPSocket] the client socket
720
767
  # @return [void]
@@ -727,9 +774,6 @@ module Raptor
727
774
 
728
775
  # Builds a Rack environment hash from parsed HTTP request data.
729
776
  #
730
- # Populates all required Rack env keys including rack.* keys, REMOTE_ADDR,
731
- # SERVER_NAME, SERVER_PORT, and hijack support.
732
- #
733
777
  # @param env [Hash] partial env hash from the HTTP parser
734
778
  # @param parse_data [Hash] metadata from the parsing pass, including content_length
735
779
  # @param body [String, nil] decoded request body, or nil if no body
@@ -842,11 +886,8 @@ module Raptor
842
886
  env["HTTP_X_FORWARDED_SSL"]&.casecmp?("on") || false
843
887
  end
844
888
 
845
- # Determines whether the connection should be kept alive after the response.
846
- #
847
- # Returns false if the request limit has been reached. For HTTP/1.1, keep-alive
848
- # is the default unless the client sent Connection: close. For HTTP/1.0,
849
- # keep-alive must be explicitly requested.
889
+ # Returns true when the connection should be kept alive after the
890
+ # current response.
850
891
  #
851
892
  # @param env [Hash] the Rack environment
852
893
  # @param request_count [Integer] number of requests handled on this connection
@@ -865,9 +906,8 @@ module Raptor
865
906
  end
866
907
  end
867
908
 
868
- # Sends an HTTP 103 Early Hints response to the client.
869
- #
870
- # Skips any hints with illegal header keys or values. No-ops if hints is empty.
909
+ # Sends an HTTP 103 Early Hints response, skipping any entries with
910
+ # illegal header keys or values. No-ops when `hints` is empty.
871
911
  #
872
912
  # @param socket [TCPSocket] the client socket to write to
873
913
  # @param hints [Hash] header name to value (or array of values) pairs
@@ -895,9 +935,6 @@ module Raptor
895
935
 
896
936
  # Writes a complete HTTP response to the socket.
897
937
  #
898
- # Handles header normalization, validation, connection management, TCP corking,
899
- # and dispatches to the appropriate body write strategy.
900
- #
901
938
  # @param socket [TCPSocket] the client socket to write to
902
939
  # @param env [Hash] the Rack environment
903
940
  # @param status [Integer] HTTP status code
@@ -949,10 +986,8 @@ module Raptor
949
986
  raise ArgumentError, "status must be >= 100" unless status >= 100
950
987
  end
951
988
 
952
- # Normalizes response headers by downcasing keys and filtering invalid entries.
953
- #
954
- # Removes headers with illegal keys, rack.* prefixed headers, and "status" headers.
955
- # Raises if headers is not a Hash or contains non-String keys.
989
+ # Returns a normalised copy of the response headers with lowercased
990
+ # keys and illegal/`rack.*`/`status` entries dropped.
956
991
  #
957
992
  # @param headers [Hash] raw headers from the Rack application
958
993
  # @return [Hash] normalized headers with lowercased string keys
@@ -977,10 +1012,8 @@ module Raptor
977
1012
  normalized
978
1013
  end
979
1014
 
980
- # Validates that headers are appropriate for the given status code.
981
- #
982
- # Raises if content-type or content-length are present for status codes
983
- # that must not have an entity body (204, 304, 1xx).
1015
+ # Raises when the headers include entries forbidden for the response
1016
+ # status (`content-type` or `content-length` on a 204, 304, or 1xx).
984
1017
  #
985
1018
  # @param headers [Hash] normalized response headers
986
1019
  # @param status [Integer] HTTP status code
@@ -996,8 +1029,7 @@ module Raptor
996
1029
  end
997
1030
  end
998
1031
 
999
- # Returns the HTTP status line for `status`. Callers must not retain the
1000
- # returned string across further calls on the same thread.
1032
+ # Returns the HTTP status line for `status`.
1001
1033
  #
1002
1034
  # @param http_version [String] "HTTP/1.1" or "HTTP/1.0"
1003
1035
  # @param status [Integer] HTTP status code
@@ -1012,10 +1044,8 @@ module Raptor
1012
1044
  response
1013
1045
  end
1014
1046
 
1015
- # Writes response headers and delegates body writing to the hijack callback.
1016
- #
1017
- # Uncorks the socket before calling the hijack so the app has full control
1018
- # of the raw connection.
1047
+ # Writes the response headers, uncorks the socket, and hands the raw
1048
+ # socket to the hijack callback.
1019
1049
  #
1020
1050
  # @param socket [TCPSocket] the client socket
1021
1051
  # @param response [String] the status line accumulated so far
@@ -1032,11 +1062,9 @@ module Raptor
1032
1062
  response_hijack.call(socket)
1033
1063
  end
1034
1064
 
1035
- # Writes a response with no entity body.
1036
- #
1037
- # Used for HEAD requests and status codes that must not carry a body
1038
- # (204, 304, 1xx). Adds a zero content-length for non-no-body statuses
1039
- # that did not supply one.
1065
+ # Writes a response with no entity body, adding a zero
1066
+ # `Content-Length` when the status may carry a body but none was
1067
+ # supplied.
1040
1068
  #
1041
1069
  # @param socket [TCPSocket] the client socket
1042
1070
  # @param response [String] the status line accumulated so far
@@ -1055,12 +1083,9 @@ module Raptor
1055
1083
  socket_write(socket, response)
1056
1084
  end
1057
1085
 
1058
- # Writes a complete response with a body.
1059
- #
1060
- # Selects the appropriate write strategy based on body type: callable (streaming),
1061
- # file (zero-copy), array, or generic enumerable. Automatically determines
1062
- # content-length where possible, falling back to chunked transfer encoding
1063
- # for HTTP/1.1 when the length cannot be determined upfront.
1086
+ # Writes a complete response with a body. Emits a `Content-Length`
1087
+ # when the total size is known upfront, otherwise chunked encoding on
1088
+ # HTTP/1.1.
1064
1089
  #
1065
1090
  # @param socket [TCPSocket] the client socket
1066
1091
  # @param response [String] the status line accumulated so far
@@ -1112,9 +1137,8 @@ module Raptor
1112
1137
  end
1113
1138
  end
1114
1139
 
1115
- # Calculates content length from an array or file body without consuming it.
1116
- #
1117
- # Returns nil for enumerable bodies whose length cannot be determined upfront.
1140
+ # Returns the byte length of the body when it can be determined
1141
+ # upfront (array or file), otherwise nil.
1118
1142
  #
1119
1143
  # @param body [Object] the response body
1120
1144
  # @return [Integer, nil] the byte length, or nil if it cannot be determined
@@ -1135,9 +1159,6 @@ module Raptor
1135
1159
 
1136
1160
  # Writes a file body to the socket.
1137
1161
  #
1138
- # Uses zero-copy IO.copy_stream for large files, direct buffering for small ones,
1139
- # and chunked encoding when required.
1140
- #
1141
1162
  # @param socket [TCPSocket] the client socket
1142
1163
  # @param response [String] headers already serialized, to be written before the body
1143
1164
  # @param path [String] filesystem path of the file to send
@@ -1171,8 +1192,6 @@ module Raptor
1171
1192
 
1172
1193
  # Writes an array body to the socket.
1173
1194
  #
1174
- # Dispatches to the single-chunk or multi-chunk path based on array length.
1175
- #
1176
1195
  # @param socket [TCPSocket] the client socket
1177
1196
  # @param response [String] headers already serialized, to be written before the body
1178
1197
  # @param body_array [Array<String>] the response body chunks
@@ -1294,10 +1313,8 @@ module Raptor
1294
1313
  value.match?(ILLEGAL_HEADER_VALUE_REGEX)
1295
1314
  end
1296
1315
 
1297
- # Formats a headers hash into an HTTP header string.
1298
- #
1299
- # Skips entries with illegal keys or values. Array values are written
1300
- # as separate header lines.
1316
+ # Appends normalised header lines to `result`. Skips entries with
1317
+ # illegal keys or values. Array values are written as separate lines.
1301
1318
  #
1302
1319
  # @param headers [Hash] normalized response headers
1303
1320
  # @return [String] formatted header lines, each ending with CRLF
@@ -1315,9 +1332,9 @@ module Raptor
1315
1332
  end
1316
1333
  end
1317
1334
 
1318
- # Appends one or more `name: value` header lines to `result`. Newline-
1319
- # separated values are emitted as separate lines; empty values and values
1320
- # with illegal characters are skipped silently.
1335
+ # Appends one or more `name: value` header lines to `result`, splitting
1336
+ # newline-joined values across separate lines and skipping empty or
1337
+ # illegal values.
1321
1338
  #
1322
1339
  # @param result [String] the buffer to append to
1323
1340
  # @param name [String] the header name
@@ -1342,10 +1359,8 @@ module Raptor
1342
1359
  end
1343
1360
  end
1344
1361
 
1345
- # Calls all rack.response_finished callbacks registered in the environment.
1346
- #
1347
- # Callbacks are called in reverse registration order. Individual callback
1348
- # failures are rescued so all callbacks are always attempted.
1362
+ # Calls every `rack.response_finished` callback in reverse
1363
+ # registration order, rescuing any that raise.
1349
1364
  #
1350
1365
  # @param env [Hash, nil] the Rack environment
1351
1366
  # @param status [Integer, nil] the response status code
@@ -1390,10 +1405,8 @@ module Raptor
1390
1405
  end
1391
1406
 
1392
1407
  if Socket.const_defined?(:TCP_CORK)
1393
- # Enables TCP_CORK on the socket to batch outgoing packets into fewer segments.
1394
- #
1395
- # Only applies to TCP sockets. No-op on non-TCP sockets.
1396
- # Available on Linux only; this method is not defined on other platforms.
1408
+ # Enables `TCP_CORK` on the socket to batch outgoing packets into
1409
+ # fewer segments. Linux-only; a no-op elsewhere.
1397
1410
  #
1398
1411
  # @param socket [TCPSocket] the socket to cork
1399
1412
  # @return [void]
@@ -1403,10 +1416,8 @@ module Raptor
1403
1416
  socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_CORK, 1) if socket.is_a?(TCPSocket)
1404
1417
  end
1405
1418
 
1406
- # Disables TCP_CORK on the socket, flushing any buffered packets.
1407
- #
1408
- # Only applies to TCP sockets. No-op on non-TCP sockets.
1409
- # Available on Linux only; this method is not defined on other platforms.
1419
+ # Disables `TCP_CORK` on the socket, flushing any buffered packets.
1420
+ # Linux-only; a no-op elsewhere.
1410
1421
  #
1411
1422
  # @param socket [TCPSocket] the socket to uncork
1412
1423
  # @return [void]