raptor 0.20.2 → 0.21.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,6 +4,8 @@
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
 
9
11
  require_relative "http"
@@ -14,6 +16,8 @@ module Raptor
14
16
  # Handles HTTP/2 request processing and Rack application integration.
15
17
  #
16
18
  class Http2
19
+ RESPONSE_TRAILERS = "raptor.response_trailers"
20
+
17
21
  # Serialises concurrent frame writes on a single HTTP/2 connection so
18
22
  # exactly one thread is writing at any moment.
19
23
  #
@@ -72,15 +76,20 @@ module Raptor
72
76
  end
73
77
  end
74
78
 
75
- # Tracks the peer's connection-level and per-stream receive windows so
76
- # outbound `DATA` frames respect RFC 7540 section 5.2.
79
+ class StreamClosedError < StandardError
80
+ # @rbs () -> String
81
+ def message = "HTTP/2 stream closed"
82
+ end
83
+
84
+ # Coordinates outbound flow control and stream cancellation without
85
+ # polling or mutexes.
77
86
  #
78
87
  class FlowControl
79
- ACQUIRE_POLL_INTERVAL = 0.001
80
-
81
88
  # @rbs @connection_window: Atom
82
89
  # @rbs @stream_windows: Atom
83
90
  # @rbs @initial_stream_window: Atom
91
+ # @rbs @cancelled_streams: Atom
92
+ # @rbs @available: AtomicConditionVariable
84
93
 
85
94
  # Creates a new FlowControl with the spec-default windows.
86
95
  #
@@ -91,32 +100,25 @@ module Raptor
91
100
  @connection_window = Atom.new(DEFAULT_WINDOW_SIZE)
92
101
  @stream_windows = Atom.new({})
93
102
  @initial_stream_window = Atom.new(DEFAULT_WINDOW_SIZE)
103
+ @cancelled_streams = Atom.new({})
104
+ @available = AtomicConditionVariable.new
94
105
  end
95
106
 
96
- # Reserves outbound capacity on the given stream, polling until at
107
+ # Reserves outbound capacity on the given stream, waiting until at
97
108
  # least one byte is available on both the connection and stream
98
- # windows. The returned size is capped at `MAX_FRAME_SIZE`.
109
+ # windows. Raises when the stream or connection closes.
99
110
  #
100
111
  # @param stream_id [Integer] the HTTP/2 stream identifier
101
112
  # @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
113
  # @return [Integer] the number of bytes the caller may now send
104
114
  #
105
- # @rbs (Integer stream_id, Integer max_bytes, ?end_stream: bool) -> Integer
106
- def acquire(stream_id, max_bytes, end_stream: false)
115
+ # @rbs (Integer stream_id, Integer max_bytes) -> Integer
116
+ def acquire(stream_id, max_bytes)
107
117
  initial = @initial_stream_window.value
108
118
  capped = max_bytes < MAX_FRAME_SIZE ? max_bytes : MAX_FRAME_SIZE
109
119
 
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
120
+ @available.wait do
121
+ check(stream_id)
120
122
  stream_window = @stream_windows.value[stream_id] || initial
121
123
  capped_full = capped < stream_window ? capped : stream_window
122
124
  granted = capped_full.positive? ? reserve_connection(capped_full) : 0
@@ -126,10 +128,8 @@ module Raptor
126
128
  current = windows[stream_id] || initial
127
129
  windows.merge(stream_id => current - granted)
128
130
  end
129
- return granted
131
+ granted
130
132
  end
131
-
132
- sleep ACQUIRE_POLL_INTERVAL
133
133
  end
134
134
  end
135
135
 
@@ -141,6 +141,7 @@ module Raptor
141
141
  # @rbs (Integer increment) -> void
142
142
  def add_connection_window(increment)
143
143
  @connection_window.swap { |window| window + increment }
144
+ @available.broadcast
144
145
  end
145
146
 
146
147
  # Increments the send window for the given stream by `increment` bytes.
@@ -156,6 +157,7 @@ module Raptor
156
157
  current = windows[stream_id] || initial
