protocol-http2 0.26.2 → 0.27.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 71a2305405458d3561894bf24c86be5ea1173afcac8b7b4a9ef428d7456a8d71
4
- data.tar.gz: 33ab8df1d8cf05915b30dd0f7097e58c5ac0e9f4d41adf9fda9f404099efc66d
3
+ metadata.gz: e218c8562a926b98f24ad707341e1ee24d26ed6ce94b9f6550049000f415cd2b
4
+ data.tar.gz: 8ed236e94626982bd750d03a5c568c7b438e233b09256b671e5373db0881617c
5
5
  SHA512:
6
- metadata.gz: e839c86778149cea55719b358e8e145332c14fac5a8ef383f089d939d70dd4bdc654453e8e4ed71a6c0514c01bcbddc19382925e70a1c66fbc2ade195abbbc0c
7
- data.tar.gz: d92e068cc07e8222795c9faa3242a72d8cdabbcefb4a42e11177fe59c01c654e9df0f56adf9e65471bf26f1310cfe3b2fcb2f49e5d4ab03e471d9d077a9af554
6
+ metadata.gz: 31dfad736c2d3807ba103ccd70dea68d95375fe53f290582f805691c7b51cc43512494e2aae439c90082b0fbf518568c78e574bf9a3bb19dc4312127eb6378db
7
+ data.tar.gz: 2ee408ca054cd3ae3eb205489ea0b7095fe896af11bcb810e377a627d824cd2b6440d013ace9ebba79886ac71c067a541209007c9ee169dcd78803c2be531172
checksums.yaml.gz.sig CHANGED
Binary file
data/context/index.yaml CHANGED
@@ -3,6 +3,8 @@
3
3
  ---
4
4
  description: A low level implementation of the HTTP/2 protocol.
5
5
  metadata:
6
+ bug_tracker_uri: https://github.com/socketry/protocol-http2/issues
7
+ changelog_uri: https://github.com/socketry/protocol-http2/blob/main/releases.md
6
8
  documentation_uri: https://socketry.github.io/protocol-http2/
7
9
  source_code_uri: https://github.com/socketry/protocol-http2.git
8
10
  files:
@@ -3,6 +3,7 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2019-2026, by Samuel Williams.
5
5
  # Copyright, 2023, by Marco Concetto Rudilosso.
6
+ # Copyright, 2026, by Denis Talakevich.
6
7
 
7
8
  require_relative "framer"
8
9
  require_relative "flow_controlled"
@@ -44,6 +45,9 @@ module Protocol
44
45
 
45
46
  @local_window = LocalWindow.new
46
47
  @remote_window = Window.new
48
+
49
+ # The lowest Last-Stream-ID received in a GOAWAY frame, or nil if no GOAWAY frame has been received:
50
+ @goaway_stream_id = nil
47
51
  end
48
52
 
49
53
  # The connection stream ID (always 0 for connection-level operations).
@@ -94,16 +98,41 @@ module Protocol
94
98
  # The highest stream_id that has been successfully accepted by this connection.
95
99
  attr :remote_stream_id
96
100
 
101
+ # The lowest Last-Stream-ID received in a GOAWAY frame.
102
+ attr :goaway_stream_id
103
+
97
104
  # Whether the connection is effectively or actually closed.
98
105
  def closed?
99
106
  @state == :closed || @framer.nil?
100
107
  end
101
108
 
109
+ # Whether the remote peer has sent us a GOAWAY frame. We must not initiate any new streams on this connection, but existing streams may still be in progress.
110
+ # @returns [Boolean] True if a GOAWAY frame has been received.
111
+ def goaway_received?
112
+ !@goaway_stream_id.nil?
113
+ end
114
+
115
+ # Transition the connection into the closed state if a graceful GOAWAY was received and there is nothing left to drain.
116
+ #
117
+ # As with {close!}, this is a state transition only: the owner of the connection is responsible for closing the underlying framer.
118
+ def close_if_drained!
119
+ if self.goaway_received? && @streams.empty?
120
+ self.close!
121
+ end
122
+ end
123
+
102
124
  # Remove a stream from the active streams collection.
125
+ #
126
+ # If the remote peer has sent a graceful GOAWAY frame, the connection is only kept open in order to drain the streams it accepted, so when the last one completes there is nothing left to read.
127
+ #
103
128
  # @parameter id [Integer] The stream ID to remove.
104
129
  # @returns [Stream | Nil] The removed stream, or nil if not found.
105
130
  def delete(id)
106
- @streams.delete(id)
131
+ stream = @streams.delete(id)
132
+
133
+ self.close_if_drained!
134
+
135
+ return stream
107
136
  end
108
137
 
109
138
  # Close the underlying framer and all streams.
@@ -229,25 +258,39 @@ module Protocol
229
258
  end
