raptor 0.22.3 → 0.24.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/Dockerfile +10 -2
  4. data/README.md +183 -61
  5. data/docs/assets/profile_picture.png +0 -0
  6. data/docs/assets/startup_output.svg +37 -0
  7. data/docs/raptor-vs-puma.md +104 -424
  8. data/docs/talk.md +1388 -0
  9. data/ext/raptor_bpf/reuseport_select.bpf.c +18 -9
  10. data/ext/raptor_http/extconf.rb +2 -0
  11. data/ext/raptor_http/raptor_http.c +165 -234
  12. data/ext/raptor_http2/raptor_http2.c +272 -223
  13. data/ext/raptor_native/raptor_native.c +38 -20
  14. data/lib/rackup/handler/raptor.rb +14 -12
  15. data/lib/raptor/binder.rb +12 -12
  16. data/lib/raptor/cli.rb +19 -18
  17. data/lib/raptor/cluster.rb +120 -74
  18. data/lib/raptor/control_server.rb +5 -2
  19. data/lib/raptor/detached_body.rb +6 -2
  20. data/lib/raptor/http.rb +7 -6
  21. data/lib/raptor/http1.rb +281 -234
  22. data/lib/raptor/http2.rb +356 -276
  23. data/lib/raptor/plugin.rb +98 -0
  24. data/lib/raptor/reactor.rb +138 -148
  25. data/lib/raptor/reuseport_bpf.rb +62 -38
  26. data/lib/raptor/server.rb +45 -34
  27. data/lib/raptor/stats.rb +15 -3
  28. data/lib/raptor/thread_locals.rb +4 -3
  29. data/lib/raptor/version.rb +1 -1
  30. data/sig/generated/rackup/handler/raptor.rbs +2 -1
  31. data/sig/generated/raptor/binder.rbs +9 -9
  32. data/sig/generated/raptor/cli.rbs +6 -6
  33. data/sig/generated/raptor/cluster.rbs +18 -5
  34. data/sig/generated/raptor/control_server.rbs +2 -1
  35. data/sig/generated/raptor/detached_body.rbs +5 -1
  36. data/sig/generated/raptor/http.rbs +3 -5
  37. data/sig/generated/raptor/http1.rbs +119 -102
  38. data/sig/generated/raptor/http2.rbs +204 -190
  39. data/sig/generated/raptor/plugin.rbs +52 -0
  40. data/sig/generated/raptor/reactor.rbs +107 -118
  41. data/sig/generated/raptor/reuseport_bpf.rbs +54 -17
  42. data/sig/generated/raptor/server.rbs +26 -26
  43. data/sig/generated/raptor/stats.rbs +3 -2
  44. data/sig/generated/raptor/thread_locals.rbs +4 -3
  45. metadata +6 -2
  46. 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,148 +451,11 @@ 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.
564
457
  #
565
- # @param data [Hash] the connection state including buffer and HPACK table
458
+ # @param data [Hash] the connection state including buffer, HPACK table and body-size limit
566
459
  # @return [Hash] updated state with outgoing_frames and completed_requests
567
460
  #
568
461
  # @rbs (Hash[Symbol, untyped] data) -> Hash[Symbol, untyped]
@@ -581,6 +474,7 @@ module Raptor
581
474
  preface_received = data[:http2_preface_received] || false
582
475
  last_client_stream_id = data[:http2_last_client_stream_id] || 0
583
476
  pending_headers = data[:http2_pending_headers]
477
+ max_body_size = data[:max_body_size]
584
478
  goaway_error = nil
585
479
 
586
480
  unless preface_received
@@ -593,6 +487,12 @@ module Raptor
593
487
  end
594
488
 
595
489
  loop do
490
+ # Reject oversized frames before waiting for their payload.
491
+ if buffer.bytesize >= 3 && (buffer.getbyte(0) << 16 | buffer.getbyte(1) << 8 | buffer.getbyte(2)) > MAX_FRAME_SIZE
492
+ goaway_error = ERROR_FRAME_SIZE_ERROR
493
+ break
494
+ end
495
+
596
496
  parsed = parser.parse_frame(buffer)
597
497
  break unless parsed
598
498
 
@@ -614,9 +514,14 @@ module Raptor
614
514
 
615
515
  when :headers
616
516
  stream_id = frame[:stream_id]
617
- header_payload = frame[:payload]
517
+ header_payload = unpadded_payload(frame)
618
518
  trailers = streams.key?(stream_id)
619
519
 
520
+ unless header_payload
521
+ goaway_error = ERROR_PROTOCOL_ERROR
522
+ break
523
+ end
524
+
620
525
  unless trailers
621
526
  if stream_id.even? || stream_id <= last_client_stream_id
622
527
  goaway_error = ERROR_PROTOCOL_ERROR
