raptor 0.22.2 → 0.23.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/Dockerfile +3 -1
  4. data/README.md +38 -31
  5. data/docs/raptor-vs-puma.md +44 -38
  6. data/docs/talk.md +1295 -0
  7. data/ext/raptor_bpf/reuseport_select.bpf.c +18 -9
  8. data/ext/raptor_http/extconf.rb +2 -0
  9. data/ext/raptor_http/raptor_http.c +91 -119
  10. data/ext/raptor_http2/raptor_http2.c +272 -223
  11. data/ext/raptor_native/raptor_native.c +38 -20
  12. data/lib/rackup/handler/raptor.rb +12 -11
  13. data/lib/raptor/binder.rb +12 -12
  14. data/lib/raptor/cli.rb +18 -18
  15. data/lib/raptor/cluster.rb +44 -44
  16. data/lib/raptor/control_server.rb +3 -2
  17. data/lib/raptor/detached_body.rb +6 -2
  18. data/lib/raptor/http.rb +7 -5
  19. data/lib/raptor/http1.rb +233 -208
  20. data/lib/raptor/http2.rb +314 -267
  21. data/lib/raptor/reactor.rb +139 -149
  22. data/lib/raptor/reuseport_bpf.rb +62 -38
  23. data/lib/raptor/server.rb +53 -38
  24. data/lib/raptor/stats.rb +15 -3
  25. data/lib/raptor/thread_locals.rb +4 -3
  26. data/lib/raptor/version.rb +1 -1
  27. data/sig/generated/rackup/handler/raptor.rbs +2 -1
  28. data/sig/generated/raptor/binder.rbs +9 -9
  29. data/sig/generated/raptor/cli.rbs +6 -6
  30. data/sig/generated/raptor/cluster.rbs +9 -9
  31. data/sig/generated/raptor/control_server.rbs +2 -1
  32. data/sig/generated/raptor/detached_body.rbs +5 -1
  33. data/sig/generated/raptor/http.rbs +3 -3
  34. data/sig/generated/raptor/http1.rbs +100 -91
  35. data/sig/generated/raptor/http2.rbs +200 -189
  36. data/sig/generated/raptor/reactor.rbs +107 -118
  37. data/sig/generated/raptor/reuseport_bpf.rbs +54 -17
  38. data/sig/generated/raptor/server.rbs +26 -26
  39. data/sig/generated/raptor/stats.rbs +3 -2
  40. data/sig/generated/raptor/thread_locals.rbs +4 -3
  41. metadata +2 -2
  42. data/docs/brisrails-talk.md +0 -2398
data/lib/raptor/http2.rb CHANGED
@@ -10,14 +10,44 @@ require "rack"
10
10
 
11
11
  require_relative "detached_body"
12
12
  require_relative "http"
13
+ require_relative "log"
13
14
  require_relative "raptor_http2"
15
+ require_relative "server"
14
16
  require_relative "thread_locals"
15
17
 
16
18
  module Raptor
17
19
  # Handles HTTP/2 request processing and Rack application integration.
18
20
  #
19
21
  class Http2
22
+ EAGER_READ_BUFFER_SIZE = 64 * 1024
23
+ EAGER_MAX_ROUNDS = 8
24
+
25
+ FLAG_END_STREAM = 0x1
26
+ FLAG_ACK = 0x1
27
+ FLAG_END_HEADERS = 0x4
28
+ FLAG_PADDED = 0x8
29
+ FLAG_PRIORITY = 0x20
30
+
31
+ ERROR_NO_ERROR = 0x0
32
+ ERROR_PROTOCOL_ERROR = 0x1
33
+ ERROR_INTERNAL_ERROR = 0x2
34
+ ERROR_FRAME_SIZE_ERROR = 0x6
35
+ ERROR_REFUSED_STREAM = 0x7
36
+ ERROR_COMPRESSION_ERROR = 0x9
37
+
38
+ DEFAULT_WINDOW_SIZE = 65_535
39
+ MAX_FRAME_SIZE = 16_384
40
+
20
41
  RESPONSE_TRAILERS = "raptor.response_trailers"
42
+ SERVER_PROTOCOL = "HTTP/2"
43
+
44
+ REQUEST_PSEUDO_HEADERS = [":method", ":scheme", ":path", ":authority"].freeze
45
+ REQUIRED_REQUEST_PSEUDO_HEADERS = [":method", ":scheme", ":path"].freeze
46
+
47
+ class StreamClosedError < StandardError
48
+ # @rbs () -> String
49
+ def message = "HTTP/2 stream closed"
50
+ end
21
51
 
22
52
  # Queues concurrent frame writes while the reactor remains the sole
23
53
  # owner of the connection socket.
@@ -54,11 +84,11 @@ module Raptor
54
84
  # Queues frames for the reactor to write. Before attachment, writes
55
85
  # directly so the writer remains usable during connection setup.
56
86
  #
57
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
87
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
58
88
  # @param frames [Array<String>, nil] frame bytes to write in order
59
89
  # @return [void]
60
90
  #
61
- # @rbs (OpenSSL::SSL::SSLSocket socket, Array[String]? frames) -> void
91
+ # @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Array[String]? frames) -> void
62
92
  def write_frames(socket, frames)
63
93
  return if !frames || frames.empty?
64
94
 
@@ -82,11 +112,6 @@ module Raptor
82
112
  end
83
113
  end
84
114
 
85
- class StreamClosedError < StandardError
86
- # @rbs () -> String
87
- def message = "HTTP/2 stream closed"
88
- end
89
-
90
115
  # Coordinates outbound flow control and stream cancellation without
91
116
  # polling or mutexes.
92
117
  #
@@ -112,21 +137,22 @@ module Raptor
112
137
 
113
138
  # Reserves outbound capacity on the given stream, waiting until at
114
139
  # least one byte is available on both the connection and stream
115
- # windows. Raises when the stream or connection closes.
140
+ # windows.
116
141
  #
117
142
  # @param stream_id [Integer] the HTTP/2 stream identifier
118
143
  # @param max_bytes [Integer] the largest size the caller would like to send
119
144
  # @return [Integer] the number of bytes the caller may now send
145
+ # @raise [StreamClosedError] if the stream or connection has closed
120
146
  #
121
147
  # @rbs (Integer stream_id, Integer max_bytes) -> Integer
122
148
  def acquire(stream_id, max_bytes)
123
- initial = @initial_stream_window.value
124
- capped = max_bytes < MAX_FRAME_SIZE ? max_bytes : MAX_FRAME_SIZE
149
+ capped = [max_bytes, MAX_FRAME_SIZE].min
125
150
 