157
158
  windows.merge(stream_id => current + increment)
158
159
  end
160
+ @available.broadcast
159
161
  end
160
162
 
161
163
  # Updates the peer's `SETTINGS_INITIAL_WINDOW_SIZE`. Shifts every
@@ -174,6 +176,40 @@ module Raptor
174
176
  @stream_windows.swap do |windows|
175
177
  windows.transform_values { |size| size + delta }
176
178
  end
179
+ @available.broadcast
180
+ end
181
+
182
+ # Raises when the given stream can no longer write a response.
183
+ #
184
+ # @param stream_id [Integer] the HTTP/2 stream identifier
185
+ # @return [void]
186
+ # @raise [StreamClosedError] if the stream or connection has closed
187
+ #
188
+ # @rbs (Integer stream_id) -> void
189
+ def check(stream_id)
190
+ cancelled = @cancelled_streams.value
191
+ raise StreamClosedError if cancelled.key?(0) || cancelled.key?(stream_id)
192
+ end
193
+
194
+ # Cancels pending and future writes for the given stream.
195
+ #
196
+ # @param stream_id [Integer] the HTTP/2 stream identifier
197
+ # @return [void]
198
+ #
199
+ # @rbs (Integer stream_id) -> void
200
+ def cancel_stream(stream_id)
201
+ @cancelled_streams.swap { |streams| streams.merge(stream_id => true) }
202
+ @available.broadcast
203
+ end
204
+
205
+ # Cancels pending and future writes for the connection.
206
+ #
207
+ # @return [void]
208
+ #
209
+ # @rbs () -> void
210
+ def close
211
+ @cancelled_streams.swap { |streams| streams.merge(0 => true) }
212
+ @available.broadcast
177
213
  end
178
214
 
179
215
  # Discards any per-stream tracking for the given stream.
@@ -183,8 +219,6 @@ module Raptor
183
219
  #
184
220
  # @rbs (Integer stream_id) -> void
185
221
  def discard_stream(stream_id)
186
- return unless @stream_windows.value.key?(stream_id)
187
-
188
222
  @stream_windows.swap do |windows|
189
223
  next windows unless windows.key?(stream_id)
190
224
 
@@ -192,6 +226,13 @@ module Raptor
192
226
  pruned.delete(stream_id)
193
227
  pruned
194
228
  end
229
+ @cancelled_streams.swap do |streams|
230
+ next streams unless streams.key?(stream_id)
231
+
232
+ pruned = streams.dup
233
+ pruned.delete(stream_id)
234
+ pruned
235
+ end
195
236
  end
196
237
 
197
238
  private
@@ -214,6 +255,142 @@ module Raptor
214
255
  end
215
256
  end
216
257
 
