raptor 0.20.2 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/raptor/http2.rb CHANGED
@@ -4,8 +4,11 @@
4
4
  require "stringio"
5
5
 
6
6
  require "atomic-ruby/atom"
7
+ require "atomic-ruby/atomic_boolean"
8
+ require "atomic-ruby/atomic_condition_variable"
7
9
  require "rack"
8
10
 
11
+ require_relative "detached_body"
9
12
  require_relative "http"
10
13
  require_relative "raptor_http2"
11
14
  require_relative "thread_locals"
@@ -14,13 +17,14 @@ module Raptor
14
17
  # Handles HTTP/2 request processing and Rack application integration.
15
18
  #
16
19
  class Http2
17
- # Serialises concurrent frame writes on a single HTTP/2 connection so
18
- # exactly one thread is writing at any moment.
20
+ RESPONSE_TRAILERS = "raptor.response_trailers"
21
+
22
+ # Queues concurrent frame writes while the reactor remains the sole
23
+ # owner of the connection socket.
19
24
  #
20
25
  class Writer
21
- IDLE = :idle
22
-
23
- # @rbs @state: Atom
26
+ # @rbs @reactor: Reactor?
27
+ # @rbs @connection_id: Integer?
24
28
  # @rbs @write_timeout: Integer
25
29
 
26
30
  # Creates a new Writer.
@@ -30,57 +34,63 @@ module Raptor
30
34
  #
31
35
  # @rbs (write_timeout: Integer) -> void
32
36
  def initialize(write_timeout:)
33
- @state = Atom.new(IDLE)
37
+ @reactor = nil
38
+ @connection_id = nil
34
39
  @write_timeout = write_timeout
35
40
  end
36
41
 
37
- # Writes frames to the socket, coordinating with concurrent writers
38
- # so that exactly one thread is actively writing at any time.
42
+ # Routes subsequent frame writes through the connection's reactor.
43
+ #
44
+ # @param reactor [Reactor] the reactor that owns the connection
45
+ # @param connection_id [Integer] unique connection identifier
46
+ # @return [void]
47
+ #
48
+ # @rbs (Reactor reactor, Integer connection_id) -> void
49
+ def attach(reactor, connection_id)
50
+ @reactor = reactor
51
+ @connection_id = connection_id
52
+ end
53
+
54
+ # Queues frames for the reactor to write. Before attachment, writes
55
+ # directly so the writer remains usable during connection setup.
39
56
  #
40
57
  # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
41
- # @param frames [Array<String>] frame bytes to write in order
58
+ # @param frames [Array<String>, nil] frame bytes to write in order
42
59
  # @return [void]
43
60
  #
44
- # @rbs (OpenSSL::SSL::SSLSocket socket, Array[String] frames) -> void
61
+ # @rbs (OpenSSL::SSL::SSLSocket socket, Array[String]? frames) -> void
45
62
  def write_frames(socket, frames)
46
63
  return if !frames || frames.empty?
47
64
 
48
- claimed = false
49
- @state.swap do |current|
50
- if current.equal?(IDLE)
51
- claimed = true
52
- frames
53
- else
54
- claimed = false
55
- current + frames
56
- end
65
+ if @reactor
66
+ @reactor.write_http2_frames(@connection_id, frames)
67
+ else
68
+ Http.socket_write(socket, frames.join, timeout: @write_timeout) rescue nil
57
69
  end
70
+ end
58
71
 
59
- return unless claimed
60
-
61
- loop do
62
- pending = nil
63
- @state.swap do |current|
64
- pending = current
65
- current.empty? ? IDLE : []
66
- end
67
-
68
- break if pending.empty?
69
-
70
- Http.socket_write(socket, pending.join, timeout: @write_timeout) rescue nil
71
- end
72
+ # Attaches a response body that outlives its application thread.
73
+ #
74
+ # @rbs (Integer stream_id, DetachedBody body, ^(Symbol) -> void finished) -> bool
75
+ def attach_body(stream_id, body, finished)
76
+ @reactor.attach_http2_body(@connection_id, stream_id, body, finished)
72
77
  end
73
78
  end
74
79
 
75
- # Tracks the peer's connection-level and per-stream receive windows so
76
- # outbound `DATA` frames respect RFC 7540 section 5.2.
80
+ class StreamClosedError < StandardError
81
+ # @rbs () -> String
82
+ def message = "HTTP/2 stream closed"
83
+ end
84
+
85
+ # Coordinates outbound flow control and stream cancellation without
86
+ # polling or mutexes.
77
87
  #
78
88
  class FlowControl
79
- ACQUIRE_POLL_INTERVAL = 0.001
80
-
81
89
  # @rbs @connection_window: Atom
82
90
  # @rbs @stream_windows: Atom
83
91
  # @rbs @initial_stream_window: Atom
92
+ # @rbs @cancelled_streams: Atom
93
+ # @rbs @available: AtomicConditionVariable
84
94
 
85
95
  # Creates a new FlowControl with the spec-default windows.
86
96
  #
@@ -91,32 +101,25 @@ module Raptor
91
101
  @connection_window = Atom.new(DEFAULT_WINDOW_SIZE)
92
102
  @stream_windows = Atom.new({})
93
103
  @initial_stream_window = Atom.new(DEFAULT_WINDOW_SIZE)
104
+ @cancelled_streams = Atom.new({})
105
+ @available = AtomicConditionVariable.new
94
106
  end