126
151
  @available.wait do
127
152
  check(stream_id)
153
+ initial = @initial_stream_window.value
128
154
  stream_window = @stream_windows.value[stream_id] || initial
129
- capped_full = capped < stream_window ? capped : stream_window
155
+ capped_full = [capped, stream_window].min
130
156
  granted = capped_full.positive? ? reserve_connection(capped_full) : 0
131
157
 
132
158
  if granted.positive?
@@ -144,6 +170,7 @@ module Raptor
144
170
  # @param stream_id [Integer] the HTTP/2 stream identifier
145
171
  # @param max_bytes [Integer] the largest size the caller would like to send
146
172
  # @return [Integer] available bytes, or 0 when flow control is exhausted
173
+ # @raise [StreamClosedError] if the stream or connection has closed
147
174
  #
148
175
  # @rbs (Integer stream_id, Integer max_bytes) -> Integer
149
176
  def try_acquire(stream_id, max_bytes)
@@ -191,7 +218,7 @@ module Raptor
191
218
  end
192
219
 
193
220
  # Updates the peer's `SETTINGS_INITIAL_WINDOW_SIZE`. Shifts every
194
- # existing stream window by the delta as required by RFC 7540 section 6.9.2.
221
+ # existing stream window by the delta as required by RFC 9113 section 6.9.2.
195
222
  #
196
223
  # @param new_size [Integer] the peer's new initial window size
197
224
  # @return [void]
@@ -267,18 +294,16 @@ module Raptor
267
294
 
268
295
  private
269
296
 
270
- # Reserves up to `capped` bytes from the connection window,
271
- # returning the number of bytes granted (0 when the window is
272
- # exhausted).
297
+ # Reserves up to `capped` bytes from the connection window.
273
298
  #
274
299
  # @param capped [Integer] the largest reservation the caller will accept
275
- # @return [Integer]
300
+ # @return [Integer] bytes granted, or 0 when the window is exhausted
276
301
  #
277
302
  # @rbs (Integer capped) -> Integer
278
303
  def reserve_connection(capped)
279
304
  granted = 0
280
305
  @connection_window.swap do |window|
281
- granted = window > capped ? capped : window
306
+ granted = [window, capped].min
282
307
  granted.positive? ? window - granted : window
283
308
  end
284
309
  granted
@@ -288,7 +313,7 @@ module Raptor
288
313
  # Adapts one HTTP/2 response stream to Rack's streaming body interface.
289
314
  #
290
315
  class ResponseStream
291
- # @rbs @socket: OpenSSL::SSL::SSLSocket
316
+ # @rbs @socket: TCPSocket | OpenSSL::SSL::SSLSocket
292
317
  # @rbs @writer: Writer
293
318
  # @rbs @flow_control: FlowControl
294
319
  # @rbs @stream_id: Integer
@@ -304,14 +329,14 @@ module Raptor
304
329
 
305
330
  # Creates a response stream for a Rack streaming body.
306
331
  #
307
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
332
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
308
333
  # @param writer [Writer] frame writer for the connection
309
- # @param flow_control [FlowControl] outbound flow controller
334
+ # @param flow_control [FlowControl] per-connection outbound flow controller
310
335
  # @param stream_id [Integer] the HTTP/2 stream identifier
311
336
  # @param trailers [Hash] trailing response headers populated by the Rack application
312
337
  # @return [void]
313
338
  #
314
- # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Hash[String, String | Array[String]] trailers) -> void
339
+ # @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Hash[String, String | Array[String]] trailers) -> void
315
340
  def initialize(socket, writer, flow_control, stream_id, trailers)
316
341
  @socket = socket
317
342
  @writer = writer
@@ -323,9 +348,9 @@ module Raptor
323
348
  @closed = false
324
349
  end
325
350
 
326
- # Streaming responses are write-only.
351
+ # Rejects reads; streaming responses are write-only.
327
352
  #
328
- # @raise [IOError]
353
+ # @raise [IOError] always
329
354
  #
330
355
  # @rbs (*untyped) -> bot
331
356
  def read(*)
@@ -336,6 +361,9 @@ module Raptor
336
361
  #
337
362
  # @param chunk [String] response body bytes
338
363
  # @return [Integer] number of bytes written
364
+ # @raise [IOError] if the stream has already closed
365
+ # @raise [TypeError] if the chunk is not a String
366
+ # @raise [StreamClosedError] if the stream or connection has closed
339
367
  #
340
368
  # @rbs (String chunk) -> Integer
341
369
  def write(chunk)
@@ -365,7 +393,8 @@ module Raptor
365
393
  self
366
394
  end
367
395
 
368
- # Flushes the stream. Frame writes are already immediate.
396
+ # Flushes the stream. Frames are already handed to the writer as they
397
+ # are produced.
369
398
  #
370
399
  # @return [ResponseStream]
371
400
  #
@@ -377,6 +406,7 @@ module Raptor
377
406
  # Finishes the stream.
378
407
  #
379
408
  # @return [void]
409
+ # @raise [StreamClosedError] if the stream or connection has closed
380
410
  #
381
411
  # @rbs () -> void
382
412
  def close
@@ -395,7 +425,7 @@ module Raptor
395
425
 
396
426
  # Closes the unsupported read side of the stream.
397
427
  #
398
- # @raise [IOError]
428
+ # @raise [IOError] always
399
429
  #
400
430
  # @rbs () -> bot
401
431
  def close_read
@@ -421,143 +451,6 @@ module Raptor
421
451
  end
422
452
  end
423
453
 