@@ -648,6 +553,10 @@ module Raptor
648
553
  end
649
554
 
650
555
  pending_headers = pending_headers.merge(buffer: pending_headers[:buffer] + frame[:payload])
556
+ if pending_headers[:buffer].bytesize > Http2Parser::MAX_HEADER_LIST_SIZE
557
+ goaway_error = ERROR_PROTOCOL_ERROR
558
+ break
559
+ end
651
560
 
652
561
  if frame[:flags].anybits?(FLAG_END_HEADERS)
653
562
  stream_id = pending_headers[:stream_id]
@@ -661,27 +570,47 @@ module Raptor
661
570
 
662
571
  when :data
663
572
  stream_id = frame[:stream_id]
573
+ chunk = unpadded_payload(frame)
664
574
 
665
- unless streams.key?(stream_id)
575
+ if stream_id.zero? || stream_id > last_client_stream_id || !chunk
666
576
  goaway_error = ERROR_PROTOCOL_ERROR
667
577
  break
668
578
  end
669
579
 
580
+ # DATA for closed streams still counts toward connection flow control.
581
+ connection_window -= frame[:length]
582
+ if connection_window < DEFAULT_WINDOW_SIZE / 2
583
+ outgoing_frames << parser.build_frame(:window_update, 0, 0, [DEFAULT_WINDOW_SIZE - connection_window].pack("N"))
584
+ connection_window = DEFAULT_WINDOW_SIZE
585
+ end
586
+
670
587
  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
588
+ next unless stream
589
+
590
+ if stream[:too_large]
591
+ streams.delete(stream_id) if frame[:flags].anybits?(FLAG_END_STREAM)
592
+ next
593
+ end
594
+
595
+ body = stream[:body] || ""
596
+ if max_body_size && body.bytesize + chunk.bytesize > max_body_size
597
+ headers = parser.encode_headers([[":status", "413"], ["content-length", "0"]])
598
+ outgoing_frames << parser.build_frame(:headers, FLAG_END_HEADERS | FLAG_END_STREAM, stream_id, headers)
599
+ if frame[:flags].anybits?(FLAG_END_STREAM)
600
+ streams.delete(stream_id)
601
+ else
602
+ streams[stream_id] = { too_large: true }
682
603
  end
604
+ next
683
605
  end
684
606
 
607
+ stream_window_used = (stream[:window_used] || 0) + frame[:length]
608
+ if stream_window_used >= DEFAULT_WINDOW_SIZE / 2 && frame[:flags].nobits?(FLAG_END_STREAM)
609
+ outgoing_frames << parser.build_frame(:window_update, 0, stream_id, [stream_window_used].pack("N"))
610
+ stream_window_used = 0
611
+ end
612
+ stream = stream.merge(body: body + chunk, window_used: stream_window_used)
613
+
685
614
  if frame[:flags].anybits?(FLAG_END_STREAM)
686
615
  stream_headers = stream[:headers] || []
