http-2 1.2.0 → 1.2.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37f0171472c53a3f13cb8f8259302aed5f07c9dbc90f27893ddf8b868c78e41a
4
- data.tar.gz: 52b05bc6ebc9d08d2a761ba465f39315e32ad3738a4c22d1d9a3ab58e7357867
3
+ metadata.gz: 1b41c25d0512d4056a38265801e7eef05df3cab5720ecbe11ff088c906752019
4
+ data.tar.gz: 0b7322ff7f49e8dd892a9804a1a710be23f3c7105f76f7c67f62b281843616fc
5
5
  SHA512:
6
- metadata.gz: 9f9c98e6f3d031388f451d22b142764d221967bcf72252689652182ffcd9e47bb60b90003491b7a7056eea51a7faaaece880c942ef4d02d223579924587ab103
7
- data.tar.gz: 1bbe211ac07f751a5381b2c8d633392bb5460b5324af0f717f7ad06f4ff8695c7a3a8122bcca8ceb01b7b5063cd3f95c4e8fdff14a1bbcc3ce640e5609d90da9
6
+ metadata.gz: 32b4191d53effc33d5186da7093af95698809f2fec89fe37ce01898628d598e7a1215704875ec06ebd9fcaf84b11517310ee46631fb7a2c6b4a2a17fb3ad8b94
7
+ data.tar.gz: 5ce7ec0abedca471a25e45883ba4b8d315eb1e78879ed5a8c563dc6a58a7e128bbe9f78f878a8d9956dd90254718634bc67323b57039cb33e87d6e1552cac392
data/README.md CHANGED
@@ -85,6 +85,15 @@ Events emitted by the connection object:
85
85
  </tr>
86
86
  </table>
87
87
 
88
+ Connection shutdown happens in two steps. Once a GOAWAY frame is sent or
89
+ received, no new streams may be opened: `closed?` turns true and `new_stream`
90
+ raises `ConnectionClosed` — new work belongs on a new connection. After a
91
+ graceful GOAWAY (`:no_error`), streams the peer may still complete keep
92
+ running: `closing?` distinguishes this draining state, in which the connection
93
+ keeps processing frames — including connection-level flow control — until the
94
+ last tracked stream closes. Streams the GOAWAY refused (id above its
95
+ last-stream-id) are reported via the `:goaway` event, so their callers can
96
+ retry them on a fresh connection.
88
97
 
89
98
  ### Stream lifecycle management
90
99
 
@@ -56,7 +56,8 @@ module HTTP2
56
56
  include Error
57
57
  include BufferUtils
58
58
 
59
- # Connection state (:new, :closed).
59
+ # Connection state (:waiting_magic, :waiting_connection_preface,
60
+ # :connected, :closing, :closed).
60
61
  attr_reader :state
61
62
 
62
63
  # Size of current connection flow control window (by default, set to
@@ -113,8 +114,15 @@ module HTTP2
113
114
  @send_buffer = FrameBuffer.new
114
115
  end
115
116
 
117
+ # No new streams may be opened (a GOAWAY was sent or received).
116
118
  def closed?
117
- @state == :closed
119
+ @state == :closed || @state == :closing
120
+ end
121
+
122
+ # Graceful GOAWAY received: tracked streams may still complete,
123
+ # but no new ones can be opened.
124
+ def closing?
125
+ @state == :closing
118
126
  end
119
127
 
120
128
  # Allocates new stream for current connection.
@@ -123,12 +131,15 @@ module HTTP2
123
131
  # @param window [Integer]
124
132
  # @param parent [Stream]
125
133
  def new_stream(**args)
126
- raise ConnectionClosed if @state == :closed
127
- raise StreamLimitExceeded if @active_stream_count >= @remote_settings[:settings_max_concurrent_streams]
134
+ raise ConnectionClosed if closed?
128
135
 
129
136
  connection_error(:protocol_error, msg: "id is smaller than previous") if @stream_id < @last_stream_id
130
137
 
131
- stream = activate_stream(id: @stream_id, **args)
138
+ stream = activate_stream(
139
+ id: @stream_id,
140
+ max_concurrent_streams: @remote_settings[:settings_max_concurrent_streams],
141
+ **args
142
+ )
132
143
  @last_stream_id = stream.id
133
144
 
134
145
  @stream_id += 2