230
259
 
231
260
  # Process a GOAWAY frame from the remote peer.
261
+ #
262
+ # A GOAWAY frame with a zero error code is a graceful shutdown: the remote peer will not accept any new streams, but it is still processing the streams at or below `last_stream_id` and will send their responses (RFC 9113 §6.8). We must keep reading until those streams complete, otherwise requests which the remote peer has already processed - and whose side effects have already happened - fail locally. The connection is closed once the last of those streams completes, or the remote peer closes it.
263
+ #
264
+ # A GOAWAY frame with a non-zero error code is a connection error: the connection transitions into the closed state and {GoawayError} is raised.
265
+ #
232
266
  # @parameter frame [GoawayFrame] The GOAWAY frame to process.
233
267
  # @raises [GoawayError] If the frame indicates a connection error.
234
268
  def receive_goaway(frame)
235
- # We capture the last stream that was processed.
236
- @remote_stream_id, error_code, message = frame.unpack
269
+ # We capture the last locally-initiated stream that may have been processed by the peer.
270
+ goaway_stream_id, error_code, message = frame.unpack
237
271
 
238
- self.close!
272
+ # A peer can send an initial GOAWAY with a high stream ID, followed by another GOAWAY with a lower stream ID. The effective cutoff can only decrease (RFC 9113 §6.8).
273
+ if @goaway_stream_id.nil? || goaway_stream_id < @goaway_stream_id
274
+ @goaway_stream_id = goaway_stream_id
275
+ end
239
276
 
240
- # Streams above the last stream ID were not processed by the remote peer and are safe to retry (RFC 9113 §6.8).
241
- error = ::Protocol::HTTP::RefusedError.new("GOAWAY: request not processed.")
277
+ # Locally-initiated streams above the last stream ID were not processed by the remote peer and are safe to retry (RFC 9113 §6.8). They are removed from the connection before being closed, both so that what remains is exactly the set of streams we are waiting on, and so that closing them cannot mutate the collection while we are traversing it.
278
+ refused_streams = @streams.select{|id, stream| local_stream_id?(id) && id > @goaway_stream_id}
279
+ refused_streams.each_key{|id| @streams.delete(id)}
242
280
 
243
- @streams.each_value do |stream|
244
- if stream.id > @remote_stream_id
245
- stream.close(error)
246
- end
281
+ # The state of the connection is decided before any stream is closed, so that it cannot be left undecided by a `closed` hook which raises, and cannot be influenced by one which creates a stream.
282
+ if error_code != 0
283
+ self.close!
284
+ else
285
+ self.close_if_drained!
286
+ end
287
+
288
+ unless refused_streams.empty?
289
+ error = ::Protocol::HTTP::RefusedError.new("GOAWAY: request not processed.")
290
+ refused_streams.each_value{|stream| stream.close(error)}
247
291
  end
248
292
 
249
293
  if error_code != 0
250
- # Shut down immediately.
251
294
  raise GoawayError.new(message, error_code)
252
295
  end
253
296
  end
@@ -404,6 +447,14 @@ module Protocol
404
447
  false
405
448
  end
406
449
 
450
+ # Check if the given stream ID represents a locally-initiated stream.
451
+ # This method should be overridden by client/server implementations.
452
+ # @parameter id [Integer] The stream ID to check.
453
+ # @returns [Boolean] True if the stream ID is locally-initiated.
454
+ def local_stream_id?(id)
455
+ false
456
+ end
457
+
407
458
  # Accept an incoming stream from the other side of the connnection.
408
459
  # On the server side, we accept requests.
409
460
  def accept_stream(stream_id, &block)
@@ -425,6 +476,11 @@ module Protocol
425
476
  # On the client side, we create requests.
426
477
  # @return [Stream] the created stream.
427
478
  def create_stream(id = next_stream_id, &block)
479
+ if self.goaway_received? and local_stream_id?(id)
480
+ # Receivers of a GOAWAY frame MUST NOT open additional streams on the connection (RFC 9113 §6.8). A new connection has to be established for new streams.
481
+ raise ProtocolError, "Cannot create stream #{id} after GOAWAY!"
482
+ end
483
+
428
484
  if @streams.key?(id)
429
485
  raise ProtocolError, "Cannot create stream with id #{id}, already exists!"
430
486
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2025, by Samuel Williams.
4
+ # Copyright, 2019-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "frame"
7
7
 
@@ -7,6 +7,6 @@
7
7
  module Protocol
8
8
  # @namespace
9
9
  module HTTP2
10
- VERSION = "0.26.2"
10
+ VERSION = "0.27.0"
11
11
  end
12
12
  end
data/license.md CHANGED
@@ -5,6 +5,7 @@ Copyright, 2019, by Yuta Iwama.
5
5
  Copyright, 2020, by Olle Jonsson.