95
107
 
96
- # Reserves outbound capacity on the given stream, polling until at
108
+ # Reserves outbound capacity on the given stream, waiting until at
97
109
  # least one byte is available on both the connection and stream
98
- # windows. The returned size is capped at `MAX_FRAME_SIZE`.
110
+ # windows. Raises when the stream or connection closes.
99
111
  #
100
112
  # @param stream_id [Integer] the HTTP/2 stream identifier
101
113
  # @param max_bytes [Integer] the largest size the caller would like to send
102
- # @param end_stream [Boolean] true when this is the final frame on the stream
103
114
  # @return [Integer] the number of bytes the caller may now send
104
115
  #
105
- # @rbs (Integer stream_id, Integer max_bytes, ?end_stream: bool) -> Integer
106
- def acquire(stream_id, max_bytes, end_stream: false)
116
+ # @rbs (Integer stream_id, Integer max_bytes) -> Integer
117
+ def acquire(stream_id, max_bytes)
107
118
  initial = @initial_stream_window.value
108
119
  capped = max_bytes < MAX_FRAME_SIZE ? max_bytes : MAX_FRAME_SIZE
109
120
 
110
- if end_stream && capped <= initial && !@stream_windows.value.key?(stream_id)
111
- loop do
112
- granted = reserve_connection(capped)
113
- return granted if granted.positive?
114
-
115
- sleep ACQUIRE_POLL_INTERVAL
116
- end
117
- end
118
-
119
- loop do
121
+ @available.wait do
122
+ check(stream_id)
120
123
  stream_window = @stream_windows.value[stream_id] || initial
121
124
  capped_full = capped < stream_window ? capped : stream_window
122
125
  granted = capped_full.positive? ? reserve_connection(capped_full) : 0
@@ -126,11 +129,33 @@ module Raptor
126
129
  current = windows[stream_id] || initial
127
130
  windows.merge(stream_id => current - granted)
128
131
  end
129
- return granted
132
+ granted
130
133
  end
134
+ end
135
+ end
131
136
 
132
- sleep ACQUIRE_POLL_INTERVAL
137
+ # Reserves outbound capacity without waiting for a window update.
138
+ #
139
+ # @param stream_id [Integer] the HTTP/2 stream identifier
140
+ # @param max_bytes [Integer] the largest size the caller would like to send
141
+ # @return [Integer] available bytes, or 0 when flow control is exhausted
142
+ #
143
+ # @rbs (Integer stream_id, Integer max_bytes) -> Integer
144
+ def try_acquire(stream_id, max_bytes)
145
+ check(stream_id)
146
+ initial = @initial_stream_window.value
147
+ stream_window = @stream_windows.value[stream_id] || initial
148
+ capped = [max_bytes, MAX_FRAME_SIZE, stream_window].min
149
+ return 0 unless capped.positive?
150
+
151
+ granted = reserve_connection(capped)
152
+ if granted.positive?
153
+ @stream_windows.swap do |windows|
154
+ current = windows[stream_id] || initial
155
+ windows.merge(stream_id => current - granted)
156
+ end
133
157
  end
158
+ granted
134
159
  end
135
160
 
136
161
  # Increments the connection-level send window by `increment` bytes.
@@ -141,6 +166,7 @@ module Raptor
141
166
  # @rbs (Integer increment) -> void
142
167
  def add_connection_window(increment)
143
168
  @connection_window.swap { |window| window + increment }
169
+ @available.broadcast
144
170
  end
145
171
 
146
172
  # Increments the send window for the given stream by `increment` bytes.
@@ -156,6 +182,7 @@ module Raptor
156
182
  current = windows[stream_id] || initial
157
183
  windows.merge(stream_id => current + increment)
158
184
  end
185
+ @available.broadcast
159
186
  end
160
187
 
161
188
  # Updates the peer's `SETTINGS_INITIAL_WINDOW_SIZE`. Shifts every
@@ -174,6 +201,40 @@ module Raptor
174
201
  @stream_windows.swap do |windows|
175
202
  windows.transform_values { |size| size + delta }
176
203
  end
204
+ @available.broadcast
205
+ end
206
+
207
+ # Raises when the given stream can no longer write a response.
208
+ #
209
+ # @param stream_id [Integer] the HTTP/2 stream identifier
210
+ # @return [void]
211
+ # @raise [StreamClosedError] if the stream or connection has closed
212
+ #
213
+ # @rbs (Integer stream_id) -> void
214
+ def check(stream_id)
215
+ cancelled = @cancelled_streams.value
216
+ raise StreamClosedError if cancelled.key?(0) || cancelled.key?(stream_id)
217
+ end
218
+
219
+ # Cancels pending and future writes for the given stream.
220
+ #
221
+ # @param stream_id [Integer] the HTTP/2 stream identifier
222
+ # @return [void]
223
+ #
224
+ # @rbs (Integer stream_id) -> void
225
+ def cancel_stream(stream_id)
226
+ @cancelled_streams.swap { |streams| streams.merge(stream_id => true) }
227
+ @available.broadcast
228
+ end
229
+
230
+ # Cancels pending and future writes for the connection.
231
+ #
232
+ # @return [void]
233
+ #
234
+ # @rbs () -> void
235
+ def close
236
+ @cancelled_streams.swap { |streams| streams.merge(0 => true) }
237
+ @available.broadcast
177
238
  end
178
239
 