258
+ # Adapts one HTTP/2 response stream to Rack's streaming body interface.
259
+ #
260
+ class ResponseStream
261
+ # @rbs @socket: OpenSSL::SSL::SSLSocket
262
+ # @rbs @writer: Writer
263
+ # @rbs @flow_control: FlowControl
264
+ # @rbs @stream_id: Integer
265
+ # @rbs @trailers: Hash[String, String | Array[String]]
266
+ # @rbs @parser: Http2Parser
267
+ # @rbs @bytes_written: Integer
268
+ # @rbs @closed: bool
269
+
270
+ # Returns the number of response body bytes written to the stream.
271
+ #
272
+ # @return [Integer]
273
+ attr_reader :bytes_written #: Integer
274
+
275
+ # Creates a response stream for a Rack streaming body.
276
+ #
277
+ # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
278
+ # @param writer [Writer] frame writer for the connection
279
+ # @param flow_control [FlowControl] outbound flow controller
280
+ # @param stream_id [Integer] the HTTP/2 stream identifier
281
+ # @param trailers [Hash] trailing response headers populated by the Rack application
282
+ # @return [void]
283
+ #
284
+ # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Hash[String, String | Array[String]] trailers) -> void
285
+ def initialize(socket, writer, flow_control, stream_id, trailers)
286
+ @socket = socket
287
+ @writer = writer
288
+ @flow_control = flow_control
289
+ @stream_id = stream_id
290
+ @trailers = trailers
291
+ @parser = Http2Parser.new
292
+ @bytes_written = 0
293
+ @closed = false
294
+ end
295
+
296
+ # Streaming responses are write-only.
297
+ #
298
+ # @raise [IOError]
299
+ #
300
+ # @rbs (*untyped) -> bot
301
+ def read(*)
302
+ raise IOError, "not opened for reading"
303
+ end
304
+
305
+ # Writes response body bytes to the stream.
306
+ #
307
+ # @param chunk [String] response body bytes
308
+ # @return [Integer] number of bytes written
309
+ #
310
+ # @rbs (String chunk) -> Integer
311
+ def write(chunk)
312
+ raise IOError, "closed stream" if @closed
313
+ raise TypeError, "body must yield String values" unless chunk.is_a?(String)
314
+
315
+ offset = 0
316
+ while offset < chunk.bytesize
317
+ granted = @flow_control.acquire(@stream_id, chunk.bytesize - offset)
318
+ slice = offset.zero? && granted == chunk.bytesize ? chunk : chunk.byteslice(offset, granted)
319
+ offset += granted
320
+ @writer.write_frames(@socket, [@parser.build_frame(:data, 0, @stream_id, slice)])
321
+ end
322
+
323
+ @bytes_written += chunk.bytesize
324
+ chunk.bytesize
325
+ end
326
+
327
+ # Writes response body bytes and returns the stream.
328
+ #
329
+ # @param chunk [String] response body bytes
330
+ # @return [ResponseStream]
331
+ #
332
+ # @rbs (String chunk) -> ResponseStream
333
+ def <<(chunk)
334
+ write(chunk)
335
+ self
336
+ end
337
+
338
+ # Flushes the stream. Frame writes are already immediate.
339
+ #
340
+ # @return [ResponseStream]
341
+ #
342
+ # @rbs () -> ResponseStream
343
+ def flush
344
+ self
345
+ end
346
+
347
+ # Finishes the stream.
348
+ #
349
+ # @return [void]
350
+ #
351
+ # @rbs () -> void
352
+ def close
353
+ return if @closed
354
+
355
+ @flow_control.check(@stream_id)
356
+ @closed = true
357
+ if @trailers.empty?
358
+ frame = @parser.build_frame(:data, FLAG_END_STREAM, @stream_id, nil)
359
+ else
360
+ encoded = @parser.encode_response_trailers(@trailers)
361
+ frame = @parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, @stream_id, encoded)
362
+ end
363
+ @writer.write_frames(@socket, [frame])
364
+ end
365
+
366
+ # Closes the unsupported read side of the stream.
367
+ #
368
+ # @raise [IOError]
369
+ #
370
+ # @rbs () -> bot
371
+ def close_read
372
+ raise IOError, "closing non-duplex IO for reading"
373
+ end
374
+
375
+ # Finishes the writable side of the stream.
376
+ #
377
+ # @return [void]
378
+ #
379
+ # @rbs () -> void
380
+ def close_write
381
+ close
382
+ end
383
+
384
+ # Returns whether the stream has finished.
385
+ #
386
+ # @return [Boolean]
387
+ #
388
+ # @rbs () -> bool
389
+ def closed?
390
+ @closed
391
+ end
392
+ end
393
+
217
394
  EAGER_READ_TIMEOUT = 0.001
218
395
  EAGER_READ_BUFFER_SIZE = 64 * 1024
219
396
  EAGER_MAX_ROUNDS = 8
@@ -225,6 +402,8 @@ module Raptor
225
402
 
226
403
  ERROR_NO_ERROR = 0x0
227
404
  ERROR_PROTOCOL_ERROR = 0x1
405
+ ERROR_INTERNAL_ERROR = 0x2
406
+ ERROR_REFUSED_STREAM = 0x7
228
407
 