687
616
  completed_requests << {
@@ -715,6 +644,14 @@ module Raptor
715
644
  completed_requests.reject! { |request| request[:stream_id] == stream_id }
716
645
  cancelled_streams << stream_id
717
646
  end
647
+ rescue Http2ParserError
648
+ # A failed header decode can desynchronize HPACK state for the whole connection.
649
+ goaway_error = if frame[:type] == :headers || frame[:type] == :continuation
650
+ ERROR_COMPRESSION_ERROR
651
+ else
652
+ ERROR_FRAME_SIZE_ERROR
653
+ end
654
+ break
718
655
  end
719
656
 
720
657
  if goaway_error
@@ -725,6 +662,50 @@ module Raptor
725
662
  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
663
  end
727
664
 
665
+ # Returns a DATA or HEADERS payload without padding, or nil if padding
666
+ # is invalid.
667
+ #
668
+ # @param frame [Hash] the parsed frame
669
+ # @return [String, nil]
670
+ #
671
+ # @rbs (Hash[Symbol, untyped] frame) -> String?
672
+ def self.unpadded_payload(frame)
673
+ payload = frame[:payload]
674
+ return payload if frame[:flags].nobits?(FLAG_PADDED)
675
+
676
+ pad_length = payload.getbyte(0)
677
+ payload.byteslice(1, payload.bytesize - 1 - pad_length) if pad_length && pad_length < payload.bytesize
678
+ end
679
+ private_class_method :unpadded_payload
680
+
681
+ # Returns whether request pseudo-headers violate RFC 9113 section 8.3.
682
+ #
683
+ # @param headers [Array<Array(String, String)>] decoded header pairs
684
+ # @return [Boolean]
685
+ #
686
+ # @rbs (Array[[String, String]] headers) -> bool
687
+ def self.invalid_pseudo_headers?(headers)
688
+ seen_pseudo = {}
689
+ seen_regular = false
690
+
691
+ headers.each do |name, _value|
692
+ if name.start_with?(":")
693
+ return true if seen_regular
694
+ return true unless REQUEST_PSEUDO_HEADERS.include?(name)
695
+ return true if seen_pseudo[name]
696
+
697
+ seen_pseudo[name] = true
698
+ else
699
+ seen_regular = true
700
+ end
701
+ end
702
+
703
+ return false if seen_pseudo[":method"] && headers.assoc(":method")&.last == "CONNECT"
704
+
705
+ REQUIRED_REQUEST_PSEUDO_HEADERS.any? { |name| !seen_pseudo[name] }
706
+ end
707
+ private_class_method :invalid_pseudo_headers?
708
+
728
709
  # Applies one complete request header block to its stream, resetting
729
710
  # malformed initial headers or trailers and completing valid trailers.
730
711
  #
@@ -732,7 +713,7 @@ module Raptor
732
713
  # @param completed_requests [Array<Hash>] accumulator of completed stream requests
733
714
  # @param outgoing_frames [Array<String>] accumulator of protocol response frames
734
715
  # @param parser [Http2Parser] frame parser and encoder
735
- # @param stream_id [Integer] the stream identifier
716
+ # @param stream_id [Integer] the HTTP/2 stream identifier
736
717
  # @param decoded_headers [Array<Array(String, String)>] decoded header pairs
737
718
  # @param end_stream [Boolean] whether the source frame had END_STREAM set
738
719
  # @param trailers [Boolean] whether this is a trailing header block
@@ -751,11 +732,13 @@ module Raptor
751
732
  outgoing_frames << parser.build_frame(:rst_stream, 0, stream_id, [ERROR_PROTOCOL_ERROR].pack("N"))
752
733
  elsif trailers
753
734
  stream = streams.delete(stream_id)
754
- completed_requests << {
755
- stream_id: stream_id,
756
- headers: stream[:headers],
757
- body: stream[:body] || ""
758
- }
735
+ unless stream[:too_large]
736
+ completed_requests << {
737
+ stream_id: stream_id,
738
+ headers: stream[:headers],
739
+ body: stream[:body] || ""
740
+ }
741
+ end
759
742
  else
760
743
  streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, end_stream)
761
744
  end
@@ -769,7 +752,7 @@ module Raptor
769
752
  #
770
753
  # @param streams [Hash] current open-stream map
771
754
  # @param completed_requests [Array<Hash>] accumulator of completed stream requests
772
- # @param stream_id [Integer] the stream identifier
755
+ # @param stream_id [Integer] the HTTP/2 stream identifier
773
756
  # @param decoded_headers [Array<Array(String, String)>] decoded header pairs
774
757
  # @param end_stream [Boolean] whether the source frame had END_STREAM set
775
758
  # @return [Array(Hash, Array<Hash>)] updated streams and completed_requests
@@ -820,6 +803,7 @@ module Raptor
820
803
  id: data[:id],
821
804
  protocol: :http2,
822
805
  buffer: buffer || "",
806
+ max_body_size: data[:max_body_size],
823
807
  hpack_table: hpack_table,
824
808
  http2_streams: streams,
825
809
  http2_window: connection_window,
@@ -840,20 +824,95 @@ module Raptor
840
824
  end
841
825
  private_class_method :build_result
842
826
 