179
240
  # Discards any per-stream tracking for the given stream.
@@ -183,8 +244,6 @@ module Raptor
183
244
  #
184
245
  # @rbs (Integer stream_id) -> void
185
246
  def discard_stream(stream_id)
186
- return unless @stream_windows.value.key?(stream_id)
187
-
188
247
  @stream_windows.swap do |windows|
189
248
  next windows unless windows.key?(stream_id)
190
249
 
@@ -192,6 +251,13 @@ module Raptor
192
251
  pruned.delete(stream_id)
193
252
  pruned
194
253
  end
254
+ @cancelled_streams.swap do |streams|
255
+ next streams unless streams.key?(stream_id)
256
+
257
+ pruned = streams.dup
258
+ pruned.delete(stream_id)
259
+ pruned
260
+ end
195
261
  end
196
262
 
197
263
  private
@@ -214,6 +280,142 @@ module Raptor
214
280
  end
215
281
  end
216
282
 
283
+ # Adapts one HTTP/2 response stream to Rack's streaming body interface.
284
+ #
285
+ class ResponseStream
286
+ # @rbs @socket: OpenSSL::SSL::SSLSocket
287
+ # @rbs @writer: Writer
288
+ # @rbs @flow_control: FlowControl
289
+ # @rbs @stream_id: Integer
290
+ # @rbs @trailers: Hash[String, String | Array[String]]
291
+ # @rbs @parser: Http2Parser
292
+ # @rbs @bytes_written: Integer
293
+ # @rbs @closed: bool
294
+
295
+ # Returns the number of response body bytes written to the stream.
296
+ #
297
+ # @return [Integer]
298
+ attr_reader :bytes_written #: Integer
299
+
300
+ # Creates a response stream for a Rack streaming body.
301
+ #
302
+ # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
303
+ # @param writer [Writer] frame writer for the connection
304
+ # @param flow_control [FlowControl] outbound flow controller
305
+ # @param stream_id [Integer] the HTTP/2 stream identifier
306
+ # @param trailers [Hash] trailing response headers populated by the Rack application
307
+ # @return [void]
308
+ #
309
+ # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Hash[String, String | Array[String]] trailers) -> void
310
+ def initialize(socket, writer, flow_control, stream_id, trailers)
311
+ @socket = socket
312
+ @writer = writer
313
+ @flow_control = flow_control
314
+ @stream_id = stream_id
315
+ @trailers = trailers
316
+ @parser = Http2Parser.new
317
+ @bytes_written = 0
318
+ @closed = false
319
+ end
320
+
321
+ # Streaming responses are write-only.
322
+ #
323
+ # @raise [IOError]
324
+ #
325
+ # @rbs (*untyped) -> bot
326
+ def read(*)
327
+ raise IOError, "not opened for reading"
328
+ end
329
+
330
+ # Writes response body bytes to the stream.
331
+ #
332
+ # @param chunk [String] response body bytes
333
+ # @return [Integer] number of bytes written
334
+ #
335
+ # @rbs (String chunk) -> Integer
336
+ def write(chunk)
337
+ raise IOError, "closed stream" if @closed
338
+ raise TypeError, "body must yield String values" unless chunk.is_a?(String)
339
+
340
+ offset = 0
341
+ while offset < chunk.bytesize
342
+ granted = @flow_control.acquire(@stream_id, chunk.bytesize - offset)
343
+ slice = offset.zero? && granted == chunk.bytesize ? chunk : chunk.byteslice(offset, granted)
344
+ offset += granted
345
+ @writer.write_frames(@socket, [@parser.build_frame(:data, 0, @stream_id, slice)])
346
+ end
347
+
348
+ @bytes_written += chunk.bytesize
349
+ chunk.bytesize
350
+ end
351
+
352
+ # Writes response body bytes and returns the stream.
353
+ #
354
+ # @param chunk [String] response body bytes
355
+ # @return [ResponseStream]
356
+ #
357
+ # @rbs (String chunk) -> ResponseStream
358
+ def <<(chunk)
359
+ write(chunk)
360
+ self
361
+ end
362
+
363
+ # Flushes the stream. Frame writes are already immediate.
364
+ #
365
+ # @return [ResponseStream]
366
+ #
367
+ # @rbs () -> ResponseStream
368
+ def flush
369
+ self
370
+ end
371
+
372
+ # Finishes the stream.
373
+ #
374
+ # @return [void]
375
+ #
376
+ # @rbs () -> void
377
+ def close
378
+ return if @closed
379
+
380
+ @flow_control.check(@stream_id)
381
+ @closed = true
382
+ if @trailers.empty?
383
+ frame = @parser.build_frame(:data, FLAG_END_STREAM, @stream_id, nil)
384
+ else
385
+ encoded = @parser.encode_response_trailers(@trailers)
386
+ frame = @parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, @stream_id, encoded)
387
+ end
388
+ @writer.write_frames(@socket, [frame])
389
+ end
390
+
391
+ # Closes the unsupported read side of the stream.
392
+ #
393
+ # @raise [IOError]
394
+ #
395
+ # @rbs () -> bot
396
+ def close_read
397
+ raise IOError, "closing non-duplex IO for reading"
398
+ end
399
+
400
+ # Finishes the writable side of the stream.
401
+ #
402
+ # @return [void]
403
+ #
404
+ # @rbs () -> void
405
+ def close_write
406
+ close
407
+ end
408
+
409
+ # Returns whether the stream has finished.
410
+ #
411
+ # @return [Boolean]
412
+ #
413
+ # @rbs () -> bool
414
+ def closed?
415
+ @closed
416
+ end
417
+ end
418
+
217
419
  EAGER_READ_TIMEOUT = 0.001