6
6
  Copyright, 2023, by Marco Concetto Rudilosso.
7
7
  Copyright, 2024, by Adam Petro.
8
+ Copyright, 2026, by Denis Talakevich.
8
9
 
9
10
  Permission is hereby granted, free of charge, to any person obtaining a copy
10
11
  of this software and associated documentation files (the "Software"), to deal
data/readme.md CHANGED
@@ -14,6 +14,11 @@ Please see the [project documentation](https://socketry.github.io/protocol-http2
14
14
 
15
15
  Please see the [project releases](https://socketry.github.io/protocol-http2/releases/index) for all releases.
16
16
 
17
+ ### v0.27.0
18
+
19
+ - On a graceful `GOAWAY` (error code `0`), keep the connection open until the streams the remote peer accepted have completed, instead of closing it immediately and failing those requests with `EOFError`.
20
+ - `Connection#create_stream` refuses to open a locally-initiated stream once a `GOAWAY` has been received, as required by RFC 9113 §6.8.
21
+
17
22
  ### v0.26.2
18
23
 
19
24
  - Ignore the reserved high bit when decoding GOAWAY last stream IDs.
@@ -52,13 +57,6 @@ Please see the [project releases](https://socketry.github.io/protocol-http2/rele
52
57
 
53
58
  - **Breaking**: Removed support for priority frame and stream dependencies. The `Protocol::HTTP2::Stream` class no longer tracks dependencies, and `Stream#send_headers` no longer takes `priority` as the first argument. This change simplifies the internal implementation significantly as HTTP/2 priority frames have been deprecated in the protocol specification.
54
59
 
55
- ### v0.20.0
56
-
57
- - Improved performance of dependency management by avoiding linear search operations.
58
- - Removed `traces` as a required dependency - it's now optional and only used when explicitly needed.
59
- - Added better documentation for `maximum_concurrent_streams` setting.
60
- - Restored 100% test coverage and exposed trace provider for optional tracing support.
61
-
62
60
  ## See Also
63
61
 
64
62
  - [Async::HTTP](https://github.com/socketry/async-http) - A high-level HTTP client and server implementation.
@@ -67,26 +65,26 @@ Please see the [project releases](https://socketry.github.io/protocol-http2/rele
67
65
 
68
66
  We welcome contributions to this project.
69
67
 
70
- 1. Fork it.
68
+ 1. Fork the repository.
71
69
  2. Create your feature branch (`git checkout -b my-new-feature`).
72
- 3. Commit your changes (`git commit -am 'Add some feature'`).
70
+ 3. Commit your changes (`git commit -am 'Add some feature.'`).
73
71
  4. Push to the branch (`git push origin my-new-feature`).
74
- 5. Create new Pull Request.
72
+ 5. Create a new pull request.
75
73
 
76
74
  ### Running Tests
77
75
 
78
76
  To run the test suite:
79
77
 
80
- ``` shell
81
- bundle exec sus
78
+ ``` bash
79
+ $ bundle exec sus
82
80
  ```
83
81
 
84
82
  ### Making Releases
85
83
 
86
84
  To make a new release:
87
85
 
88
- ``` shell
89
- bundle exec bake gem:release:patch # or minor or major
86
+ ``` bash
87
+ $ bundle exec bake gem:release:patch # or minor or major
90
88
  ```
91
89
 
92
90
  ### Developer Certificate of Origin
data/releases.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Releases
2
2
 
3
+ ## v0.27.0
4
+
5
+ - On a graceful `GOAWAY` (error code `0`), keep the connection open until the streams the remote peer accepted have completed, instead of closing it immediately and failing those requests with `EOFError`.
6
+ - `Connection#create_stream` refuses to open a locally-initiated stream once a `GOAWAY` has been received, as required by RFC 9113 §6.8.
7
+
3
8
  ## v0.26.2
4
9
 
5
10
  - Ignore the reserved high bit when decoding GOAWAY last stream IDs.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,12 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protocol-http2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.26.2
4
+ version: 0.27.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  - Yuta Iwama
9
9
  - Adam Petro
10
+ - Denis Talakevich
10
11
  - Marco Concetto Rudilosso
11
12
  - Olle Jonsson
12
13
  bindir: bin
@@ -107,6 +108,8 @@ homepage: https://github.com/socketry/protocol-http2
107
108
  licenses:
108
109
  - MIT
109
110
  metadata:
111
+ bug_tracker_uri: https://github.com/socketry/protocol-http2/issues
112
+ changelog_uri: https://github.com/socketry/protocol-http2/blob/main/releases.md
110
113
  documentation_uri: https://socketry.github.io/protocol-http2/
111
114
  source_code_uri: https://github.com/socketry/protocol-http2.git
112
115
  rdoc_options: []
metadata.gz.sig CHANGED
Binary file