827
+ # @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
828
+ # @rbs @server_port_string: String
829
+ # @rbs @write_timeout: Integer
830
+ # @rbs @max_body_size: Integer?
831
+ # @rbs @access_log_io: IO?
832
+ # @rbs @clean_thread_locals: bool
833
+ # @rbs @clean_fiber_locals: bool
834
+ # @rbs @on_error: (^(Hash[String, untyped]?, Exception) -> void)?
835
+ # @rbs @running: AtomicBoolean
836
+ # @rbs @initial_settings_frame: String
837
+
838
+ # Creates a new Http2 handler.
839
+ #
840
+ # @param app [#call] the Rack application to dispatch requests to
841
+ # @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
842
+ # @param connection_options [Hash] per-connection settings shared across protocols
843
+ # @option connection_options [Integer] :write_timeout per-write socket timeout in seconds
844
+ # @option connection_options [Integer, nil] :max_body_size maximum request body size in bytes
845
+ # @param http2_options [Hash] HTTP/2-specific settings
846
+ # @option http2_options [Integer] :max_concurrent_streams maximum HTTP/2 concurrent streams per connection
847
+ # @param access_log_io [IO, nil] IO to write Common Log Format access entries to, or nil to disable
848
+ # @param clean_thread_locals [Boolean] whether to clear application thread locals after each request
849
+ # @param clean_fiber_locals [Boolean] whether to process each request in a fresh Fiber
850
+ # @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
851
+ # @return [void]
852
+ #
853
+ # @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
854
+ def initialize(app, server_port, connection_options: {}, http2_options: {}, access_log_io: nil, clean_thread_locals: true, clean_fiber_locals: false, on_error: nil)
855
+ @app = app
856
+ @server_port_string = server_port.to_s.freeze
857
+ @write_timeout = connection_options[:write_timeout] || Http::WRITE_TIMEOUT
858
+ @max_body_size = connection_options[:max_body_size]
859
+ @access_log_io = access_log_io
860
+ @clean_thread_locals = clean_thread_locals
861
+ @clean_fiber_locals = clean_fiber_locals
862
+ @on_error = on_error
863
+ @running = AtomicBoolean.new(true)
864
+
865
+ parser = Http2Parser.new
866
+ settings_payload = parser.build_settings(
867
+ max_concurrent_streams: http2_options[:max_concurrent_streams],
868
+ initial_window_size: DEFAULT_WINDOW_SIZE,
869
+ max_header_list_size: Http2Parser::MAX_HEADER_LIST_SIZE
870
+ )
871
+ @initial_settings_frame = parser.build_frame(:settings, 0, 0, settings_payload).freeze
872
+ end
873
+
874
+ # Returns a Ractor-safe proc that parses HTTP/2 frames from the
875
+ # state hash's buffered bytes.
876
+ #
877
+ # @return [Proc]
878
+ #
879
+ # @rbs () -> ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
880
+ def parser_worker
881
+ proc do |data|
882
+ Http2.process_frames(data)
883
+ end
884
+ end
885
+
886
+ # Stops new stream dispatch and asks clients to finish streams already
887
+ # handed to the application.
888
+ #
889
+ # @param reactor [Reactor] the reactor managing HTTP/2 connections
890
+ # @return [void]
891
+ #
892
+ # @rbs (Reactor reactor) -> void
893
+ def shutdown(reactor)
894
+ @running.make_false
895
+ parser = Http2Parser.new
896
+ reactor.drain_http2 do |stream_id|
897
+ payload = [stream_id, ERROR_NO_ERROR].pack("NN")
898
+ parser.build_frame(:goaway, 0, 0, payload)
899
+ end
900
+ end
901
+
843
902
  # Sends the server SETTINGS frame on a freshly negotiated HTTP/2
844
903
  # connection, then eagerly reads and parses the first client frame
845
904
  # batch, dispatching completed streams directly to the thread pool.
846
905
  # Falls back to the reactor when no initial data is ready.
847
906
  #
848
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
849
- # @param id [Integer] unique client identifier
907
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
908
+ # @param id [Integer] unique connection identifier
850
909
  # @param reactor [Reactor] the reactor managing the connection
851
910
  # @param thread_pool [AtomicThreadPool] thread pool for application processing
852
- # @param remote_addr [String] client IP address
853
- # @param url_scheme [String] "https"
911
+ # @param remote_addr [String] the client IP address
912
+ # @param url_scheme [String] "http" or "https"
854
913
  # @return [void]
855
914
  #
856
- # @rbs (OpenSSL::SSL::SSLSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
915
+ # @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
857
916
  def eager_accept(socket, id, reactor, thread_pool, remote_addr, url_scheme)
858
917
  unless @running.true?
859
918
  socket.close rescue nil
@@ -867,6 +926,7 @@ module Raptor
867
926
  initial_state = {
868
927
  id: id,
869
928
  protocol: :http2,
929
+ max_body_size: @max_body_size,
870
930
  remote_addr: remote_addr,
871
931
  url_scheme: url_scheme
872
932
  }
@@ -901,7 +961,7 @@ module Raptor
901
961
  buffer << socket.read_nonblock(socket.pending)
902
962
  end
903
963
 
904
- result = Raptor::Http2.process_frames(initial_state.merge(buffer: buffer))
964
+ result = Http2.process_frames(initial_state.merge(buffer: buffer))
905
965
  handle_parsed_request(result, reactor, thread_pool)
906
966
  rescue => error
907
967
  Log.rescued_error(error)
@@ -913,45 +973,45 @@ module Raptor
913
973
  # further buffered frame batches before returning control to the
914
974
  # reactor.
915
975
  #
916
- # @param result [Hash] the parsed result produced by {process_frames}
976
+ # @param parsed_request [Hash] the parsed result produced by {.process_frames}
917
977
  # @param reactor [Reactor] the reactor managing the connection
918
- # @param thread_pool [AtomicThreadPool] thread pool for Rack app dispatch
978
+ # @param thread_pool [AtomicThreadPool] thread pool for application processing
919
979
  # @return [void]