424
- EAGER_READ_BUFFER_SIZE = 64 * 1024
425
- EAGER_MAX_ROUNDS = 8
426
-
427
- FLAG_END_STREAM = 0x1
428
- FLAG_END_HEADERS = 0x4
429
- FLAG_ACK = 0x1
430
- FLAG_PRIORITY = 0x20
431
-
432
- ERROR_NO_ERROR = 0x0
433
- ERROR_PROTOCOL_ERROR = 0x1
434
- ERROR_INTERNAL_ERROR = 0x2
435
- ERROR_REFUSED_STREAM = 0x7
436
-
437
- DEFAULT_WINDOW_SIZE = 65_535
438
- MAX_FRAME_SIZE = 16_384
439
-
440
- SERVER_PROTOCOL = "HTTP/2"
441
-
442
- REQUEST_PSEUDO_HEADERS = [":method", ":scheme", ":path", ":authority"].freeze
443
- REQUIRED_REQUEST_PSEUDO_HEADERS = [":method", ":scheme", ":path"].freeze
444
-
445
- # Returns true when a decoded header block violates the HTTP/2 pseudo-header
446
- # rules from RFC 9113 section 8.3: an unknown pseudo-header, a duplicate
447
- # pseudo-header, a pseudo-header appearing after any regular header, or a
448
- # required pseudo-header (`:method`, `:scheme`, `:path`) missing on
449
- # non-`CONNECT` requests.
450
- #
451
- # @param headers [Array<Array(String, String)>] decoded header pairs
452
- # @return [Boolean]
453
- #
454
- # @rbs (Array[[String, String]] headers) -> bool
455
- def self.invalid_pseudo_headers?(headers)
456
- seen_pseudo = {}
457
- seen_regular = false
458
-
459
- headers.each do |name, _value|
460
- if name.start_with?(":")
461
- return true if seen_regular
462
- return true unless REQUEST_PSEUDO_HEADERS.include?(name)
463
- return true if seen_pseudo[name]
464
- seen_pseudo[name] = true
465
- else
466
- seen_regular = true
467
- end
468
- end
469
-
470
- return false if seen_pseudo[":method"] && headers.assoc(":method")&.last == "CONNECT"
471
-
472
- REQUIRED_REQUEST_PSEUDO_HEADERS.any? { |name| !seen_pseudo[name] }
473
- end
474
-
475
- # @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
476
- # @rbs @server_port: Integer
477
- # @rbs @write_timeout: Integer
478
- # @rbs @access_log_io: IO?
479
- # @rbs @clean_thread_locals: bool
480
- # @rbs @clean_fiber_locals: bool
481
- # @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
482
- # @rbs @initial_settings_frame: String
483
- # @rbs @running: AtomicBoolean
484
-
485
- # Returns the initial server SETTINGS frame to send on every new
486
- # HTTP/2 connection.
487
- #
488
- # @return [String]
489
- attr_reader :initial_settings_frame
490
-
491
- # Creates a new Http2 handler.
492
- #
493
- # @param app [#call] the Rack application to dispatch requests to
494
- # @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
495
- # @param connection_options [Hash] per-connection settings shared across protocols
496
- # @option connection_options [Integer] :write_timeout per-write socket timeout in seconds
497
- # @param http2_options [Hash] HTTP/2-specific settings
498
- # @option http2_options [Integer] :max_concurrent_streams maximum HTTP/2 concurrent streams per connection
499
- # @param access_log_io [IO, nil] IO to write Common Log Format access entries to, or nil to disable
500
- # @param clean_thread_locals [Boolean] whether to clear application thread locals after each request
501
- # @param clean_fiber_locals [Boolean] whether to process each request in a fresh Fiber
502
- # @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
503
- # @return [void]
504
- #
505
- # @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?connection_options: Hash[Symbol, untyped], ?http2_options: Hash[Symbol, untyped], ?access_log_io: IO?, ?clean_thread_locals: bool, ?clean_fiber_locals: bool, ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
506
- def initialize(app, server_port, connection_options: {}, http2_options: {}, access_log_io: nil, clean_thread_locals: true, clean_fiber_locals: false, on_error: nil)
507
- @app = app
508
- @server_port = server_port
509
- @write_timeout = connection_options[:write_timeout] || Http::WRITE_TIMEOUT
510
- @access_log_io = access_log_io
511
- @clean_thread_locals = clean_thread_locals
512
- @clean_fiber_locals = clean_fiber_locals
513
- @on_error = on_error
514
- @running = AtomicBoolean.new(true)
515
-
516
- parser = Http2Parser.new
517
- settings_payload = parser.build_settings(
518
- max_concurrent_streams: http2_options[:max_concurrent_streams],
519
- initial_window_size: DEFAULT_WINDOW_SIZE
520
- )
521
- @initial_settings_frame = parser.build_frame(:settings, 0, 0, settings_payload).freeze
522
- end
523
-
524
- # Creates a per-connection {Writer} configured with the handler's write timeout.
525
- #
526
- # @return [Writer] a new per-connection frame writer
527
- #
528
- # @rbs () -> Writer
529
- def create_writer
530
- Writer.new(write_timeout: @write_timeout)
531
- end
532
-
533
- # Stops new stream dispatch and asks clients to finish streams already
534
- # handed to the application.
535
- #
536
- # @param reactor [Reactor] the reactor managing HTTP/2 connections
537
- # @return [void]
538
- #
539
- # @rbs (Reactor reactor) -> void
540
- def shutdown(reactor)
541
- @running.make_false
542
- parser = Http2Parser.new
543
- reactor.drain_http2 do |stream_id|
544
- payload = [stream_id, ERROR_NO_ERROR].pack("NN")
545
- parser.build_frame(:goaway, 0, 0, payload)
546
- end
547
- end
548
-
549
- # Returns a Ractor-safe proc that parses HTTP/2 frames from the
550
- # state hash's buffered bytes.
551
- #
552
- # @return [Proc]
553
- #
554
- # @rbs () -> ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
555
- def parser_worker
556
- proc do |data|
557
- Raptor::Http2.process_frames(data)
558
- end
559
- end
560
-
561
454
  # Advances HTTP/2 frame parsing from the connection buffer, returning
562
455
  # updated connection state along with any outgoing protocol frames and
563
456
  # completed stream requests.
@@ -593,6 +486,12 @@ module Raptor
593
486
  end
594
487
 
595
488
  loop do
489
+ # Reject oversized frames before waiting for their payload.
490
+ if buffer.bytesize >= 3 && (buffer.getbyte(0) << 16 | buffer.getbyte(1) << 8 | buffer.getbyte(2)) > MAX_FRAME_SIZE
491
+ goaway_error = ERROR_FRAME_SIZE_ERROR
492
+ break
493
+ end
494
+
596
495
  parsed = parser.parse_frame(buffer)
597
496
  break unless parsed
598
497
 
@@ -614,9 +513,14 @@ module Raptor
614
513
 
615
514
  when :headers
616
515
  stream_id = frame[:stream_id]
617
- header_payload = frame[:payload]
516
+ header_payload = unpadded_payload(frame)
618
517
  trailers = streams.key?(stream_id)
619
518
 
519
+ unless header_payload
520
+ goaway_error = ERROR_PROTOCOL_ERROR
521
+ break
522
+ end
523
+
620
524
  unless trailers
621
525
  if stream_id.even? || stream_id <= last_client_stream_id