229
408
  DEFAULT_WINDOW_SIZE = 65_535
230
409
  MAX_FRAME_SIZE = 16_384
@@ -272,6 +451,7 @@ module Raptor
272
451
  # @rbs @clean_fiber_locals: bool
273
452
  # @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
274
453
  # @rbs @initial_settings_frame: String
454
+ # @rbs @running: AtomicBoolean
275
455
 
276
456
  # Returns the initial server SETTINGS frame to send on every new
277
457
  # HTTP/2 connection.
@@ -302,6 +482,7 @@ module Raptor
302
482
  @clean_thread_locals = clean_thread_locals
303
483
  @clean_fiber_locals = clean_fiber_locals
304
484
  @on_error = on_error
485
+ @running = AtomicBoolean.new(true)
305
486
 
306
487
  parser = Http2Parser.new
307
488
  settings_payload = parser.build_settings(
@@ -320,6 +501,22 @@ module Raptor
320
501
  Writer.new(write_timeout: @write_timeout)
321
502
  end
322
503
 
504
+ # Stops new stream dispatch and asks clients to finish streams already
505
+ # handed to the application.
506
+ #
507
+ # @param reactor [Reactor] the reactor managing HTTP/2 connections
508
+ # @return [void]
509
+ #
510
+ # @rbs (Reactor reactor) -> void
511
+ def shutdown(reactor)
512
+ @running.make_false
513
+ parser = Http2Parser.new
514
+ reactor.drain_http2 do |stream_id|
515
+ payload = [stream_id, ERROR_NO_ERROR].pack("NN")
516
+ parser.build_frame(:goaway, 0, 0, payload)
517
+ end
518
+ end
519
+
323
520
  # Returns a Ractor-safe proc that parses HTTP/2 frames from the
324
521
  # state hash's buffered bytes.
325
522
  #
@@ -347,7 +544,9 @@ module Raptor
347
544
  streams = data[:http2_streams] ? data[:http2_streams].dup : {}
348
545
  outgoing_frames = []
349
546
  completed_requests = []
547
+ cancelled_streams = []
350
548
  window_updates = []
549
+ ping_acknowledgements = []
351
550
  peer_initial_window_size = nil
352
551
  connection_window = data[:http2_window] || DEFAULT_WINDOW_SIZE
353
552
  preface_received = data[:http2_preface_received] || false
@@ -360,7 +559,7 @@ module Raptor
360
559
  buffer = buffer.byteslice(24..-1) || ""
361
560
  preface_received = true
362
561
  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)
562
+ 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
563
  end
365
564
  end
366
565
 
@@ -387,8 +586,9 @@ module Raptor
387
586
  when :headers
388
587
  stream_id = frame[:stream_id]
389
588
  header_payload = frame[:payload]
589
+ trailers = streams.key?(stream_id)
390
590
 
391
- unless streams.key?(stream_id)
591
+ unless trailers
392
592
  if stream_id.even? || stream_id <= last_client_stream_id
393
593
  goaway_error = ERROR_PROTOCOL_ERROR
394
594
  break
@@ -404,14 +604,12 @@ module Raptor
404
604
 
405
605
  if frame[:flags].anybits?(FLAG_END_HEADERS)
406
606
  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
607
+ streams, completed_requests = complete_header_block(
608
+ streams, completed_requests, outgoing_frames, parser,
609
+ stream_id, decoded_headers, end_stream, trailers
610
+ )
413
611
  else
414
- pending_headers = { stream_id: stream_id, buffer: header_payload, end_stream: end_stream }
612
+ pending_headers = { stream_id: stream_id, buffer: header_payload, end_stream: end_stream, trailers: trailers }
415
613
  end
416
614
 
417
615
  when :continuation
@@ -425,12 +623,10 @@ module Raptor
425
623
  if frame[:flags].anybits?(FLAG_END_HEADERS)
426
624
  stream_id = pending_headers[:stream_id]
427
625
  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