218
420
  EAGER_READ_BUFFER_SIZE = 64 * 1024
219
421
  EAGER_MAX_ROUNDS = 8
@@ -225,6 +427,8 @@ module Raptor
225
427
 
226
428
  ERROR_NO_ERROR = 0x0
227
429
  ERROR_PROTOCOL_ERROR = 0x1
430
+ ERROR_INTERNAL_ERROR = 0x2
431
+ ERROR_REFUSED_STREAM = 0x7
228
432
 
229
433
  DEFAULT_WINDOW_SIZE = 65_535
230
434
  MAX_FRAME_SIZE = 16_384
@@ -272,6 +476,7 @@ module Raptor
272
476
  # @rbs @clean_fiber_locals: bool
273
477
  # @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
274
478
  # @rbs @initial_settings_frame: String
479
+ # @rbs @running: AtomicBoolean
275
480
 
276
481
  # Returns the initial server SETTINGS frame to send on every new
277
482
  # HTTP/2 connection.
@@ -302,6 +507,7 @@ module Raptor
302
507
  @clean_thread_locals = clean_thread_locals
303
508
  @clean_fiber_locals = clean_fiber_locals
304
509
  @on_error = on_error
510
+ @running = AtomicBoolean.new(true)
305
511
 
306
512
  parser = Http2Parser.new
307
513
  settings_payload = parser.build_settings(
@@ -320,6 +526,22 @@ module Raptor
320
526
  Writer.new(write_timeout: @write_timeout)
321
527
  end
322
528
 
529
+ # Stops new stream dispatch and asks clients to finish streams already
530
+ # handed to the application.
531
+ #
532
+ # @param reactor [Reactor] the reactor managing HTTP/2 connections
533
+ # @return [void]
534
+ #
535
+ # @rbs (Reactor reactor) -> void
536
+ def shutdown(reactor)
537
+ @running.make_false
538
+ parser = Http2Parser.new
539
+ reactor.drain_http2 do |stream_id|
540
+ payload = [stream_id, ERROR_NO_ERROR].pack("NN")
541
+ parser.build_frame(:goaway, 0, 0, payload)
542
+ end
543
+ end
544
+
323
545
  # Returns a Ractor-safe proc that parses HTTP/2 frames from the
324
546
  # state hash's buffered bytes.
325
547
  #
@@ -347,7 +569,9 @@ module Raptor
347
569
  streams = data[:http2_streams] ? data[:http2_streams].dup : {}
348
570
  outgoing_frames = []
349
571
  completed_requests = []
572
+ cancelled_streams = []
350
573
  window_updates = []
574
+ ping_acknowledgements = []
351
575
  peer_initial_window_size = nil
352
576
  connection_window = data[:http2_window] || DEFAULT_WINDOW_SIZE
353
577
  preface_received = data[:http2_preface_received] || false
@@ -360,7 +584,7 @@ module Raptor
360
584
  buffer = buffer.byteslice(24..-1) || ""
361
585
  preface_received = true
362
586
  else
363
- return build_result(data, buffer, hpack_table, streams, outgoing_frames, completed_requests, window_updates, peer_initial_window_size, connection_window, preface_received, last_client_stream_id, pending_headers, false)
587
+ return 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, false)
364
588
  end
365
589
  end
366
590
 
@@ -387,8 +611,9 @@ module Raptor
387
611
  when :headers
388
612
  stream_id = frame[:stream_id]
389
613
  header_payload = frame[:payload]
614
+ trailers = streams.key?(stream_id)
390
615
 
391
- unless streams.key?(stream_id)
616
+ unless trailers
392
617
  if stream_id.even? || stream_id <= last_client_stream_id
393
618
  goaway_error = ERROR_PROTOCOL_ERROR
394
619
  break
@@ -404,14 +629,12 @@ module Raptor
404
629
 
405
630
  if frame[:flags].anybits?(FLAG_END_HEADERS)
406
631
  decoded_headers, hpack_table = parser.parse_headers(header_payload, hpack_table)
407
- if invalid_pseudo_headers?(decoded_headers)
408
- streams.delete(stream_id)
409
- outgoing_frames << parser.build_frame(:rst_stream, 0, stream_id, [ERROR_PROTOCOL_ERROR].pack("N"))
410
- else
411
- streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, end_stream)
412
- end
632
+ streams, completed_requests = complete_header_block(
633
+ streams, completed_requests, outgoing_frames, parser,
634
+ stream_id, decoded_headers, end_stream, trailers
635
+ )
413
636
  else
414
- pending_headers = { stream_id: stream_id, buffer: header_payload, end_stream: end_stream }
637
+ pending_headers = { stream_id: stream_id, buffer: header_payload, end_stream: end_stream, trailers: trailers }
415
638
  end
416
639
 
417
640
  when :continuation
@@ -425,12 +648,10 @@ module Raptor
425
648
  if frame[:flags].anybits?(FLAG_END_HEADERS)
426
649
  stream_id = pending_headers[:stream_id]
427
650
  decoded_headers, hpack_table = parser.parse_headers(pending_headers[:buffer], hpack_table)