@@ -158,8 +169,7 @@ module HTTP2
158
169
  def goaway(error = :no_error, payload = nil)
159
170
  send(type: :goaway, stream: 0, last_stream: @last_stream_id,
160
171
  error: error, payload: payload)
161
- @state = :closed
162
- @closed_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
172
+ close!
163
173
  end
164
174
 
165
175
  # Sends a WINDOW_UPDATE frame to the peer.
@@ -295,7 +305,11 @@ module HTTP2
295
305
  # PUSH_PROMISE and CONTINUATION frames MUST be minimally
296
306
  # processed to ensure a consistent compression state
297
307
  decode_headers(frame)
298
- return if @state == :closed
308
+ # After a GOAWAY, HEADERS may only complete a tracked stream at
309
+ # or below the highest id seen (Section 6.8); anything else is
310
+ # discarded - decoded above, so the compression state is intact.
311
+ # Skip only this frame: later ones may serve completing streams.
312
+ next if closed? && (stream_id > @last_stream_id || !@streams.key?(stream_id))
299
313
 
300
314
  stream = @streams[stream_id]
301
315
  if stream.nil?
@@ -323,7 +337,7 @@ module HTTP2
323
337
  end
324
338
 
325
339
  decode_headers(frame)
326
- return if @state == :closed
340
+ next if closed?
327
341
 
328
342
  # PUSH_PROMISE frames MUST be associated with an existing, peer-
329
343
  # initiated stream... A receiver MUST treat the receipt of a
@@ -375,11 +389,16 @@ module HTTP2
375
389
  end
376
390
  else
377
391
  case frame_type
378
- # The PRIORITY frame can be sent for a stream in the "idle" or
379
- # "closed" state. This allows for the reprioritization of a
380
- # group of dependent streams by altering the priority of an
381
- # unused or closed parent stream.
392
+ # Priority signaling is deprecated (RFC 9113 Section 5.3.2), but
393
+ # a PRIORITY frame may still name a stream id not opened yet:
394
+ # activate it, so the priority sticks if the stream opens.
382
395
  when :priority