626
+ streams, completed_requests = complete_header_block(
627
+ streams, completed_requests, outgoing_frames, parser,
628
+ stream_id, decoded_headers, pending_headers[:end_stream], pending_headers[:trailers]
629
+ )
434
630
  pending_headers = nil
435
631
  end
436
632
 
@@ -475,7 +671,9 @@ module Raptor
475
671
  window_updates << [frame[:stream_id], increment]
476
672
 
477
673
  when :ping
478
- if frame[:flags].nobits?(FLAG_ACK)
674
+ if frame[:flags].anybits?(FLAG_ACK)
675
+ ping_acknowledgements << frame[:payload]
676
+ else
479
677
  outgoing_frames << parser.build_frame(:ping, FLAG_ACK, 0, frame[:payload])
480
678
  end
481
679
 
@@ -483,7 +681,10 @@ module Raptor
483
681
  break
484
682
 
485
683
  when :rst_stream
486
- streams.delete(frame[:stream_id])
684
+ stream_id = frame[:stream_id]
685
+ streams.delete(stream_id)
686
+ completed_requests.reject! { |request| request[:stream_id] == stream_id }
687
+ cancelled_streams << stream_id
487
688
  end
488
689
  end
489
690
 
@@ -492,8 +693,47 @@ module Raptor
492
693
  outgoing_frames << parser.build_frame(:goaway, 0, 0, goaway_payload)
493
694
  end
494
695
 
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)
696
+ 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)
697
+ end
698
+
699
+ # Applies one complete request header block to its stream, resetting
700
+ # malformed initial headers or trailers and completing valid trailers.
701
+ #
702
+ # @param streams [Hash] current open-stream map
703
+ # @param completed_requests [Array<Hash>] accumulator of completed stream requests
704
+ # @param outgoing_frames [Array<String>] accumulator of protocol response frames
705
+ # @param parser [Http2Parser] frame parser and encoder
706
+ # @param stream_id [Integer] the stream identifier
707
+ # @param decoded_headers [Array<Array(String, String)>] decoded header pairs
708
+ # @param end_stream [Boolean] whether the source frame had END_STREAM set
709
+ # @param trailers [Boolean] whether this is a trailing header block
710
+ # @return [Array(Hash, Array<Hash>)] updated streams and completed requests
711
+ #
712
+ # @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]]]
713
+ def self.complete_header_block(streams, completed_requests, outgoing_frames, parser, stream_id, decoded_headers, end_stream, trailers)
714
+ invalid = if trailers
715
+ !end_stream || decoded_headers.any? { |name, _value| name.start_with?(":") }
716
+ else
717
+ invalid_pseudo_headers?(decoded_headers)
718
+ end
719
+
720
+ if invalid
721
+ streams.delete(stream_id)
722
+ outgoing_frames << parser.build_frame(:rst_stream, 0, stream_id, [ERROR_PROTOCOL_ERROR].pack("N"))
723
+ elsif trailers
724
+ stream = streams.delete(stream_id)
725
+ completed_requests << {
726
+ stream_id: stream_id,
727
+ headers: stream[:headers],
728
+ body: stream[:body] || ""
729
+ }
730
+ else
731
+ streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, end_stream)
732
+ end
733
+
734
+ [streams, completed_requests]
496
735
  end
736
+ private_class_method :complete_header_block
497
737
 
498
738
  # Merges a decoded header block into the stream's accumulated state,
499
739
  # promoting the stream to `completed_requests` when END_STREAM is set.
@@ -534,7 +774,9 @@ module Raptor
534
774
  # @param streams [Hash] updated stream states
535
775
  # @param outgoing_frames [Array<String>] frames to write to the socket
536
776
  # @param completed_requests [Array<Hash>] fully received stream requests
777
+ # @param cancelled_streams [Array<Integer>] streams reset by the peer
537
778
  # @param window_updates [Array<Array(Integer, Integer)>] inbound WINDOW_UPDATE pairs as [stream_id, increment]
779
+ # @param ping_acknowledgements [Array<String>] acknowledged PING payloads
538
780
  # @param peer_initial_window_size [Integer, nil] new SETTINGS_INITIAL_WINDOW_SIZE announced by the peer