428
- if invalid_pseudo_headers?(decoded_headers)
429
- streams.delete(stream_id)
430
- outgoing_frames << parser.build_frame(:rst_stream, 0, stream_id, [ERROR_PROTOCOL_ERROR].pack("N"))
431
- else
432
- streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, pending_headers[:end_stream])
433
- end
651
+ streams, completed_requests = complete_header_block(
652
+ streams, completed_requests, outgoing_frames, parser,
653
+ stream_id, decoded_headers, pending_headers[:end_stream], pending_headers[:trailers]
654
+ )
434
655
  pending_headers = nil
435
656
  end
436
657
 
@@ -475,7 +696,9 @@ module Raptor
475
696
  window_updates << [frame[:stream_id], increment]
476
697
 
477
698
  when :ping
478
- if frame[:flags].nobits?(FLAG_ACK)
699
+ if frame[:flags].anybits?(FLAG_ACK)
700
+ ping_acknowledgements << frame[:payload]
701
+ else
479
702
  outgoing_frames << parser.build_frame(:ping, FLAG_ACK, 0, frame[:payload])
480
703
  end
481
704
 
@@ -483,7 +706,10 @@ module Raptor
483
706
  break
484
707
 
485
708
  when :rst_stream
486
- streams.delete(frame[:stream_id])
709
+ stream_id = frame[:stream_id]
710
+ streams.delete(stream_id)
711
+ completed_requests.reject! { |request| request[:stream_id] == stream_id }
712
+ cancelled_streams << stream_id
487
713
  end
488
714
  end
489
715
 
@@ -492,8 +718,47 @@ module Raptor
492
718
  outgoing_frames << parser.build_frame(:goaway, 0, 0, goaway_payload)
493
719
  end
494
720
 
495
- build_result(data, buffer, hpack_table, streams, outgoing_frames, completed_requests, window_updates, peer_initial_window_size, connection_window, preface_received, last_client_stream_id, pending_headers, !!goaway_error)
721
+ 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)
722
+ end
723
+
724
+ # Applies one complete request header block to its stream, resetting
725
+ # malformed initial headers or trailers and completing valid trailers.
726
+ #
727
+ # @param streams [Hash] current open-stream map
728
+ # @param completed_requests [Array<Hash>] accumulator of completed stream requests
729
+ # @param outgoing_frames [Array<String>] accumulator of protocol response frames
730
+ # @param parser [Http2Parser] frame parser and encoder
731
+ # @param stream_id [Integer] the stream identifier
732
+ # @param decoded_headers [Array<Array(String, String)>] decoded header pairs
733
+ # @param end_stream [Boolean] whether the source frame had END_STREAM set
734
+ # @param trailers [Boolean] whether this is a trailing header block
735
+ # @return [Array(Hash, Array<Hash>)] updated streams and completed requests
736
+ #
737
+ # @rbs (Hash[Integer, Hash[Symbol, untyped]] streams, Array[Hash[Symbol, untyped]] completed_requests, Array[String] outgoing_frames, Http2Parser parser, Integer stream_id, Array[[String, String]] decoded_headers, bool end_stream, bool trailers) -> [Hash[Integer, Hash[Symbol, untyped]], Array[Hash[Symbol, untyped]]]
738
+ def self.complete_header_block(streams, completed_requests, outgoing_frames, parser, stream_id, decoded_headers, end_stream, trailers)
739
+ invalid = if trailers
740
+ !end_stream || decoded_headers.any? { |name, _value| name.start_with?(":") }
741
+ else
742
+ invalid_pseudo_headers?(decoded_headers)
743
+ end
744
+
745
+ if invalid
746
+ streams.delete(stream_id)
747
+ outgoing_frames << parser.build_frame(:rst_stream, 0, stream_id, [ERROR_PROTOCOL_ERROR].pack("N"))
748
+ elsif trailers
749
+ stream = streams.delete(stream_id)
750
+ completed_requests << {
751
+ stream_id: stream_id,
752
+ headers: stream[:headers],
753
+ body: stream[:body] || ""
754
+ }
755
+ else
756
+ streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, end_stream)
757
+ end
758
+
759
+ [streams, completed_requests]
496
760
  end
761
+ private_class_method :complete_header_block
497
762
 
498
763
  # Merges a decoded header block into the stream's accumulated state,
499
764
  # promoting the stream to `completed_requests` when END_STREAM is set.
@@ -534,7 +799,9 @@ module Raptor
534
799
  # @param streams [Hash] updated stream states
535
800
  # @param outgoing_frames [Array<String>] frames to write to the socket
536
801
  # @param completed_requests [Array<Hash>] fully received stream requests
802
+ # @param cancelled_streams [Array<Integer>] streams reset by the peer
537
803
  # @param window_updates [Array<Array(Integer, Integer)>] inbound WINDOW_UPDATE pairs as [stream_id, increment]
804
+ # @param ping_acknowledgements [Array<String>] acknowledged PING payloads
538
805
  # @param peer_initial_window_size [Integer, nil] new SETTINGS_INITIAL_WINDOW_SIZE announced by the peer
539
806
  # @param connection_window [Integer] current connection flow control window
540
807
  # @param preface_received [Boolean] whether the connection preface has been received
@@ -543,8 +810,8 @@ module Raptor
543
810
  # @param close_connection [Boolean] whether the connection should be closed after writing outgoing frames
544
811
  # @return [Hash] frozen result hash
545
812
  #