622
526
  goaway_error = ERROR_PROTOCOL_ERROR
@@ -648,6 +552,10 @@ module Raptor
648
552
  end
649
553
 
650
554
  pending_headers = pending_headers.merge(buffer: pending_headers[:buffer] + frame[:payload])
555
+ if pending_headers[:buffer].bytesize > Http2Parser::MAX_HEADER_LIST_SIZE
556
+ goaway_error = ERROR_PROTOCOL_ERROR
557
+ break
558
+ end
651
559
 
652
560
  if frame[:flags].anybits?(FLAG_END_HEADERS)
653
561
  stream_id = pending_headers[:stream_id]
@@ -661,26 +569,29 @@ module Raptor
661
569
 
662
570
  when :data
663
571
  stream_id = frame[:stream_id]
572
+ chunk = unpadded_payload(frame)
664
573
 
665
- unless streams.key?(stream_id)
574
+ if stream_id.zero? || stream_id > last_client_stream_id || !chunk
666
575
  goaway_error = ERROR_PROTOCOL_ERROR
667
576
  break
668
577
  end
669
578
 
579
+ # DATA for closed streams still counts toward connection flow control.
580
+ connection_window -= frame[:length]
581
+ if connection_window < DEFAULT_WINDOW_SIZE / 2
582
+ outgoing_frames << parser.build_frame(:window_update, 0, 0, [DEFAULT_WINDOW_SIZE - connection_window].pack("N"))
583
+ connection_window = DEFAULT_WINDOW_SIZE
584
+ end
585
+
670
586
  stream = streams[stream_id]
671
- existing_body = stream[:body] || ""
672
- stream = stream.merge(body: existing_body + frame[:payload])
673
-
674
- if frame[:payload].bytesize.positive?
675
- connection_window -= frame[:payload].bytesize
676
- if connection_window < DEFAULT_WINDOW_SIZE / 2
677
- increment = DEFAULT_WINDOW_SIZE - connection_window
678
- wu_payload = [increment].pack("N")
679
- outgoing_frames << parser.build_frame(:window_update, 0, 0, wu_payload)
680
- outgoing_frames << parser.build_frame(:window_update, 0, stream_id, wu_payload)
681
- connection_window += increment
682
- end
587
+ next unless stream
588
+
589
+ stream_window_used = (stream[:window_used] || 0) + frame[:length]
590
+ if stream_window_used >= DEFAULT_WINDOW_SIZE / 2 && frame[:flags].nobits?(FLAG_END_STREAM)
591
+ outgoing_frames << parser.build_frame(:window_update, 0, stream_id, [stream_window_used].pack("N"))
592
+ stream_window_used = 0
683
593
  end
594
+ stream = stream.merge(body: (stream[:body] || "") + chunk, window_used: stream_window_used)
684
595
 
685
596
  if frame[:flags].anybits?(FLAG_END_STREAM)
686
597
  stream_headers = stream[:headers] || []
@@ -715,6 +626,14 @@ module Raptor
715
626
  completed_requests.reject! { |request| request[:stream_id] == stream_id }
716
627
  cancelled_streams << stream_id
717
628
  end
629
+ rescue Http2ParserError
630
+ # A failed header decode can desynchronize HPACK state for the whole connection.
631
+ goaway_error = if frame[:type] == :headers || frame[:type] == :continuation
632
+ ERROR_COMPRESSION_ERROR
633
+ else
634
+ ERROR_FRAME_SIZE_ERROR
635
+ end
636
+ break
718
637
  end
719
638
 
720
639
  if goaway_error
@@ -725,6 +644,50 @@ module Raptor
725
644
  build_result(data, buffer, hpack_table, streams, outgoing_frames, completed_requests, cancelled_streams, window_updates, ping_acknowledgements, peer_initial_window_size, connection_window, preface_received, last_client_stream_id, pending_headers, !!goaway_error)
726
645
  end
727
646
 
647
+ # Returns a DATA or HEADERS payload without padding, or nil if padding
648
+ # is invalid.
649
+ #
650
+ # @param frame [Hash] the parsed frame
651
+ # @return [String, nil]
652
+ #
653
+ # @rbs (Hash[Symbol, untyped] frame) -> String?
654
+ def self.unpadded_payload(frame)
655
+ payload = frame[:payload]
656
+ return payload if frame[:flags].nobits?(FLAG_PADDED)
657
+
658
+ pad_length = payload.getbyte(0)
659
+ payload.byteslice(1, payload.bytesize - 1 - pad_length) if pad_length && pad_length < payload.bytesize
660
+ end
661
+ private_class_method :unpadded_payload
662
+
663
+ # Returns whether request pseudo-headers violate RFC 9113 section 8.3.
664
+ #
665
+ # @param headers [Array<Array(String, String)>] decoded header pairs
666
+ # @return [Boolean]
667
+ #
668
+ # @rbs (Array[[String, String]] headers) -> bool
669
+ def self.invalid_pseudo_headers?(headers)
670
+ seen_pseudo = {}
671
+ seen_regular = false
672
+
673
+ headers.each do |name, _value|
674
+ if name.start_with?(":")
675
+ return true if seen_regular
676
+ return true unless REQUEST_PSEUDO_HEADERS.include?(name)
677
+ return true if seen_pseudo[name]
678
+
679
+ seen_pseudo[name] = true
680
+ else
681
+ seen_regular = true
682
+ end
683
+ end
684
+
685
+ return false if seen_pseudo[":method"] && headers.assoc(":method")&.last == "CONNECT"
686
+
687
+ REQUIRED_REQUEST_PSEUDO_HEADERS.any? { |name| !seen_pseudo[name] }
688
+ end
689
+ private_class_method :invalid_pseudo_headers?
690
+
728
691
  # Applies one complete request header block to its stream, resetting
729
692
  # malformed initial headers or trailers and completing valid trailers.
730
693
  #
@@ -732,7 +695,7 @@ module Raptor
732
695
  # @param completed_requests [Array<Hash>] accumulator of completed stream requests
733
696
  # @param outgoing_frames [Array<String>] accumulator of protocol response frames
734
697
  # @param parser [Http2Parser] frame parser and encoder
735
- # @param stream_id [Integer] the stream identifier
698
+ # @param stream_id [Integer] the HTTP/2 stream identifier
736
699
  # @param decoded_headers [Array<Array(String, String)>] decoded header pairs
737
700
  # @param end_stream [Boolean] whether the source frame had END_STREAM set
738
701
  # @param trailers [Boolean] whether this is a trailing header block