920
980
  #
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])
981
+ # @rbs (Hash[Symbol, untyped] parsed_request, Reactor reactor, AtomicThreadPool thread_pool) -> void
982
+ def handle_parsed_request(parsed_request, reactor, thread_pool)
983
+ socket = reactor.socket_for(parsed_request[:id])
924
984
  return unless socket
925
985
 
926
- writer = reactor.writer_for(result[:id])
927
- flow_control = reactor.flow_control_for(result[:id])
986
+ writer = reactor.writer_for(parsed_request[:id])
987
+ flow_control = reactor.flow_control_for(parsed_request[:id])
928
988
 
929
989
  rounds = 0
930
990
  loop do
931
- reactor.acknowledge_http2_ping(result[:id], result[:ping_acknowledgements])
932
- result[:cancelled_streams]&.each do |stream_id|
991
+ reactor.acknowledge_http2_ping(parsed_request[:id], parsed_request[:ping_acknowledgements])
992
+ parsed_request[:cancelled_streams]&.each do |stream_id|
933
993
  flow_control.cancel_stream(stream_id)
934
- reactor.cancel_http2_body(result[:id], stream_id)
994
+ reactor.cancel_http2_body(parsed_request[:id], stream_id)
935
995
  end
936
996
 
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])
997
+ if flow_control && (parsed_request[:window_updates]&.any? || parsed_request[:peer_initial_window_size])
998
+ apply_flow_control_updates(flow_control, parsed_request)
999
+ reactor.resume_http2_bodies(parsed_request[:id])
940
1000
  end
941
1001
 
942
- writer.write_frames(socket, result[:outgoing_frames])
1002
+ writer.write_frames(socket, parsed_request[:outgoing_frames])
943
1003
 
944
- if result[:close_connection]
945
- reactor.close_connection(result[:id])
1004
+ if parsed_request[:close_connection]
1005
+ reactor.close_connection(parsed_request[:id])
946
1006
  return
947
1007
  end
948
1008
 
949
- result[:completed_requests]&.each do |request|
1009
+ parsed_request[:completed_requests]&.each do |request|
950
1010
  stream_id = request[:stream_id]
951
- remote_addr = result[:remote_addr] || Server::DEFAULT_REMOTE_ADDR
1011
+ remote_addr = parsed_request[:remote_addr] || Server::DEFAULT_REMOTE_ADDR
952
1012
 
953
- unless reactor.dispatch_http2_stream(result[:id], stream_id)
954
- write_http2_reset_stream(socket, writer, stream_id, ERROR_REFUSED_STREAM)
1013
+ unless reactor.dispatch_http2_stream(parsed_request[:id], stream_id)
1014
+ write_reset_stream(socket, writer, stream_id, ERROR_REFUSED_STREAM)
955
1015
  next
956
1016
  end
957
1017
 
@@ -971,19 +1031,28 @@ module Raptor
971
1031
  next_batch = eager_read_next_batch(socket)
972
1032
  break unless next_batch
973
1033
 
974
- result = Raptor::Http2.process_frames(result.merge(buffer: result[:buffer] + next_batch))
1034
+ parsed_request = Http2.process_frames(parsed_request.merge(buffer: parsed_request[:buffer] + next_batch))
975
1035
  end
976
1036
 
977
- reactor.update_http2_state(result)
1037
+ reactor.update_state(parsed_request)
978
1038
  end
979
1039
 
980
1040
  private
981
1041
 
1042
+ # Creates a per-connection {Writer} configured with the handler's write timeout.
1043
+ #
1044
+ # @return [Writer] a new per-connection frame writer
1045
+ #
1046
+ # @rbs () -> Writer
1047
+ def create_writer
1048
+ Writer.new(write_timeout: @write_timeout)
1049
+ end
1050
+
982
1051
  # Applies inbound flow-control updates from a parsed result to the
983
- # connection's `FlowControl`.
1052
+ # connection's {FlowControl}.
984
1053
  #
985
- # @param flow_control [FlowControl] the per-connection flow controller
986
- # @param result [Hash] the parsed result from `process_frames`
1054
+ # @param flow_control [FlowControl] per-connection outbound flow controller
1055
+ # @param result [Hash] the parsed result from {.process_frames}
987
1056
  # @return [void]
988
1057
  #
989
1058
  # @rbs (FlowControl flow_control, Hash[Symbol, untyped] result) -> void
@@ -1004,22 +1073,19 @@ module Raptor
1004
1073
  # Reads the next frame batch from `socket` if it has already arrived,
1005
1074
  # without waiting on the shared collector thread.
1006
1075
  #
1007
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1076
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
1008
1077
  # @return [String, nil] the bytes read, or nil if nothing was available
1009
1078
  #
1010
- # @rbs (OpenSSL::SSL::SSLSocket socket) -> String?
1079
+ # @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket) -> String?
1011
1080
  def eager_read_next_batch(socket)