546
- # @rbs (Hash[Symbol, untyped] data, String buffer, Array[untyped] hpack_table, Hash[Integer, Hash[Symbol, untyped]] streams, Array[String] outgoing_frames, Array[Hash[Symbol, untyped]] completed_requests, Array[[Integer, Integer]] window_updates, Integer? peer_initial_window_size, Integer connection_window, bool preface_received, Integer last_client_stream_id, Hash[Symbol, untyped]? pending_headers, bool close_connection) -> Hash[Symbol, untyped]
547
- def self.build_result(data, buffer, hpack_table, streams, outgoing_frames, completed_requests, window_updates, peer_initial_window_size, connection_window, preface_received, last_client_stream_id, pending_headers, close_connection)
813
+ # @rbs (Hash[Symbol, untyped] data, String buffer, Array[untyped] hpack_table, Hash[Integer, Hash[Symbol, untyped]] streams, Array[String] outgoing_frames, Array[Hash[Symbol, untyped]] completed_requests, Array[Integer] cancelled_streams, Array[[Integer, Integer]] window_updates, Array[String] ping_acknowledgements, Integer? peer_initial_window_size, Integer connection_window, bool preface_received, Integer last_client_stream_id, Hash[Symbol, untyped]? pending_headers, bool close_connection) -> Hash[Symbol, untyped]
814
+ def self.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, close_connection)
548
815
  result = {
549
816
  id: data[:id],
550
817
  protocol: :http2,
@@ -561,7 +828,9 @@ module Raptor
561
828
  remote_addr: data[:remote_addr],
562
829
  url_scheme: data[:url_scheme]
563
830
  }
831
+ result[:cancelled_streams] = cancelled_streams unless cancelled_streams.empty?
564
832
  result[:window_updates] = window_updates unless window_updates.empty?
833
+ result[:ping_acknowledgements] = ping_acknowledgements unless ping_acknowledgements.empty?
565
834
  result[:peer_initial_window_size] = peer_initial_window_size if peer_initial_window_size
566
835
  Ractor.make_shareable(result)
567
836
  end
@@ -582,8 +851,15 @@ module Raptor
582
851
  #
583
852
  # @rbs (OpenSSL::SSL::SSLSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
584
853
  def eager_accept(socket, id, reactor, thread_pool, remote_addr, url_scheme)
854
+ unless @running.true?
855
+ socket.close rescue nil
856
+ return
857
+ end
858
+
585
859
  writer = create_writer
586
860
  flow_control = FlowControl.new
861
+ ping_payload = [id].pack("Q>")
862
+ ping_frame = Http2Parser.new.build_frame(:ping, 0, 0, ping_payload).freeze
587
863
  initial_state = {
588
864
  id: id,
589
865
  protocol: :http2,
@@ -591,7 +867,19 @@ module Raptor
591
867
  url_scheme: url_scheme
592
868
  }
593
869
 
594
- reactor.attach_http2(id: id, socket: socket, state: initial_state, writer: writer, flow_control: flow_control)
870
+ reactor.attach_http2(
871
+ id: id,
872
+ socket: socket,
873
+ state: initial_state,
874
+ writer: writer,
875
+ flow_control: flow_control,
876
+ ping_frame: ping_frame,
877
+ ping_payload: ping_payload
878
+ )
879
+ unless @running.true?
880
+ reactor.close_connection(id)
881
+ return
882
+ end
595
883
 
596
884
  socket.write(@initial_settings_frame) rescue nil
597
885
 
@@ -605,7 +893,7 @@ module Raptor
605
893
  return
606
894
  end
607
895
 
608
- while socket.pending.positive?
896
+ while socket.respond_to?(:pending) && socket.pending.positive?
609
897
  buffer << socket.read_nonblock(socket.pending)
610
898
  end
611
899
 
@@ -636,8 +924,15 @@ module Raptor
636
924
 
637
925
  rounds = 0
638
926
  loop do
639
- if flow_control && (result[:window_updates] || result[:peer_initial_window_size])
927
+ reactor.acknowledge_http2_ping(result[:id], result[:ping_acknowledgements])
928
+ result[:cancelled_streams]&.each do |stream_id|
929
+ flow_control.cancel_stream(stream_id)
930
+ reactor.cancel_http2_body(result[:id], stream_id)
931
+ end
932
+
933
+ if flow_control && (result[:window_updates]&.any? || result[:peer_initial_window_size])
640
934
  apply_flow_control_updates(flow_control, result)
935
+ reactor.resume_http2_bodies(result[:id])
641
936
  end
642
937
 
643
938
  writer.write_frames(socket, result[:outgoing_frames])
@@ -651,6 +946,11 @@ module Raptor
651
946
  stream_id = request[:stream_id]
652
947
  remote_addr = result[:remote_addr] || Server::DEFAULT_REMOTE_ADDR
653
948
 
949
+ unless reactor.dispatch_http2_stream(result[:id], stream_id)
950
+ write_http2_reset_stream(socket, writer, stream_id, ERROR_REFUSED_STREAM)
951
+ next
952
+ end
953
+
654
954
  thread_pool << proc do
655
955
  dispatch_stream_request(
656
956
  socket, writer, flow_control, stream_id,
@@ -716,7 +1016,7 @@ module Raptor
716
1016
  buffer = String.new
717
1017
  buffer << data
718
1018
 
719
- while socket.pending.positive?
1019
+ while socket.respond_to?(:pending) && socket.pending.positive?
720
1020
  buffer << socket.read_nonblock(socket.pending)
721
1021
  end
722
1022
 
@@ -750,13 +1050,57 @@ module Raptor
750
1050
  #
751
1051
  # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
752
1052
  def perform_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:)