@@ -769,7 +732,7 @@ module Raptor
769
732
  #
770
733
  # @param streams [Hash] current open-stream map
771
734
  # @param completed_requests [Array<Hash>] accumulator of completed stream requests
772
- # @param stream_id [Integer] the stream identifier
735
+ # @param stream_id [Integer] the HTTP/2 stream identifier
773
736
  # @param decoded_headers [Array<Array(String, String)>] decoded header pairs
774
737
  # @param end_stream [Boolean] whether the source frame had END_STREAM set
775
738
  # @return [Array(Hash, Array<Hash>)] updated streams and completed_requests
@@ -840,20 +803,92 @@ module Raptor
840
803
  end
841
804
  private_class_method :build_result
842
805
 
806
+ # @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
807
+ # @rbs @server_port_string: String
808
+ # @rbs @write_timeout: Integer
809
+ # @rbs @access_log_io: IO?
810
+ # @rbs @clean_thread_locals: bool
811
+ # @rbs @clean_fiber_locals: bool
812
+ # @rbs @on_error: (^(Hash[String, untyped]?, Exception) -> void)?
813
+ # @rbs @running: AtomicBoolean
814
+ # @rbs @initial_settings_frame: String
815
+
816
+ # Creates a new Http2 handler.
817
+ #
818
+ # @param app [#call] the Rack application to dispatch requests to
819
+ # @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
820
+ # @param connection_options [Hash] per-connection settings shared across protocols
821
+ # @option connection_options [Integer] :write_timeout per-write socket timeout in seconds
822
+ # @param http2_options [Hash] HTTP/2-specific settings
823
+ # @option http2_options [Integer] :max_concurrent_streams maximum HTTP/2 concurrent streams per connection
824
+ # @param access_log_io [IO, nil] IO to write Common Log Format access entries to, or nil to disable
825
+ # @param clean_thread_locals [Boolean] whether to clear application thread locals after each request
826
+ # @param clean_fiber_locals [Boolean] whether to process each request in a fresh Fiber
827
+ # @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
828
+ # @return [void]
829
+ #
830
+ # @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?connection_options: Hash[Symbol, untyped], ?http2_options: Hash[Symbol, untyped], ?access_log_io: IO?, ?clean_thread_locals: bool, ?clean_fiber_locals: bool, ?on_error: (^(Hash[String, untyped]?, Exception) -> void)?) -> void
831
+ def initialize(app, server_port, connection_options: {}, http2_options: {}, access_log_io: nil, clean_thread_locals: true, clean_fiber_locals: false, on_error: nil)
832
+ @app = app
833
+ @server_port_string = server_port.to_s.freeze
834
+ @write_timeout = connection_options[:write_timeout] || Http::WRITE_TIMEOUT
835
+ @access_log_io = access_log_io
836
+ @clean_thread_locals = clean_thread_locals
837
+ @clean_fiber_locals = clean_fiber_locals
838
+ @on_error = on_error
839
+ @running = AtomicBoolean.new(true)
840
+
841
+ parser = Http2Parser.new
842
+ settings_payload = parser.build_settings(
843
+ max_concurrent_streams: http2_options[:max_concurrent_streams],
844
+ initial_window_size: DEFAULT_WINDOW_SIZE,
845
+ max_header_list_size: Http2Parser::MAX_HEADER_LIST_SIZE
846
+ )
847
+ @initial_settings_frame = parser.build_frame(:settings, 0, 0, settings_payload).freeze
848
+ end
849
+
850
+ # Returns a Ractor-safe proc that parses HTTP/2 frames from the
851
+ # state hash's buffered bytes.
852
+ #
853
+ # @return [Proc]
854
+ #
855
+ # @rbs () -> ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
856
+ def parser_worker
857
+ proc do |data|
858
+ Http2.process_frames(data)
859
+ end
860
+ end
861
+
862
+ # Stops new stream dispatch and asks clients to finish streams already
863
+ # handed to the application.
864
+ #
865
+ # @param reactor [Reactor] the reactor managing HTTP/2 connections
866
+ # @return [void]
867
+ #
868
+ # @rbs (Reactor reactor) -> void
869
+ def shutdown(reactor)
870
+ @running.make_false
871
+ parser = Http2Parser.new
872
+ reactor.drain_http2 do |stream_id|
873
+ payload = [stream_id, ERROR_NO_ERROR].pack("NN")
874
+ parser.build_frame(:goaway, 0, 0, payload)
875
+ end
876
+ end
877
+
843
878
  # Sends the server SETTINGS frame on a freshly negotiated HTTP/2
844
879
  # connection, then eagerly reads and parses the first client frame
845
880
  # batch, dispatching completed streams directly to the thread pool.
846
881
  # Falls back to the reactor when no initial data is ready.
847
882
  #
848
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
849
- # @param id [Integer] unique client identifier
883
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
884
+ # @param id [Integer] unique connection identifier
850
885
  # @param reactor [Reactor] the reactor managing the connection
851
886
  # @param thread_pool [AtomicThreadPool] thread pool for application processing
852
- # @param remote_addr [String] client IP address
853
- # @param url_scheme [String] "https"
887
+ # @param remote_addr [String] the client IP address
888
+ # @param url_scheme [String] "http" or "https"
854
889
  # @return [void]
855
890
  #
856
- # @rbs (OpenSSL::SSL::SSLSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
891
+ # @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
857
892
  def eager_accept(socket, id, reactor, thread_pool, remote_addr, url_scheme)
858
893
  unless @running.true?
859
894
  socket.close rescue nil
@@ -892,7 +927,7 @@ module Raptor
892
927
  rescue IO::WaitReadable
893
928
  reactor.watch(id)
894
929
  return
895
- rescue EOFError, IOError
930
+ rescue EOFError, IOError, SystemCallError
896
931
  reactor.close_connection(id)
897
932
  return
898
933
  end
@@ -901,7 +936,7 @@ module Raptor
901
936
  buffer << socket.read_nonblock(socket.pending)
902
937
  end
903
938
 
904
- result = Raptor::Http2.process_frames(initial_state.merge(buffer: buffer))
939
+ result = Http2.process_frames(initial_state.merge(buffer: buffer))
905
940
  handle_parsed_request(result, reactor, thread_pool)
906
941
  rescue => error
907
942
  Log.rescued_error(error)
@@ -913,45 +948,45 @@ module Raptor
913
948
  # further buffered frame batches before returning control to the
914
949
  # reactor.
915
950
  #