1012
1081
  return unless socket.wait_readable(0)
1013
1082
 
1014
- data = begin
1083
+ buffer = begin
1015
1084
  socket.read_nonblock(EAGER_READ_BUFFER_SIZE)
1016
1085
  rescue IO::WaitReadable, EOFError, IOError, SystemCallError
1017
1086
  return
1018
1087
  end
1019
1088
 
1020
- buffer = String.new
1021
- buffer << data
1022
-
1023
1089
  while socket.respond_to?(:pending) && socket.pending.positive?
1024
1090
  buffer << socket.read_nonblock(socket.pending)
1025
1091
  end
@@ -1030,8 +1096,8 @@ module Raptor
1030
1096
  # Dispatches a completed stream request to the Rack app and writes
1031
1097
  # the response back as HTTP/2 frames.
1032
1098
  #
1033
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1034
- # @param writer [Writer] lock-free frame writer for the connection
1099
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
1100
+ # @param writer [Writer] frame writer for the connection
1035
1101
  # @param flow_control [FlowControl] per-connection outbound flow controller
1036
1102
  # @param stream_id [Integer] the HTTP/2 stream identifier
1037
1103
  # @param headers [Array<Array(String, String)>] request headers
@@ -1039,12 +1105,12 @@ module Raptor
1039
1105
  # @param remote_addr [String] the client IP address
1040
1106
  # @return [void]
1041
1107
  #
1042
- # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
1108
+ # @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
1109
  def dispatch_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:)
1044
1110
  if @clean_fiber_locals
1045
- Fiber.new { perform_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:) }.resume
1111
+ Fiber.new { perform_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr: remote_addr) }.resume
1046
1112
  else
1047
- perform_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:)
1113
+ perform_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr: remote_addr)
1048
1114
  end
1049
1115
  ensure
1050
1116
  ThreadLocals.clear if @clean_thread_locals
@@ -1052,8 +1118,8 @@ module Raptor
1052
1118
 
1053
1119
  # Calls the Rack app and writes its response for one stream.
1054
1120
  #
1055
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1056
- # @param writer [Writer] lock-free frame writer for the connection
1121
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
1122
+ # @param writer [Writer] frame writer for the connection
1057
1123
  # @param flow_control [FlowControl] per-connection outbound flow controller
1058
1124
  # @param stream_id [Integer] the HTTP/2 stream identifier
1059
1125
  # @param headers [Array<Array(String, String)>] request headers
@@ -1061,7 +1127,7 @@ module Raptor
1061
1127
  # @param remote_addr [String] the client IP address
1062
1128
  # @return [void]
1063
1129
  #
1064
- # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
1130
+ # @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
1131
  def perform_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:)
1066
1132
  env = nil
1067
1133
  status = nil
@@ -1081,14 +1147,14 @@ module Raptor
1081
1147
  Http.call_response_finished(env, status, response_headers, error)
1082
1148
  flow_control.discard_stream(stream_id)
1083
1149
  end
1084
- write_http2_detached_response(socket, writer, flow_control, stream_id, status, response_headers, response_body, finished) do
1150
+ write_detached_response(socket, writer, flow_control, stream_id, status, response_headers, response_body, finished) do
1085
1151
  response_started = true
1086
1152
  end
1087
1153
  detached = true
1088
1154
  return
1089
1155
  end
1090
1156
 
1091
- response_size = write_http2_response(
1157
+ response_size = write_response(
1092
1158
  socket,
1093
1159
  writer,
1094
1160
  flow_control,
@@ -1099,11 +1165,6 @@ module Raptor
1099
1165
  trailers: env[RESPONSE_TRAILERS],
1100
1166
  request_method: env[Rack::REQUEST_METHOD]
1101
1167
  ) { response_started = true }
1102
- if response_body.is_a?(DetachedBody)
1103
- response_body.finish(:closed)
1104
- elsif response_body.respond_to?(:close)
1105
- response_body.close
1106
- end
1107
1168
  write_access_log(env, status, response_size, remote_addr) if @access_log_io
1108
1169
  Http.call_response_finished(env, status, response_headers, nil)
1109
1170
  rescue StreamClosedError => error
@@ -1111,9 +1172,9 @@ module Raptor
1111
1172
  rescue => error
1112
1173
  Http.call_response_finished(env, status, response_headers, error)
1113
1174
  if response_started
1114
- write_http2_reset_stream(socket, writer, stream_id, ERROR_INTERNAL_ERROR)
1175
+ write_reset_stream(socket, writer, stream_id, ERROR_INTERNAL_ERROR)
1115
1176
  else
1116
- write_http2_error_response(socket, writer, stream_id)
1177
+ write_error_response(socket, writer, stream_id)
1117
1178
  end
1118
1179
 
1119
1180
  if @on_error
@@ -1127,8 +1188,8 @@ module Raptor
1127
1188
 
1128
1189
  # Starts a detached HTTP/2 response after writing its response headers.
1129
1190
  #
1130
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1131
- # @param writer [Writer] lock-free frame writer for the connection
1191
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
1192
+ # @param writer [Writer] frame writer for the connection
1132
1193
  # @param flow_control [FlowControl] per-connection outbound flow controller
1133
1194
  # @param stream_id [Integer] the HTTP/2 stream identifier
1134
1195
  # @param status [Integer] HTTP status code
@@ -1138,22 +1199,22 @@ module Raptor
1138
1199
  # @yield once the response headers are queued
1139
1200
  # @return [void]
1140
1201
  #
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)
1202
+ # @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
1203
+ def write_detached_response(socket, writer, flow_control, stream_id, status, headers, body, finished)
1143
1204
  flow_control.check(stream_id)