539
781
  # @param connection_window [Integer] current connection flow control window
540
782
  # @param preface_received [Boolean] whether the connection preface has been received
@@ -543,8 +785,8 @@ module Raptor
543
785
  # @param close_connection [Boolean] whether the connection should be closed after writing outgoing frames
544
786
  # @return [Hash] frozen result hash
545
787
  #
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)
788
+ # @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]
789
+ 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
790
  result = {
549
791
  id: data[:id],
550
792
  protocol: :http2,
@@ -561,7 +803,9 @@ module Raptor
561
803
  remote_addr: data[:remote_addr],
562
804
  url_scheme: data[:url_scheme]
563
805
  }
806
+ result[:cancelled_streams] = cancelled_streams unless cancelled_streams.empty?
564
807
  result[:window_updates] = window_updates unless window_updates.empty?
808
+ result[:ping_acknowledgements] = ping_acknowledgements unless ping_acknowledgements.empty?
565
809
  result[:peer_initial_window_size] = peer_initial_window_size if peer_initial_window_size
566
810
  Ractor.make_shareable(result)
567
811
  end
@@ -582,8 +826,15 @@ module Raptor
582
826
  #
583
827
  # @rbs (OpenSSL::SSL::SSLSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
584
828
  def eager_accept(socket, id, reactor, thread_pool, remote_addr, url_scheme)
829
+ unless @running.true?
830
+ socket.close rescue nil
831
+ return
832
+ end
833
+
585
834
  writer = create_writer
586
835
  flow_control = FlowControl.new
836
+ ping_payload = [id].pack("Q>")
837
+ ping_frame = Http2Parser.new.build_frame(:ping, 0, 0, ping_payload).freeze
587
838
  initial_state = {
588
839
  id: id,
589
840
  protocol: :http2,
@@ -591,7 +842,19 @@ module Raptor
591
842
  url_scheme: url_scheme
592
843
  }
593
844
 
594
- reactor.attach_http2(id: id, socket: socket, state: initial_state, writer: writer, flow_control: flow_control)
845
+ reactor.attach_http2(
846
+ id: id,
847
+ socket: socket,
848
+ state: initial_state,
849
+ writer: writer,
850
+ flow_control: flow_control,
851
+ ping_frame: ping_frame,
852
+ ping_payload: ping_payload
853
+ )
854
+ unless @running.true?
855
+ reactor.close_connection(id)
856
+ return
857
+ end
595
858
 
596
859
  socket.write(@initial_settings_frame) rescue nil
597
860
 
@@ -605,7 +868,7 @@ module Raptor
605
868
  return
606
869
  end
607
870
 
608
- while socket.pending.positive?
871
+ while socket.respond_to?(:pending) && socket.pending.positive?
609
872
  buffer << socket.read_nonblock(socket.pending)
610
873
  end
611
874
 
@@ -636,6 +899,9 @@ module Raptor
636
899
 
637
900
  rounds = 0
638
901
  loop do
902
+ reactor.acknowledge_http2_ping(result[:id], result[:ping_acknowledgements])
903
+ result[:cancelled_streams]&.each { |stream_id| flow_control.cancel_stream(stream_id) }
904
+
639
905
  if flow_control && (result[:window_updates] || result[:peer_initial_window_size])
640
906
  apply_flow_control_updates(flow_control, result)
641
907
  end
@@ -651,6 +917,11 @@ module Raptor
651
917
  stream_id = request[:stream_id]
652
918
  remote_addr = result[:remote_addr] || Server::DEFAULT_REMOTE_ADDR
653
919
 
920
+ unless reactor.dispatch_http2_stream(result[:id], stream_id)
921
+ write_http2_reset_stream(socket, writer, stream_id, ERROR_REFUSED_STREAM)
922
+ next
923
+ end
924
+
654
925
  thread_pool << proc do