916
- # @param result [Hash] the parsed result produced by {process_frames}
951
+ # @param parsed_request [Hash] the parsed result produced by {.process_frames}
917
952
  # @param reactor [Reactor] the reactor managing the connection
918
- # @param thread_pool [AtomicThreadPool] thread pool for Rack app dispatch
953
+ # @param thread_pool [AtomicThreadPool] thread pool for application processing
919
954
  # @return [void]
920
955
  #
921
- # @rbs (Hash[Symbol, untyped] result, Reactor reactor, AtomicThreadPool thread_pool) -> void
922
- def handle_parsed_request(result, reactor, thread_pool)
923
- socket = reactor.socket_for(result[:id])
956
+ # @rbs (Hash[Symbol, untyped] parsed_request, Reactor reactor, AtomicThreadPool thread_pool) -> void
957
+ def handle_parsed_request(parsed_request, reactor, thread_pool)
958
+ socket = reactor.socket_for(parsed_request[:id])
924
959
  return unless socket
925
960
 
926
- writer = reactor.writer_for(result[:id])
927
- flow_control = reactor.flow_control_for(result[:id])
961
+ writer = reactor.writer_for(parsed_request[:id])
962
+ flow_control = reactor.flow_control_for(parsed_request[:id])
928
963
 
929
964
  rounds = 0
930
965
  loop do
931
- reactor.acknowledge_http2_ping(result[:id], result[:ping_acknowledgements])
932
- result[:cancelled_streams]&.each do |stream_id|
966
+ reactor.acknowledge_http2_ping(parsed_request[:id], parsed_request[:ping_acknowledgements])
967
+ parsed_request[:cancelled_streams]&.each do |stream_id|
933
968
  flow_control.cancel_stream(stream_id)
934
- reactor.cancel_http2_body(result[:id], stream_id)
969
+ reactor.cancel_http2_body(parsed_request[:id], stream_id)
935
970
  end
936
971
 
937
- if flow_control && (result[:window_updates]&.any? || result[:peer_initial_window_size])
938
- apply_flow_control_updates(flow_control, result)
939
- reactor.resume_http2_bodies(result[:id])
972
+ if flow_control && (parsed_request[:window_updates]&.any? || parsed_request[:peer_initial_window_size])
973
+ apply_flow_control_updates(flow_control, parsed_request)
974
+ reactor.resume_http2_bodies(parsed_request[:id])
940
975
  end
941
976
 
942
- writer.write_frames(socket, result[:outgoing_frames])
977
+ writer.write_frames(socket, parsed_request[:outgoing_frames])
943
978
 
944
- if result[:close_connection]
945
- reactor.close_connection(result[:id])
979
+ if parsed_request[:close_connection]
980
+ reactor.close_connection(parsed_request[:id])
946
981
  return
947
982
  end
948
983
 
949
- result[:completed_requests]&.each do |request|
984
+ parsed_request[:completed_requests]&.each do |request|
950
985
  stream_id = request[:stream_id]
951
- remote_addr = result[:remote_addr] || Server::DEFAULT_REMOTE_ADDR
986
+ remote_addr = parsed_request[:remote_addr] || Server::DEFAULT_REMOTE_ADDR
952
987
 
953
- unless reactor.dispatch_http2_stream(result[:id], stream_id)
954
- write_http2_reset_stream(socket, writer, stream_id, ERROR_REFUSED_STREAM)
988
+ unless reactor.dispatch_http2_stream(parsed_request[:id], stream_id)
989
+ write_reset_stream(socket, writer, stream_id, ERROR_REFUSED_STREAM)
955
990
  next
956
991
  end
957
992
 
@@ -971,19 +1006,28 @@ module Raptor
971
1006
  next_batch = eager_read_next_batch(socket)
972
1007
  break unless next_batch
973
1008
 
974
- result = Raptor::Http2.process_frames(result.merge(buffer: result[:buffer] + next_batch))
1009
+ parsed_request = Http2.process_frames(parsed_request.merge(buffer: parsed_request[:buffer] + next_batch))
975
1010
  end
976
1011
 
977
- reactor.update_http2_state(result)
1012
+ reactor.update_state(parsed_request)
978
1013
  end
979
1014
 
980
1015
  private
981
1016
 
1017
+ # Creates a per-connection {Writer} configured with the handler's write timeout.
1018
+ #
1019
+ # @return [Writer] a new per-connection frame writer
1020
+ #
1021
+ # @rbs () -> Writer
1022
+ def create_writer
1023
+ Writer.new(write_timeout: @write_timeout)
1024
+ end
1025
+
982
1026
  # Applies inbound flow-control updates from a parsed result to the
983
- # connection's `FlowControl`.
1027
+ # connection's {FlowControl}.
984
1028
  #
985
- # @param flow_control [FlowControl] the per-connection flow controller
986
- # @param result [Hash] the parsed result from `process_frames`
1029
+ # @param flow_control [FlowControl] per-connection outbound flow controller
1030
+ # @param result [Hash] the parsed result from {.process_frames}
987
1031
  # @return [void]
988
1032
  #
989
1033
  # @rbs (FlowControl flow_control, Hash[Symbol, untyped] result) -> void
@@ -1004,22 +1048,19 @@ module Raptor
1004
1048
  # Reads the next frame batch from `socket` if it has already arrived,
1005
1049
  # without waiting on the shared collector thread.
1006
1050
  #
1007
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1051
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
1008
1052
  # @return [String, nil] the bytes read, or nil if nothing was available
1009
1053
  #
1010
- # @rbs (OpenSSL::SSL::SSLSocket socket) -> String?
1054
+ # @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket) -> String?
1011
1055
  def eager_read_next_batch(socket)
1012
1056
  return unless socket.wait_readable(0)
1013
1057
 
1014
- data = begin
1058
+ buffer = begin
1015
1059
  socket.read_nonblock(EAGER_READ_BUFFER_SIZE)
1016
- rescue IO::WaitReadable, EOFError, IOError
1060
+ rescue IO::WaitReadable, EOFError, IOError, SystemCallError
1017
1061
  return
1018
1062
  end
1019
1063
 
1020
- buffer = String.new
1021
- buffer << data
1022
-
1023
1064
  while socket.respond_to?(:pending) && socket.pending.positive?
1024
1065
  buffer << socket.read_nonblock(socket.pending)
1025
1066
  end
@@ -1030,8 +1071,8 @@ module Raptor
1030
1071
  # Dispatches a completed stream request to the Rack app and writes
1031
1072
  # the response back as HTTP/2 frames.
1032
1073
  #