1144
1205
  parser = Http2Parser.new
1145
1206
  encoded = parser.encode_response_headers(status, headers)
1146
1207
  writer.write_frames(socket, [parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded)])
1147
1208
  yield
1148
1209
  attached = writer.attach_body(stream_id, body, finished)
1149
- write_http2_reset_stream(socket, writer, stream_id, ERROR_REFUSED_STREAM) unless attached
1210
+ write_reset_stream(socket, writer, stream_id, ERROR_REFUSED_STREAM) unless attached
1150
1211
  end
1151
1212
 
1152
1213
  # Writes a Rack response as HTTP/2 frames to the socket, partitioning
1153
1214
  # DATA frames through `flow_control` to fit within the peer's windows.
1154
1215
  #
1155
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1156
- # @param writer [Writer] lock-free frame writer for the connection
1216
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
1217
+ # @param writer [Writer] frame writer for the connection
1157
1218
  # @param flow_control [FlowControl] per-connection outbound flow controller
1158
1219
  # @param stream_id [Integer] the HTTP/2 stream identifier
1159
1220
  # @param status [Integer] HTTP status code
@@ -1161,10 +1222,11 @@ module Raptor
1161
1222
  # @param body [Object] response body responding to each
1162
1223
  # @param trailers [Hash] trailing response headers populated by the Rack application
1163
1224
  # @param request_method [String] request method used to suppress HEAD response bodies
1225
+ # @yield once the response headers are queued
1164
1226
  # @return [String] the response body size in bytes
1165
1227
  #
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:)
1228
+ # @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
1229
+ def write_response(socket, writer, flow_control, stream_id, status, headers, body, trailers:, request_method:)
1168
1230
  parser = Http2Parser.new
1169
1231
 
1170
1232
  encoded_headers = parser.encode_response_headers(status, headers)
@@ -1188,18 +1250,24 @@ module Raptor
1188
1250
  stream.close
1189
1251
 
1190
1252
  stream.bytes_written.to_s
1253
+ ensure
1254
+ if body.is_a?(DetachedBody)
1255
+ body.finish($! ? :cancelled : :closed)
1256
+ elsif body.respond_to?(:close)
1257
+ body.close
1258
+ end
1191
1259
  end
1192
1260
 
1193
1261
  # Sends an HTTP 103 Early Hints response.
1194
1262
  #
1195
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1263
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
1196
1264
  # @param writer [Writer] frame writer for the connection
1197
1265
  # @param flow_control [FlowControl] per-connection outbound flow controller
1198
1266
  # @param stream_id [Integer] the HTTP/2 stream identifier
1199
1267
  # @param hints [Hash] response headers to send as early hints
1200
1268
  # @return [void]
1201
1269
  #
1202
- # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Hash[String, String | Array[String]] hints) -> void
1270
+ # @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Hash[String, String | Array[String]] hints) -> void
1203
1271
  def send_early_hints(socket, writer, flow_control, stream_id, hints)
1204
1272
  return if hints.empty?
1205
1273
 
@@ -1211,14 +1279,14 @@ module Raptor
1211
1279
 
1212
1280
  # Resets one HTTP/2 stream with the given error code.
1213
1281
  #
1214
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1282
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
1215
1283
  # @param writer [Writer] frame writer for the connection
1216
1284
  # @param stream_id [Integer] the HTTP/2 stream identifier
1217
1285
  # @param error_code [Integer] the HTTP/2 error code
1218
1286
  # @return [void]
1219
1287
  #
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)
1288
+ # @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Integer error_code) -> void
1289
+ def write_reset_stream(socket, writer, stream_id, error_code)
1222
1290
  parser = Http2Parser.new
1223
1291
  frame = parser.build_frame(:rst_stream, 0, stream_id, [error_code].pack("N"))
1224
1292
  writer.write_frames(socket, [frame])
@@ -1226,13 +1294,13 @@ module Raptor
1226
1294
 