655
926
  dispatch_stream_request(
656
927
  socket, writer, flow_control, stream_id,
@@ -716,7 +987,7 @@ module Raptor
716
987
  buffer = String.new
717
988
  buffer << data
718
989
 
719
- while socket.pending.positive?
990
+ while socket.respond_to?(:pending) && socket.pending.positive?
720
991
  buffer << socket.read_nonblock(socket.pending)
721
992
  end
722
993
 
@@ -750,13 +1021,38 @@ module Raptor
750
1021
  #
751
1022
  # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
752
1023
  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)
1024
+ env = nil
1025
+ status = nil
1026
+ response_headers = nil
1027
+ response_started = false
1028
+
1029
+ flow_control.check(stream_id)
1030
+ env = build_rack_env(headers, body, socket, writer, flow_control, stream_id, remote_addr: remote_addr)
754
1031
  status, response_headers, response_body = @app.call(env)
755
1032
 
756
- response_size = write_http2_response(socket, writer, flow_control, stream_id, status, response_headers, response_body)
1033
+ response_size = write_http2_response(
1034
+ socket,
1035
+ writer,
1036
+ flow_control,
1037
+ stream_id,
1038
+ status,
1039
+ response_headers,
1040
+ response_body,
1041
+ trailers: env[RESPONSE_TRAILERS],
1042
+ request_method: env[Rack::REQUEST_METHOD]
1043
+ ) { response_started = true }
1044
+ response_body.close if response_body.respond_to?(:close)
757
1045
  write_access_log(env, status, response_size, remote_addr) if @access_log_io
1046
+ Http.call_response_finished(env, status, response_headers, nil)
1047
+ rescue StreamClosedError => error
1048
+ Http.call_response_finished(env, status, response_headers, error)
758
1049
  rescue => error
759
- write_http2_error_response(socket, writer, stream_id)
1050
+ Http.call_response_finished(env, status, response_headers, error)
1051
+ if response_started
1052
+ write_http2_reset_stream(socket, writer, stream_id, ERROR_INTERNAL_ERROR)
1053
+ else
1054
+ write_http2_error_response(socket, writer, stream_id)
1055
+ end
760
1056
 
761
1057
  if @on_error
762
1058
  @on_error.call(env, error) rescue nil
@@ -764,7 +1060,6 @@ module Raptor
764
1060
  raise
765
1061
  end
766
1062
  ensure
767
- response_body.close if response_body.respond_to?(:close)
768
1063
  flow_control.discard_stream(stream_id) if flow_control
769
1064
  end
770
1065
 
@@ -778,44 +1073,69 @@ module Raptor
778
1073
  # @param status [Integer] HTTP status code
779
1074
  # @param headers [Hash] response headers from the Rack application
780
1075
  # @param body [Object] response body responding to each
1076
+ # @param trailers [Hash] trailing response headers populated by the Rack application
1077
+ # @param request_method [String] request method used to suppress HEAD response bodies
781
1078
  # @return [String] the response body size in bytes
782
1079
  #
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)
1080
+ # @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
1081
+ def write_http2_response(socket, writer, flow_control, stream_id, status, headers, body, trailers:, request_method:)
785
1082
  parser = Http2Parser.new
786
1083
 
787
1084
  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?
1085
+ flow_control.check(stream_id)
1086
+ no_body = request_method == "HEAD" || (status >= 100 && status < 200) || status == 204 || status == 304
1087
+ if no_body
797
1088
  writer.write_frames(socket, [parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, stream_id, encoded_headers)])
1089
+ yield if block_given?
798
1090
  return "0"
799
1091
  end
800
1092
 
801
- frames = [parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded_headers)]
1093
+ writer.write_frames(socket, [parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded_headers)])
1094
+ yield if block_given?
802
1095
 
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
1096
+ stream = ResponseStream.new(socket, writer, flow_control, stream_id, trailers)
1097
+ if body.respond_to?(:each)
1098
+ body.each { |chunk| stream.write(chunk) }
1099
+ else
1100
+ body.call(stream)
815
1101
  end
1102
+ stream.close
816
1103
 