753
- env = build_rack_env(headers, body, remote_addr: remote_addr)
1053
+ env = nil
1054
+ status = nil
1055
+ response_headers = nil
1056
+ response_started = false
1057
+ detached = false
1058
+
1059
+ flow_control.check(stream_id)
1060
+ env = build_rack_env(headers, body, socket, writer, flow_control, stream_id, remote_addr: remote_addr)
754
1061
  status, response_headers, response_body = @app.call(env)
755
1062
 
756
- response_size = write_http2_response(socket, writer, flow_control, stream_id, status, response_headers, response_body)
1063
+ no_body = env[Rack::REQUEST_METHOD] == "HEAD" || (status >= 100 && status < 200) || status == 204 || status == 304
1064
+ if response_body.is_a?(DetachedBody) && !no_body
1065
+ finished = proc do |reason|
1066
+ error = StreamClosedError.new unless reason == :closed
1067
+ write_access_log(env, status, "-", remote_addr) if @access_log_io
1068
+ Http.call_response_finished(env, status, response_headers, error)
1069
+ flow_control.discard_stream(stream_id)
1070
+ end
1071
+ detached = write_http2_detached_response(socket, writer, flow_control, stream_id, status, response_headers, response_body, finished) do
1072
+ response_started = true
1073
+ end
1074
+ return
1075
+ end
1076
+
1077
+ response_size = write_http2_response(
1078
+ socket,
1079
+ writer,
1080
+ flow_control,
1081
+ stream_id,
1082
+ status,
1083
+ response_headers,
1084
+ response_body,
1085
+ trailers: env[RESPONSE_TRAILERS],
1086
+ request_method: env[Rack::REQUEST_METHOD]
1087
+ ) { response_started = true }
1088
+ if response_body.is_a?(DetachedBody)
1089
+ response_body.finish(:closed)
1090
+ elsif response_body.respond_to?(:close)
1091
+ response_body.close
1092
+ end
757
1093
  write_access_log(env, status, response_size, remote_addr) if @access_log_io
1094
+ Http.call_response_finished(env, status, response_headers, nil)
1095
+ rescue StreamClosedError => error
1096
+ Http.call_response_finished(env, status, response_headers, error)
758
1097
  rescue => error
759
- write_http2_error_response(socket, writer, stream_id)
1098
+ Http.call_response_finished(env, status, response_headers, error)
1099
+ if response_started
1100
+ write_http2_reset_stream(socket, writer, stream_id, ERROR_INTERNAL_ERROR)
1101
+ else
1102
+ write_http2_error_response(socket, writer, stream_id)
1103
+ end
760
1104
 
761
1105
  if @on_error
762
1106
  @on_error.call(env, error) rescue nil
@@ -764,8 +1108,21 @@ module Raptor
764
1108
  raise
765
1109
  end
766
1110
  ensure
767
- response_body.close if response_body.respond_to?(:close)
768
- flow_control.discard_stream(stream_id) if flow_control
1111
+ flow_control.discard_stream(stream_id) if flow_control && !detached
1112
+ end
1113
+
1114
+ # Starts a detached HTTP/2 response after writing its response headers.
1115
+ #
1116
+ # @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 } -> bool
1117
+ def write_http2_detached_response(socket, writer, flow_control, stream_id, status, headers, body, finished)
1118
+ flow_control.check(stream_id)
1119
+ parser = Http2Parser.new
1120
+ encoded = parser.encode_response_headers(status, headers)
1121
+ writer.write_frames(socket, [parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded)])
1122
+ yield
1123
+ attached = writer.attach_body(stream_id, body, finished)
1124
+ write_http2_reset_stream(socket, writer, stream_id, ERROR_REFUSED_STREAM) unless attached
1125
+ true
769
1126
  end
770
1127
 
771
1128
  # Writes a Rack response as HTTP/2 frames to the socket, partitioning
@@ -778,44 +1135,69 @@ module Raptor
778
1135
  # @param status [Integer] HTTP status code
779
1136
  # @param headers [Hash] response headers from the Rack application
780
1137
  # @param body [Object] response body responding to each
1138
+ # @param trailers [Hash] trailing response headers populated by the Rack application
1139
+ # @param request_method [String] request method used to suppress HEAD response bodies
781
1140
  # @return [String] the response body size in bytes
782
1141
  #
783
- # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body) -> String
784
- def write_http2_response(socket, writer, flow_control, stream_id, status, headers, body)
1142
+ # @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
1143
+ def write_http2_response(socket, writer, flow_control, stream_id, status, headers, body, trailers:, request_method:)
785
1144
  parser = Http2Parser.new
786
1145
 
787
1146
  encoded_headers = parser.encode_response_headers(status, headers)
788
- body_chunks = []
789
- body_bytes = 0
790
- body.each do |chunk|
791
- next if chunk.empty?
792
- body_chunks << chunk
793
- body_bytes += chunk.bytesize
794
- end
795
-
796
- if body_chunks.empty?
1147
+ flow_control.check(stream_id)
1148
+ no_body = request_method == "HEAD" || (status >= 100 && status < 200) || status == 204 || status == 304
1149
+ if no_body
797
1150
  writer.write_frames(socket, [parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, stream_id, encoded_headers)])