1033
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1034
- # @param writer [Writer] lock-free frame writer for the connection
1074
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
1075
+ # @param writer [Writer] frame writer for the connection
1035
1076
  # @param flow_control [FlowControl] per-connection outbound flow controller
1036
1077
  # @param stream_id [Integer] the HTTP/2 stream identifier
1037
1078
  # @param headers [Array<Array(String, String)>] request headers
@@ -1039,12 +1080,12 @@ module Raptor
1039
1080
  # @param remote_addr [String] the client IP address
1040
1081
  # @return [void]
1041
1082
  #
1042
- # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
1083
+ # @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
1043
1084
  def dispatch_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:)
1044
1085
  if @clean_fiber_locals
1045
- Fiber.new { perform_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:) }.resume
1086
+ Fiber.new { perform_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr: remote_addr) }.resume
1046
1087
  else
1047
- perform_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:)
1088
+ perform_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr: remote_addr)
1048
1089
  end
1049
1090
  ensure
1050
1091
  ThreadLocals.clear if @clean_thread_locals
@@ -1052,8 +1093,8 @@ module Raptor
1052
1093
 
1053
1094
  # Calls the Rack app and writes its response for one stream.
1054
1095
  #
1055
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1056
- # @param writer [Writer] lock-free frame writer for the connection
1096
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
1097
+ # @param writer [Writer] frame writer for the connection
1057
1098
  # @param flow_control [FlowControl] per-connection outbound flow controller
1058
1099
  # @param stream_id [Integer] the HTTP/2 stream identifier
1059
1100
  # @param headers [Array<Array(String, String)>] request headers
@@ -1061,7 +1102,7 @@ module Raptor
1061
1102
  # @param remote_addr [String] the client IP address
1062
1103
  # @return [void]
1063
1104
  #
1064
- # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
1105
+ # @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
1065
1106
  def perform_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:)
1066
1107
  env = nil
1067
1108
  status = nil
@@ -1081,14 +1122,14 @@ module Raptor
1081
1122
  Http.call_response_finished(env, status, response_headers, error)
1082
1123
  flow_control.discard_stream(stream_id)
1083
1124
  end
1084
- write_http2_detached_response(socket, writer, flow_control, stream_id, status, response_headers, response_body, finished) do
1125
+ write_detached_response(socket, writer, flow_control, stream_id, status, response_headers, response_body, finished) do
1085
1126
  response_started = true
1086
1127
  end
1087
1128
  detached = true
1088
1129
  return
1089
1130
  end
1090
1131
 
1091
- response_size = write_http2_response(
1132
+ response_size = write_response(
1092
1133
  socket,
1093
1134
  writer,
1094
1135
  flow_control,
@@ -1111,9 +1152,9 @@ module Raptor
1111
1152
  rescue => error
1112
1153
  Http.call_response_finished(env, status, response_headers, error)
1113
1154
  if response_started
1114
- write_http2_reset_stream(socket, writer, stream_id, ERROR_INTERNAL_ERROR)
1155
+ write_reset_stream(socket, writer, stream_id, ERROR_INTERNAL_ERROR)
1115
1156
  else
1116
- write_http2_error_response(socket, writer, stream_id)
1157
+ write_error_response(socket, writer, stream_id)
1117
1158
  end
1118
1159
 
1119
1160
  if @on_error
@@ -1127,8 +1168,8 @@ module Raptor
1127
1168
 
1128
1169
  # Starts a detached HTTP/2 response after writing its response headers.
1129
1170
  #
1130
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1131
- # @param writer [Writer] lock-free frame writer for the connection
1171
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
1172
+ # @param writer [Writer] frame writer for the connection
1132
1173
  # @param flow_control [FlowControl] per-connection outbound flow controller
1133
1174
  # @param stream_id [Integer] the HTTP/2 stream identifier
1134
1175
  # @param status [Integer] HTTP status code
@@ -1138,22 +1179,22 @@ module Raptor
1138
1179
  # @yield once the response headers are queued
1139
1180
  # @return [void]
1140
1181
  #
1141
- # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, DetachedBody body, ^(Symbol) -> void finished) { () -> void } -> void
1142
- def write_http2_detached_response(socket, writer, flow_control, stream_id, status, headers, body, finished)
1182
+ # @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, DetachedBody body, ^(Symbol) -> void finished) { () -> void } -> void
1183
+ def write_detached_response(socket, writer, flow_control, stream_id, status, headers, body, finished)
1143
1184
  flow_control.check(stream_id)
1144
1185
  parser = Http2Parser.new
1145
1186
  encoded = parser.encode_response_headers(status, headers)
1146
1187
  writer.write_frames(socket, [parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded)])
1147
1188
  yield
1148
1189
  attached = writer.attach_body(stream_id, body, finished)
1149
- write_http2_reset_stream(socket, writer, stream_id, ERROR_REFUSED_STREAM) unless attached
1190
+ write_reset_stream(socket, writer, stream_id, ERROR_REFUSED_STREAM) unless attached
1150
1191
  end
1151
1192
 
1152
1193
  # Writes a Rack response as HTTP/2 frames to the socket, partitioning
1153
1194
  # DATA frames through `flow_control` to fit within the peer's windows.
1154
1195
  #
1155
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1156
- # @param writer [Writer] lock-free frame writer for the connection
1196
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
1197
+ # @param writer [Writer] frame writer for the connection
1157
1198
  # @param flow_control [FlowControl] per-connection outbound flow controller
1158
1199
  # @param stream_id [Integer] the HTTP/2 stream identifier
1159
1200
  # @param status [Integer] HTTP status code
@@ -1161,10 +1202,11 @@ module Raptor
1161
1202
  # @param body [Object] response body responding to each
1162
1203
  # @param trailers [Hash] trailing response headers populated by the Rack application
1163
1204
  # @param request_method [String] request method used to suppress HEAD response bodies
1205
+ # @yield once the response headers are queued
1164
1206
  # @return [String] the response body size in bytes
1165
1207
  #
1166
- # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body, trailers: Hash[String, String | Array[String]], request_method: String) ?{ () -> void } -> String
1167
- def write_http2_response(socket, writer, flow_control, stream_id, status, headers, body, trailers:, request_method:)
1208
+ # @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body, trailers: Hash[String, String | Array[String]], request_method: String) ?{ () -> void } -> String
1209
+ def write_response(socket, writer, flow_control, stream_id, status, headers, body, trailers:, request_method:)
1168
1210
  parser = Http2Parser.new
1169
1211
 
1170
1212
  encoded_headers = parser.encode_response_headers(status, headers)
@@ -1192,14 +1234,14 @@ module Raptor
1192
1234
 
1193
1235
  # Sends an HTTP 103 Early Hints response.
1194
1236
  #
1195
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1237
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
1196
1238
  # @param writer [Writer] frame writer for the connection
1197
1239
  # @param flow_control [FlowControl] per-connection outbound flow controller
1198
1240
  # @param stream_id [Integer] the HTTP/2 stream identifier
1199
1241
  # @param hints [Hash] response headers to send as early hints
1200
1242
  # @return [void]
1201
1243
  #
1202
- # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Hash[String, String | Array[String]] hints) -> void
1244
+ # @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Hash[String, String | Array[String]] hints) -> void
1203
1245
  def send_early_hints(socket, writer, flow_control, stream_id, hints)
1204
1246
  return if hints.empty?
1205
1247
 
@@ -1211,14 +1253,14 @@ module Raptor
1211
1253
 
1212
1254
  # Resets one HTTP/2 stream with the given error code.
1213
1255
  #
1214
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1256
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
1215
1257
  # @param writer [Writer] frame writer for the connection
1216
1258
  # @param stream_id [Integer] the HTTP/2 stream identifier
1217
1259
  # @param error_code [Integer] the HTTP/2 error code
1218
1260
  # @return [void]
1219
1261
  #
1220
- # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Integer error_code) -> void
1221
- def write_http2_reset_stream(socket, writer, stream_id, error_code)
1262
+ # @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Integer error_code) -> void
1263
+ def write_reset_stream(socket, writer, stream_id, error_code)
1222
1264
  parser = Http2Parser.new
1223
1265
  frame = parser.build_frame(:rst_stream, 0, stream_id, [error_code].pack("N"))
1224
1266
  writer.write_frames(socket, [frame])
@@ -1226,13 +1268,13 @@ module Raptor
1226
1268
 
1227
1269
  # Writes a 500 error response as HTTP/2 frames.
1228
1270
  #
1229
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1230
- # @param writer [Writer] lock-free frame writer for the connection
1271
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
1272
+ # @param writer [Writer] frame writer for the connection
1231
1273
  # @param stream_id [Integer] the HTTP/2 stream identifier
1232
1274
  # @return [void]
1233
1275
  #
1234
- # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id) -> void
1235
- def write_http2_error_response(socket, writer, stream_id)
1276
+ # @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id) -> void
1277
+ def write_error_response(socket, writer, stream_id)
1236
1278
  parser = Http2Parser.new
1237
1279
  encoded = parser.encode_headers([[":status", "500"]])
1238
1280
 
@@ -1242,31 +1284,18 @@ module Raptor
1242
1284
  )