1227
1295
  # Writes a 500 error response as HTTP/2 frames.
1228
1296
  #
1229
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1230
- # @param writer [Writer] lock-free frame writer for the connection
1297
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
1298
+ # @param writer [Writer] frame writer for the connection
1231
1299
  # @param stream_id [Integer] the HTTP/2 stream identifier
1232
1300
  # @return [void]
1233
1301
  #
1234
- # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id) -> void
1235
- def write_http2_error_response(socket, writer, stream_id)
1302
+ # @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id) -> void
1303
+ def write_error_response(socket, writer, stream_id)
1236
1304
  parser = Http2Parser.new
1237
1305
  encoded = parser.encode_headers([[":status", "500"]])
1238
1306
 
@@ -1242,33 +1310,21 @@ module Raptor
1242
1310
  )
1243
1311
  end
1244
1312
 
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
1313
  # Builds a Rack environment hash from HTTP/2 headers and body.
1260
1314
  #
1261
1315
  # @param headers [Array<Array(String, String)>] HTTP/2 header pairs
1262
1316
  # @param body [String] the request body
1263
- # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1317
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
1264
1318
  # @param writer [Writer] frame writer for the connection
1319
+ # @param flow_control [FlowControl] per-connection outbound flow controller
1265
1320
  # @param stream_id [Integer] the HTTP/2 stream identifier
1266
1321
  # @param remote_addr [String] the client IP address
1267
1322
  # @return [Hash] fully populated Rack environment hash
1268
1323
  #
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]
1324
+ # @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
1325
  def build_rack_env(headers, body, socket, writer, flow_control, stream_id, remote_addr:)
1271
1326
  env = {}
1327
+ underscored_headers = nil
1272
1328
 
1273
1329
  headers.each do |name, value|
1274
1330
  if name.start_with?(":")
@@ -1287,7 +1343,19 @@ module Raptor
1287
1343
  env[Http::CONTENT_LENGTH] = value
1288
1344
  else
1289
1345
  rack_key = "HTTP_#{name.upcase.tr("-", "_")}"
1290
- env[rack_key] = value
1346
+ if name.include?("_")
1347
+ next if env.key?(rack_key) && !underscored_headers&.key?(rack_key)
1348
+
1349
+ (underscored_headers ||= {})[rack_key] = true
1350
+ elsif underscored_headers&.delete(rack_key)
1351
+ env.delete(rack_key)
1352
+ end
1353
+ if (existing = env[rack_key])
1354
+ separator = rack_key == Rack::HTTP_COOKIE ? "; " : ", "
1355
+ env[rack_key] = "#{existing}#{separator}#{value}"
1356
+ else
1357
+ env[rack_key] = value
1358
+ end
1291
1359
  end
1292
1360
  end
1293
1361
 
@@ -1311,7 +1379,6 @@ module Raptor
1311
1379
 
1312
1380
  env[Http::REMOTE_ADDR] = remote_addr
1313
1381
  env[Http::SERVER_SOFTWARE] = Http::SERVER_SOFTWARE_VALUE
1314
- env[Http::HTTP_VERSION] = SERVER_PROTOCOL
1315
1382
  env[RESPONSE_TRAILERS] = {}
1316
1383
 
1317
1384
  populate_server_name_and_port(env)
@@ -1335,12 +1402,25 @@ module Raptor
1335
1402
  else
1336
1403
  host, port = http_host.split(":", 2)
1337
1404
  end
1338
- env[Rack::SERVER_NAME] ||= host
1339
- env[Rack::SERVER_PORT] ||= port || @server_port.to_s
1405
+ env[Rack::SERVER_NAME] ||= host || Server::DEFAULT_SERVER_NAME
1406
+ env[Rack::SERVER_PORT] ||= port || @server_port_string
1340
1407
  else
1341
1408
  env[Rack::SERVER_NAME] ||= Server::DEFAULT_SERVER_NAME
1342
- env[Rack::SERVER_PORT] ||= @server_port.to_s
1409
+ env[Rack::SERVER_PORT] ||= @server_port_string
1343
1410
  end
1344
1411
  end
1412
+
1413
+ # Writes a completed request to the access log.
1414
+ #
1415
+ # @param env [Hash] the Rack environment
1416
+ # @param status [Integer] the response status code
1417
+ # @param size [String] the response body size in bytes, or `-` if unknown
1418
+ # @param remote_addr [String] the client IP address
1419
+ # @return [void]
1420
+ #
1421
+ # @rbs (Hash[String, untyped] env, Integer status, String size, String remote_addr) -> void
1422
+ def write_access_log(env, status, size, remote_addr)
1423
+ Http.write_access_log(@access_log_io, env, status, size, remote_addr)
1424
+ end
1345
1425
  end
1346
1426
  end