1151
+ yield if block_given?
798
1152
  return "0"
799
1153
  end
800
1154
 
801
- frames = [parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded_headers)]
1155
+ writer.write_frames(socket, [parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded_headers)])
1156
+ yield if block_given?
802
1157
 
803
- last_chunk_index = body_chunks.size - 1
804
- body_chunks.each_with_index do |chunk, chunk_index|
805
- offset = 0
806
- while offset < chunk.bytesize
807
- remaining = chunk.bytesize - offset
808
- last_frame = chunk_index == last_chunk_index && remaining <= MAX_FRAME_SIZE
809
- granted = flow_control.acquire(stream_id, remaining, end_stream: last_frame)
810
- slice = offset.zero? && granted == chunk.bytesize ? chunk : chunk.byteslice(offset, granted)
811
- offset += granted
812
- end_stream = chunk_index == last_chunk_index && offset == chunk.bytesize
813
- frames << parser.build_frame(:data, end_stream ? FLAG_END_STREAM : 0, stream_id, slice)
814
- end
1158
+ stream = ResponseStream.new(socket, writer, flow_control, stream_id, trailers)
1159
+ if body.respond_to?(:each)
1160
+ body.each { |chunk| stream.write(chunk) }
1161
+ else
1162
+ body.call(stream)
815
1163
  end
1164
+ stream.close
816
1165
 
817
- writer.write_frames(socket, frames)
818
- body_bytes.to_s
1166
+ stream.bytes_written.to_s
1167
+ end
1168
+
1169
+ # Sends an HTTP 103 Early Hints response.
1170
+ #
1171
+ # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1172
+ # @param writer [Writer] frame writer for the connection
1173
+ # @param flow_control [FlowControl] per-connection outbound flow controller
1174
+ # @param stream_id [Integer] the HTTP/2 stream identifier
1175
+ # @param hints [Hash] response headers to send as early hints
1176
+ # @return [void]
1177
+ #
1178
+ # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Hash[String, String | Array[String]] hints) -> void
1179
+ def send_early_hints(socket, writer, flow_control, stream_id, hints)
1180
+ return if hints.empty?
1181
+
1182
+ flow_control.check(stream_id)
1183
+ parser = Http2Parser.new
1184
+ encoded = parser.encode_response_headers(103, hints)
1185
+ writer.write_frames(socket, [parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded)])
1186
+ end
1187
+
1188
+ # Resets one HTTP/2 stream with the given error code.
1189
+ #
1190
+ # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1191
+ # @param writer [Writer] frame writer for the connection
1192
+ # @param stream_id [Integer] the HTTP/2 stream identifier
1193
+ # @param error_code [Integer] the HTTP/2 error code
1194
+ # @return [void]
1195
+ #
1196
+ # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Integer error_code) -> void
1197
+ def write_http2_reset_stream(socket, writer, stream_id, error_code)
1198
+ parser = Http2Parser.new
1199
+ frame = parser.build_frame(:rst_stream, 0, stream_id, [error_code].pack("N"))
1200
+ writer.write_frames(socket, [frame])
819
1201
  end
820
1202
 
821
1203
  # Writes a 500 error response as HTTP/2 frames.
@@ -854,11 +1236,14 @@ module Raptor
854
1236
  #
855
1237
  # @param headers [Array<Array(String, String)>] HTTP/2 header pairs
856
1238
  # @param body [String] the request body
1239
+ # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1240
+ # @param writer [Writer] frame writer for the connection
1241
+ # @param stream_id [Integer] the HTTP/2 stream identifier
857
1242
  # @param remote_addr [String] the client IP address
858
1243
  # @return [Hash] fully populated Rack environment hash
859
1244
  #
860
- # @rbs (Array[[String, String]] headers, String body, remote_addr: String) -> Hash[String, untyped]
861
- def build_rack_env(headers, body, remote_addr:)
1245
+ # @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]
1246
+ def build_rack_env(headers, body, socket, writer, flow_control, stream_id, remote_addr:)
862
1247
  env = {}
863
1248
 
864
1249
  headers.each do |name, value|
@@ -887,6 +1272,9 @@ module Raptor
887
1272
  env[Rack::RACK_INPUT] = StringIO.new(body).set_encoding(Encoding::ASCII_8BIT)
888
1273
  env[Rack::RACK_ERRORS] = $stderr
889
1274
  env[Rack::RACK_RESPONSE_FINISHED] = []
1275
+ env[Rack::RACK_EARLY_HINTS] = proc do |hints|
1276
+ send_early_hints(socket, writer, flow_control, stream_id, hints) rescue nil
1277
+ end
890
1278
  env[Rack::RACK_IS_HIJACK] = false
891
1279
 
892
1280
  env[Rack::SCRIPT_NAME] = "" unless env.key?(Rack::SCRIPT_NAME)
@@ -900,6 +1288,7 @@ module Raptor
900
1288
  env[Http::REMOTE_ADDR] = remote_addr
901
1289
  env[Http::SERVER_SOFTWARE] = Http::SERVER_SOFTWARE_VALUE
902
1290
  env[Http::HTTP_VERSION] = SERVER_PROTOCOL
1291
+ env[RESPONSE_TRAILERS] = {}
903
1292
 
904
1293
  populate_server_name_and_port(env)
905
1294