817
- writer.write_frames(socket, frames)
818
- body_bytes.to_s
1104
+ stream.bytes_written.to_s
1105
+ end
1106
+
1107
+ # Sends an HTTP 103 Early Hints response.
1108
+ #
1109
+ # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1110
+ # @param writer [Writer] frame writer for the connection
1111
+ # @param flow_control [FlowControl] per-connection outbound flow controller
1112
+ # @param stream_id [Integer] the HTTP/2 stream identifier
1113
+ # @param hints [Hash] response headers to send as early hints
1114
+ # @return [void]
1115
+ #
1116
+ # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Hash[String, String | Array[String]] hints) -> void
1117
+ def send_early_hints(socket, writer, flow_control, stream_id, hints)
1118
+ return if hints.empty?
1119
+
1120
+ flow_control.check(stream_id)
1121
+ parser = Http2Parser.new
1122
+ encoded = parser.encode_response_headers(103, hints)
1123
+ writer.write_frames(socket, [parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded)])
1124
+ end
1125
+
1126
+ # Resets one HTTP/2 stream with the given error code.
1127
+ #
1128
+ # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1129
+ # @param writer [Writer] frame writer for the connection
1130
+ # @param stream_id [Integer] the HTTP/2 stream identifier
1131
+ # @param error_code [Integer] the HTTP/2 error code
1132
+ # @return [void]
1133
+ #
1134
+ # @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Integer error_code) -> void
1135
+ def write_http2_reset_stream(socket, writer, stream_id, error_code)
1136
+ parser = Http2Parser.new
1137
+ frame = parser.build_frame(:rst_stream, 0, stream_id, [error_code].pack("N"))
1138
+ writer.write_frames(socket, [frame])
819
1139
  end
820
1140
 
821
1141
  # Writes a 500 error response as HTTP/2 frames.
@@ -854,11 +1174,14 @@ module Raptor
854
1174
  #
855
1175
  # @param headers [Array<Array(String, String)>] HTTP/2 header pairs
856
1176
  # @param body [String] the request body
1177
+ # @param socket [OpenSSL::SSL::SSLSocket] the connection socket
1178
+ # @param writer [Writer] frame writer for the connection
1179
+ # @param stream_id [Integer] the HTTP/2 stream identifier
857
1180
  # @param remote_addr [String] the client IP address
858
1181
  # @return [Hash] fully populated Rack environment hash
859
1182
  #
860
- # @rbs (Array[[String, String]] headers, String body, remote_addr: String) -> Hash[String, untyped]
861
- def build_rack_env(headers, body, remote_addr:)
1183
+ # @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]
1184
+ def build_rack_env(headers, body, socket, writer, flow_control, stream_id, remote_addr:)
862
1185
  env = {}
863
1186
 
864
1187
  headers.each do |name, value|
@@ -887,6 +1210,9 @@ module Raptor
887
1210
  env[Rack::RACK_INPUT] = StringIO.new(body).set_encoding(Encoding::ASCII_8BIT)
888
1211
  env[Rack::RACK_ERRORS] = $stderr
889
1212
  env[Rack::RACK_RESPONSE_FINISHED] = []
1213
+ env[Rack::RACK_EARLY_HINTS] = proc do |hints|
1214
+ send_early_hints(socket, writer, flow_control, stream_id, hints) rescue nil
1215
+ end
890
1216
  env[Rack::RACK_IS_HIJACK] = false
891
1217
 
892
1218
  env[Rack::SCRIPT_NAME] = "" unless env.key?(Rack::SCRIPT_NAME)
@@ -900,6 +1226,7 @@ module Raptor
900
1226
  env[Http::REMOTE_ADDR] = remote_addr
901
1227
  env[Http::SERVER_SOFTWARE] = Http::SERVER_SOFTWARE_VALUE
902
1228
  env[Http::HTTP_VERSION] = SERVER_PROTOCOL
1229
+ env[RESPONSE_TRAILERS] = {}
903
1230
 
904
1231
  populate_server_name_and_port(env)
905
1232