1243
1285
  end
1244
1286
 
1245
- # Instance-level wrapper around {Http.write_access_log} that routes to
1246
- # the configured `@access_log_io`.
1247
- #
1248
- # @param env [Hash] the Rack environment
1249
- # @param status [Integer] the response status code
1250
- # @param size [String] the response body size in bytes, or `-` if unknown
1251
- # @param remote_addr [String] the client IP address
1252
- # @return [void]
1253
- #
1254
- # @rbs (Hash[String, untyped] env, Integer status, String size, String remote_addr) -> void
1255
- def write_access_log(env, status, size, remote_addr)
1256
- Http.write_access_log(@access_log_io, env, status, size, remote_addr)
1257
- end
1258
-
1259
1287
  # Builds a Rack environment hash from HTTP/2 headers and body.
1260
1288
  #
1261
1289
  # @param headers [Array<Array(String, String)>] HTTP/2 header pairs
1262
1290
  # @param body [String] the request body
1263
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1291
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
1264
1292
  # @param writer [Writer] frame writer for the connection
1293
+ # @param flow_control [FlowControl] per-connection outbound flow controller
1265
1294
  # @param stream_id [Integer] the HTTP/2 stream identifier
1266
1295
  # @param remote_addr [String] the client IP address
1267
1296
  # @return [Hash] fully populated Rack environment hash
1268
1297
  #
1269
- # @rbs (Array[[String, String]] headers, String body, OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, remote_addr: String) -> Hash[String, untyped]
1298
+ # @rbs (Array[[String, String]] headers, String body, TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, remote_addr: String) -> Hash[String, untyped]
1270
1299
  def build_rack_env(headers, body, socket, writer, flow_control, stream_id, remote_addr:)
1271
1300
  env = {}
1272
1301
 
@@ -1287,7 +1316,12 @@ module Raptor
1287
1316
  env[Http::CONTENT_LENGTH] = value
1288
1317
  else
1289
1318
  rack_key = "HTTP_#{name.upcase.tr("-", "_")}"
1290
- env[rack_key] = value
1319
+ if (existing = env[rack_key])
1320
+ separator = rack_key == Rack::HTTP_COOKIE ? "; " : ", "
1321
+ env[rack_key] = "#{existing}#{separator}#{value}"
1322
+ else
1323
+ env[rack_key] = value
1324
+ end
1291
1325
  end
1292
1326
  end
1293
1327
 
@@ -1335,12 +1369,25 @@ module Raptor
1335
1369
  else
1336
1370
  host, port = http_host.split(":", 2)
1337
1371
  end
1338
- env[Rack::SERVER_NAME] ||= host
1339
- env[Rack::SERVER_PORT] ||= port || @server_port.to_s
1372
+ env[Rack::SERVER_NAME] ||= host || Server::DEFAULT_SERVER_NAME
1373
+ env[Rack::SERVER_PORT] ||= port || @server_port_string
1340
1374
  else
1341
1375
  env[Rack::SERVER_NAME] ||= Server::DEFAULT_SERVER_NAME
1342
- env[Rack::SERVER_PORT] ||= @server_port.to_s
1376
+ env[Rack::SERVER_PORT] ||= @server_port_string
1343
1377
  end
1344
1378
  end
1379
+
1380
+ # Writes a completed request to the access log.
1381
+ #
1382
+ # @param env [Hash] the Rack environment
1383
+ # @param status [Integer] the response status code
1384
+ # @param size [String] the response body size in bytes, or `-` if unknown
1385
+ # @param remote_addr [String] the client IP address
1386
+ # @return [void]
1387
+ #
1388
+ # @rbs (Hash[String, untyped] env, Integer status, String size, String remote_addr) -> void
1389
+ def write_access_log(env, status, size, remote_addr)
1390
+ Http.write_access_log(@access_log_io, env, status, size, remote_addr)
1391
+ end
1345
1392
  end
1346
1393
  end