396
+ # After a GOAWAY, a PRIORITY for a stream already used and
397
+ # closed must not resurrect it: reprioritizing a closed
398
+ # parent is a no-op here, and a phantom stream would hold
399
+ # the draining connection open.
400
+ next if closed? && stream_id <= @last_stream_id
401
+
383
402
  stream = activate_stream(
384
403
  id: stream_id,
385
404
  weight: frame[:weight] || DEFAULT_WEIGHT,
@@ -399,7 +418,6 @@ module HTTP2
399
418
  unless @streams_recently_closed.key?(stream_id)
400
419
  connection_error(:protocol_error, msg: "sent window update on idle stream")
401
420
  end
402
- stream = @streams_recently_closed[stream_id]
403
421
  process_window_update(frame: frame, encode: true)
404
422
  # Endpoints MUST ignore
405
423
  # WINDOW_UPDATE or RST_STREAM frames received in this state (closed), though
@@ -496,7 +514,7 @@ module HTTP2
496
514
  @state = :connected
497
515
  connection_settings(frame)
498
516
 
499
- when :connected
517
+ when :connected, :closing
500
518
  case frame_type
501
519
  when :settings
502
520
  # @type var frame: settings_frame
@@ -511,8 +529,13 @@ module HTTP2
511
529
  # Receivers of a GOAWAY frame MUST NOT open additional streams on
512
530
  # the connection, although a new connection can be established
513
531
  # for new streams.
514
- @state = :closed
515
- @closed_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
532
+ if frame[:error] == :no_error && !@streams.empty?
533
+ # A graceful GOAWAY lets tracked streams complete before the
534
+ # connection closes (Section 6.8).
535
+ @state = :closing
536
+ else
537
+ close!
538
+ end
516
539
  emit(:goaway, frame[:last_stream], frame[:error], frame[:payload])
517
540
  when :altsvc
518
541
  # @type var frame: altsvc_frame
@@ -769,19 +792,28 @@ module HTTP2
769
792
  # connection managemet callbacks.
770
793
  #
771
794
  # @param id [Integer]
795
+ # @param max_concurrent_streams [Symbol] threshold to cap the number of active streams with.
772
796
  # @param priority [Integer]
773
797
  # @param window [Integer]
774
798
  # @param parent [Stream]
775
- def activate_stream(id:, **args)
799
+ def activate_stream(id:, max_concurrent_streams: @local_settings[:settings_max_concurrent_streams], **args)
776
800
  connection_error(msg: "Stream ID already exists") if @streams.key?(id)
777
801
 
778
- raise StreamLimitExceeded if @active_stream_count >= @local_settings[:settings_max_concurrent_streams]
802
+ # SETTINGS_MAX_CONCURRENT_STREAMS limits the number of concurrent streams that the sender
803
+ # of the setting permits the receiver to create (RFC 9113, section 5.1.2), so the bounding
804
+ # limit is the one advertised by the endpoint that did *not* open the stream.
805
+ raise StreamLimitExceeded if @active_stream_count >= max_concurrent_streams
779
806
 
780
807
  stream = Stream.new(connection: self, id: id, **args)
781
808
 
782
809
  stream.once(:close) do
783
810
  @streams.delete(id)
784
811
 
812
+ # A graceful GOAWAY leaves the connection :closing until the
813
+ # tracked streams complete (Section 6.8); the last one to close
814
+ # moves it to its terminal state.
815
+ close! if @state == :closing && @streams.empty?
816
+
785
817
  # Store a reference to the closed stream, such that we can respond
786
818
  # to any in-flight frames while close is registered on both sides.
787
819
  # References to such streams will be purged whenever another stream
@@ -812,6 +844,11 @@ module HTTP2
812
844
  @streams[id] = stream
813
845
  end
814
846
 
847
+ def close!
848
+ @state = :closed
849
+ @closed_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
850
+ end
851
+
815
852
  def verify_stream_order(id)
816
853
  return unless id.odd?
817
854
 
data/lib/http/2/error.rb CHANGED
@@ -53,7 +53,7 @@ module HTTP2
53
53
  # Raised if stream has been closed and new frames cannot be sent.
54
54
  class StreamClosed < Error; end
55
55
 
56
- # Raised if connection has been closed (or draining) and new stream
56
+ # Raised if connection has been closed (or closing) and new stream
57
57
  # cannot be opened.
58
58
  class ConnectionClosed < Error; end
59
59
 
@@ -26,9 +26,16 @@ module HTTP2
26
26
  def read_str(str, n)
27
27
  return "".b if n == 0
28
28
 
29
+ enc = str.encoding
30
+
31
+ if enc != Encoding::BINARY
32
+ str = str.dup if str.frozen?
33
+ str.force_encoding(Encoding::BINARY)
34
+ end
35
+
29
36
  chunk = str.byteslice(0, n)
30
37
  str.bytesplice(0, chunk.length, "")
31
- chunk
38
+ chunk.force_encoding(Encoding::BINARY)
32
39
  end
33
40
  else
34
41
  def read_str(str, n)
@@ -37,7 +44,7 @@ module HTTP2
37
44
  chunk = str.byteslice(0, n)
38
45
  remaining = str.byteslice(n, str.size - n)
39
46
  remaining ? str.replace(remaining) : str.clear
40
- chunk
47
+ chunk.force_encoding(Encoding::BINARY)
41
48
  end
42
49
  end
43
50
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP2
4
- VERSION = "1.2.0"
4
+ VERSION = "1.2.2"
5
5
  end
data/sig/connection.rbs CHANGED
@@ -59,6 +59,10 @@ module HTTP2
59
59
 
60
60
  def closed?: () -> bool
61
61
 
62
+ def closing?: () -> bool
63
+
64
+ def close!: () -> void
65
+
62
66
  def new_stream: (**untyped) -> Stream
63
67
 
64
68
  def ping: (String) -> void
@@ -95,7 +99,7 @@ module HTTP2
95
99
 
96
100
  def encode_headers: (headers_frame | push_promise_frame) -> void
97
101
 
98
- def activate_stream: (id: Integer, **untyped) -> Stream
102
+ def activate_stream: (id: Integer, ?max_concurrent_streams: Integer, **untyped) -> Stream
99
103
 
100
104
  def verify_stream_order: (Integer id) -> void
101
105
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: http-2
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
@@ -78,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
78
78
  - !ruby/object:Gem::Version
79
79
  version: '0'
80
80
  requirements: []
81
- rubygems_version: 3.6.9
81
+ rubygems_version: 4.0.16
82
82
  specification_version: 4
83
83
  summary: Pure-ruby HTTP 2.0 protocol implementation
84
84
  test_files: []