protocol-http2 0.22.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b23c19aa916a6d23a22b7f49fddc73fa91dd86957222178866a1389f74b4a037
4
- data.tar.gz: b957666cdcf46c542baaebce6ec86d29d449ee80de4b4c98ca0409956b14b909
3
+ metadata.gz: 163e984c0ac8a9a19c5dd3fd4f09c7b4be35622413a562ff80f14dece63f86e3
4
+ data.tar.gz: 5329a5966f521513c7bc275a426c42264aa2fcc5078b39dbb0c84945cf18240b
5
5
  SHA512:
6
- metadata.gz: 2673eba3eafe1131725b5212beb8a3fc2c2cfd98c0b62753aecf6f1b8729f6c7e402a4d6b194ec4719e60d7489ec4c007b76e034c09573d972806aa6f9ac81ca
7
- data.tar.gz: 2d8473793aced6539d345c58274dc48665aaa29160a08fdca034d839036ef201d0dd2e05c90c33d54564e157200151fc36469a9a5feb9f679884e5ebc323d6ac
6
+ metadata.gz: 301bdf39a174d8db27486c568abf7c01926ed0c290502e3580500a461e6e6e5ac296630b4dce3c56207f314ee5bab29d7ff18eead4ad54d5ea2326043a70f118
7
+ data.tar.gz: 58f22287bd88d4c2844d1742ecbc2bf57f8640ec6b35400ade3aa565101715c1282ed59d354b8a3c5c77103e83f87ce3764865ea5ff335721f63a14bcf527c48
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,82 @@
1
+ # Getting Started
2
+
3
+ This guide explains how to use the `protocol-http2` gem to implement a basic HTTP/2 client.
4
+
5
+ ## Installation
6
+
7
+ Add the gem to your project:
8
+
9
+ ``` bash
10
+ $ bundle add protocol-http2
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ This gem provides a low-level implementation of the HTTP/2 protocol. It is designed to be used in conjunction with other libraries to provide a complete HTTP/2 client or server. However, it is straight forward to give examples of how to use the library directly.
16
+
17
+ ### Client
18
+
19
+ Here is a basic HTTP/2 client:
20
+
21
+ ``` ruby
22
+ require "async"
23
+ require "async/io/stream"
24
+ require "async/http/endpoint"
25
+ require "protocol/http2/client"
26
+
27
+ Async do
28
+ endpoint = Async::HTTP::Endpoint.parse("https://www.google.com/search?q=kittens")
29
+
30
+ peer = endpoint.connect
31
+
32
+ puts "Connected to #{peer.inspect}"
33
+
34
+ # IO Buffering:
35
+ stream = Async::IO::Stream.new(peer)
36
+
37
+ framer = Protocol::HTTP2::Framer.new(stream)
38
+ client = Protocol::HTTP2::Client.new(framer)
39
+
40
+ puts "Sending connection preface..."
41
+ client.send_connection_preface
42
+
43
+ puts "Creating stream..."
44
+ stream = client.create_stream
45
+
46
+ headers = [
47
+ [":scheme", endpoint.scheme],
48
+ [":method", "GET"],
49
+ [":authority", "www.google.com"],
50
+ [":path", endpoint.path],
51
+ ["accept", "*/*"],
52
+ ]
53
+
54
+ puts "Sending request on stream id=#{stream.id} state=#{stream.state}..."
55
+ stream.send_headers(headers, Protocol::HTTP2::END_STREAM)
56
+
57
+ puts "Waiting for response..."
58
+ $count = 0
59
+
60
+ def stream.process_headers(frame)
61
+ headers = super
62
+ puts "Got response headers: #{headers} (#{frame.end_stream?})"
63
+ end
64
+
65
+ def stream.receive_data(frame)
66
+ data = super
67
+
68
+ $count += data.scan(/kittens/).count
69
+
70
+ puts "Got response data: #{data.bytesize}"
71
+ end
72
+
73
+ until stream.closed?
74
+ frame = client.read_frame
75
+ end
76
+
77
+ puts "Got #{$count} kittens!"
78
+
79
+ puts "Closing client..."
80
+ client.close
81
+ end
82
+ ```
@@ -0,0 +1,12 @@
1
+ # Automatically generated context index for Utopia::Project guides.
2
+ # Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
3
+ ---
4
+ description: A low level implementation of the HTTP/2 protocol.
5
+ metadata:
6
+ documentation_uri: https://socketry.github.io/protocol-http2/
7
+ source_code_uri: https://github.com/socketry/protocol-http2.git
8
+ files:
9
+ - path: getting-started.md
10
+ title: Getting Started
11
+ description: This guide explains how to use the `protocol-http2` gem to implement
12
+ a basic HTTP/2 client.
@@ -1,29 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "connection"
7
7
 
8
8
  module Protocol
9
9
  module HTTP2
10
+ # Represents an HTTP/2 client connection.
11
+ # Manages client-side protocol semantics including stream ID allocation,
12
+ # connection preface handling, and push promise processing.
10
13
  class Client < Connection
14
+ # Initialize a new HTTP/2 client connection.
15
+ # @parameter framer [Framer] The frame handler for reading/writing HTTP/2 frames.
11
16
  def initialize(framer)
12
17
  super(framer, 1)
13
18
  end
14
19
 
20
+ # Check if the given stream ID represents a locally-initiated stream.
21
+ # Client streams have odd numbered IDs.
22
+ # @parameter id [Integer] The stream ID to check.
23
+ # @returns [bool] True if the stream ID is locally-initiated.
15
24
  def local_stream_id?(id)
16
25
  id.odd?
17
26
  end
18
27
 
28
+ # Check if the given stream ID represents a remotely-initiated stream.
29
+ # Server streams have even numbered IDs.
30
+ # @parameter id [Integer] The stream ID to check.
31
+ # @returns [bool] True if the stream ID is remotely-initiated.
19
32
  def remote_stream_id?(id)
20
33
  id.even?
21
34
  end
22
35
 
36
+ # Check if the given stream ID is valid for remote initiation.
37
+ # Server-initiated streams must have even numbered IDs.
38
+ # @parameter stream_id [Integer] The stream ID to validate.
39
+ # @returns [bool] True if the stream ID is valid for remote initiation.
23
40
  def valid_remote_stream_id?(stream_id)
24
41
  stream_id.even?
25
42
  end
26
43
 
44
+ # Send the HTTP/2 connection preface and initial settings.
45
+ # This must be called once when the connection is first established.
46
+ # @parameter settings [Array] Optional settings to send with the connection preface.
47
+ # @raises [ProtocolError] If called when not in the new state.
48
+ # @yields Allows custom processing during preface exchange.
27
49
  def send_connection_preface(settings = [])
28
50
  if @state == :new
29
51
  @framer.write_connection_preface
@@ -42,10 +64,15 @@ module Protocol
42
64
  end
43
65
  end
44
66
 
67
+ # Clients cannot create push promise streams.
68
+ # @raises [ProtocolError] Always, as clients cannot initiate push promises.
45
69
  def create_push_promise_stream
46
70
  raise ProtocolError, "Cannot create push promises from client!"
47
71
  end
48
72
 
73
+ # Process a push promise frame received from the server.
74
+ # @parameter frame [PushPromiseFrame] The push promise frame to process.
75
+ # @returns [Array(Stream, Hash) | Nil] The promised stream and request headers, or nil if no associated stream.
49
76
  def receive_push_promise(frame)
50
77
  if frame.stream_id == 0
51
78
  raise ProtocolError, "Cannot receive headers for stream 0!"
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2026, by Samuel Williams.
5
5
  # Copyright, 2023, by Marco Concetto Rudilosso.
6
6
 
7
7
  require_relative "framer"
@@ -12,9 +12,14 @@ require "protocol/http/header/priority"
12
12
 
13
13
  module Protocol
14
14
  module HTTP2
15
+ # This is the core connection class that handles HTTP/2 protocol semantics including
16
+ # stream management, settings negotiation, and frame processing.
15
17
  class Connection
16
18
  include FlowControlled
17
19
 
20
+ # Initialize a new HTTP/2 connection.
21
+ # @parameter framer [Framer] The frame handler for reading/writing HTTP/2 frames.
22
+ # @parameter local_stream_id [Integer] The starting stream ID for locally-initiated streams.
18
23
  def initialize(framer, local_stream_id)
19
24
  super()
20
25
 
@@ -41,10 +46,15 @@ module Protocol
41
46
  @remote_window = Window.new
42
47
  end
43
48
 
49
+ # The connection stream ID (always 0 for connection-level operations).
50
+ # @returns [Integer] Always returns 0 for the connection itself.
44
51
  def id
45
52
  0
46
53
  end
47
54
 
55
+ # Access streams by ID, with 0 returning the connection itself.
56
+ # @parameter id [Integer] The stream ID to look up.
57
+ # @returns [Connection | Stream | Nil] The connection (if id=0), stream, or nil.
48
58
  def [] id
49
59
  if id.zero?
50
60
  self
@@ -89,6 +99,9 @@ module Protocol
89
99
  @state == :closed || @framer.nil?
90
100
  end
91
101
 
102
+ # Remove a stream from the active streams collection.
103
+ # @parameter id [Integer] The stream ID to remove.
104
+ # @returns [Stream | Nil] The removed stream, or nil if not found.
92
105
  def delete(id)
93
106
  @streams.delete(id)
94
107
  end
@@ -96,6 +109,12 @@ module Protocol
96
109
  # Close the underlying framer and all streams.
97
110
  def close(error = nil)
98
111
  # The underlying socket may already be closed by this point.
112
+
113
+ # If there are active streams when the connection closes, it's an error for those streams, even if the connection itself closed cleanly:
114
+ if @streams.any? and error.nil?
115
+ error = EOFError.new("Connection closed with #{@streams.size} active stream(s)!")
116
+ end
117
+
99
118
  @streams.each_value{|stream| stream.close(error)}
100
119
  @streams.clear
101
120
 
@@ -106,10 +125,17 @@ module Protocol
106
125
  end
107
126
  end
108
127
 
128
+ # Encode headers using HPACK compression.
129
+ # @parameter headers [Array] The headers to encode.
130
+ # @parameter buffer [String] Optional buffer for encoding output.
131
+ # @returns [String] The encoded header block.
109
132
  def encode_headers(headers, buffer = String.new.b)
110
133
  HPACK::Compressor.new(buffer, @encoder, table_size_limit: @remote_settings.header_table_size).encode(headers)
111
134
  end
112
135
 
136
+ # Decode headers using HPACK decompression.
137
+ # @parameter data [String] The encoded header block data.
138
+ # @returns [Array] The decoded headers.
113
139
  def decode_headers(data)
114
140
  HPACK::Decompressor.new(data, @decoder, table_size_limit: @local_settings.header_table_size).decode
115
141
  end
@@ -141,6 +167,9 @@ module Protocol
141
167
  end
142
168
  end
143
169
 
170
+ # Execute a block within a synchronized context.
171
+ # This method provides a synchronization primitive for thread safety.
172
+ # @yields The block to execute within the synchronized context.
144
173
  def synchronize
145
174
  yield
146
175
  end
@@ -171,6 +200,8 @@ module Protocol
171
200
  raise
172
201
  end
173
202
 
203
+ # Send updated settings to the remote peer.
204
+ # @parameter changes [Hash] The settings changes to send.
174
205
  def send_settings(changes)
175
206
  @local_settings.append(changes)
176
207
 
@@ -197,6 +228,9 @@ module Protocol
197
228
  self.close!
198
229
  end
199
230
 
231
+ # Process a GOAWAY frame from the remote peer.
232
+ # @parameter frame [GoawayFrame] The GOAWAY frame to process.
233
+ # @raises [GoawayError] If the frame indicates a connection error.
200
234
  def receive_goaway(frame)
201
235
  # We capture the last stream that was processed.
202
236
  @remote_stream_id, error_code, message = frame.unpack
@@ -209,6 +243,8 @@ module Protocol
209
243
  end
210
244
  end
211
245
 
246
+ # Write a single frame to the connection.
247
+ # @parameter frame [Frame] The frame to write.
212
248
  def write_frame(frame)
213
249
  synchronize do
214
250
  @framer.write_frame(frame)
@@ -217,6 +253,10 @@ module Protocol
217
253
  @framer.flush
218
254
  end
219
255
 
256
+ # Write multiple frames within a synchronized block.
257
+ # @yields {|framer| ...} The framer for writing multiple frames.
258
+ # @parameter framer [Framer] The framer instance.
259
+ # @raises [EOFError] If the connection is closed.
220
260
  def write_frames
221
261
  if @framer
222
262
  synchronize do
@@ -229,6 +269,8 @@ module Protocol
229
269
  end
230
270
  end
231
271
 
272
+ # Update local settings and adjust stream window capacities.
273
+ # @parameter changes [Hash] The settings changes to apply locally.
232
274
  def update_local_settings(changes)
233
275
  capacity = @local_settings.initial_window_size
234
276
 
@@ -239,6 +281,8 @@ module Protocol
239
281
  @local_window.desired = capacity
240
282
  end
241
283
 
284
+ # Update remote settings and adjust stream window capacities.
285
+ # @parameter changes [Hash] The settings changes to apply to remote peer.
242
286
  def update_remote_settings(changes)
243
287
  capacity = @remote_settings.initial_window_size
244
288
 
@@ -273,12 +317,17 @@ module Protocol
273
317
  end
274
318
  end
275
319
 
320
+ # Transition the connection to the open state.
321
+ # @returns [Connection] Self for method chaining.
276
322
  def open!
277
323
  @state = :open
278
324
 
279
325
  return self
280
326
  end
281
327
 
328
+ # Receive and process a SETTINGS frame from the remote peer.
329
+ # @parameter frame [SettingsFrame] The settings frame to process.
330
+ # @raises [ProtocolError] If the connection is in an invalid state.
282
331
  def receive_settings(frame)
283
332
  if @state == :new
284
333
  # We transition to :open when we receive acknowledgement of first settings frame:
@@ -290,6 +339,8 @@ module Protocol
290
339
  end
291
340
  end
292
341
 
342
+ # Send a PING frame to the remote peer.
343
+ # @parameter data [String] The 8-byte ping payload data.
293
344
  def send_ping(data)
294
345
  if @state != :closed
295
346
  frame = PingFrame.new
@@ -301,6 +352,9 @@ module Protocol
301
352
  end
302
353
  end
303
354
 
355
+ # Process a PING frame from the remote peer.
356
+ # @parameter frame [PingFrame] The ping frame to process.
357
+ # @raises [ProtocolError] If ping is received in invalid state.
304
358
  def receive_ping(frame)
305
359
  if @state != :closed
306
360
  # This is handled in `read_payload`:
@@ -318,6 +372,9 @@ module Protocol
318
372
  end
319
373
  end
320
374
 
375
+ # Process a DATA frame from the remote peer.
376
+ # @parameter frame [DataFrame] The data frame to process.
377
+ # @raises [ProtocolError] If data is received for invalid stream.
321
378
  def receive_data(frame)
322
379
  update_local_window(frame)
323
380
 
@@ -330,6 +387,10 @@ module Protocol
330
387
  end
331
388
  end
332
389
 
390
+ # Check if the given stream ID is valid for remote initiation.
391
+ # This method should be overridden by client/server implementations.
392
+ # @parameter stream_id [Integer] The stream ID to validate.
393
+ # @returns [Boolean] True if the stream ID is valid for remote initiation.
333
394
  def valid_remote_stream_id?(stream_id)
334
395
  false
335
396
  end
@@ -366,6 +427,10 @@ module Protocol
366
427
  end
367
428
  end
368
429
 
430
+ # Create a push promise stream.
431
+ # This method should be overridden by client/server implementations.
432
+ # @yields {|stream| ...} Optional block to configure the created stream.
433
+ # @returns [Stream] The created push promise stream.
369
434
  def create_push_promise_stream(&block)
370
435
  create_stream(&block)
371
436
  end
@@ -397,10 +462,16 @@ module Protocol
397
462
  end
398
463
  end
399
464
 
465
+ # Receive and process a PUSH_PROMISE frame.
466
+ # @parameter frame [PushPromiseFrame] The push promise frame.
467
+ # @raises [ProtocolError] Always raises as push promises are not supported.
400
468
  def receive_push_promise(frame)
401
469
  raise ProtocolError, "Unable to receive push promise!"
402
470
  end
403
471
 
472
+ # Receive and process a PRIORITY_UPDATE frame.
473
+ # @parameter frame [PriorityUpdateFrame] The priority update frame.
474
+ # @raises [ProtocolError] If the stream ID is invalid.
404
475
  def receive_priority_update(frame)
405
476
  if frame.stream_id != 0
406
477
  raise ProtocolError, "Invalid stream id: #{frame.stream_id}"
@@ -414,14 +485,25 @@ module Protocol
414
485
  end
415
486
  end
416
487
 
488
+ # Check if the given stream ID represents a client-initiated stream.
489
+ # Client streams always have odd numbered IDs.
490
+ # @parameter id [Integer] The stream ID to check.
491
+ # @returns [Boolean] True if the stream ID is client-initiated.
417
492
  def client_stream_id?(id)
418
493
  id.odd?
419
494
  end
420
495
 
496
+ # Check if the given stream ID represents a server-initiated stream.
497
+ # Server streams always have even numbered IDs.
498
+ # @parameter id [Integer] The stream ID to check.
499
+ # @returns [Boolean] True if the stream ID is server-initiated.
421
500
  def server_stream_id?(id)
422
501
  id.even?
423
502
  end
424
503
 
504
+ # Check if the given stream ID represents an idle stream.
505
+ # @parameter id [Integer] The stream ID to check.
506
+ # @returns [Boolean] True if the stream ID is idle (not yet used).
425
507
  def idle_stream_id?(id)
426
508
  if id.even?
427
509
  # Server-initiated streams are even.
@@ -450,6 +532,9 @@ module Protocol
450
532
  end
451
533
  end
452
534
 
535
+ # Receive and process a RST_STREAM frame.
536
+ # @parameter frame [ResetStreamFrame] The reset stream frame.
537
+ # @raises [ProtocolError] If the frame is invalid for connection context.
453
538
  def receive_reset_stream(frame)
454
539
  if frame.connection?
455
540
  raise ProtocolError, "Cannot reset connection!"
@@ -475,6 +560,8 @@ module Protocol
475
560
  end
476
561
  end
477
562
 
563
+ # Receive and process a WINDOW_UPDATE frame.
564
+ # @parameter frame [WindowUpdateFrame] The window update frame.
478
565
  def receive_window_update(frame)
479
566
  if frame.connection?
480
567
  super
@@ -494,10 +581,15 @@ module Protocol
494
581
  end
495
582
  end
496
583
 
584
+ # Receive and process a CONTINUATION frame.
585
+ # @parameter frame [ContinuationFrame] The continuation frame.
586
+ # @raises [ProtocolError] Always raises as unexpected continuation frames are not supported.
497
587
  def receive_continuation(frame)
498
588
  raise ProtocolError, "Received unexpected continuation: #{frame.class}"
499
589
  end
500
590
 
591
+ # Receive and process a generic frame (default handler).
592
+ # @parameter frame [Frame] The frame to receive.
501
593
  def receive_frame(frame)
502
594
  # ignore.
503
595
  end
@@ -1,31 +1,52 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "frame"
7
7
 
8
8
  module Protocol
9
9
  module HTTP2
10
+ # Module for frames that can be continued with CONTINUATION frames.
10
11
  module Continued
12
+ # @constant [Integer] The maximum number of continuation frames to read to prevent resource exhaustion.
13
+ LIMIT = 8
14
+
15
+ # Initialize a continuable frame.
16
+ # @parameter arguments [Array] Arguments passed to parent constructor.
11
17
  def initialize(*)
12
18
  super
13
19
 
14
20
  @continuation = nil
15
21
  end
16
22
 
23
+ # Check if this frame has continuation frames.
24
+ # @returns [Boolean] True if there are continuation frames.
17
25
  def continued?
18
26
  !!@continuation
19
27
  end
20
28
 
29
+ # Check if this is the last header block fragment.
30
+ # @returns [Boolean] True if the END_HEADERS flag is set.
21
31
  def end_headers?
22
32
  flag_set?(END_HEADERS)
23
33
  end
24
34
 
25
- def read(stream, maximum_frame_size)
26
- super
35
+ # Read the frame and any continuation frames from the stream.
36
+ #
37
+ # There is an upper limit to the number of continuation frames that can be read to prevent resource exhaustion. If the limit is 0, only one frame will be read (the initial frame). Otherwise, the limit decrements with each continuation frame read.
38
+ #
39
+ # @parameter stream [IO] The stream to read from.
40
+ # @parameter maximum_frame_size [Integer] Maximum allowed frame size.
41
+ # @parameter limit [Integer] The maximum number of continuation frames to read.
42
+ def read(stream, maximum_frame_size, limit = LIMIT)
43
+ super(stream, maximum_frame_size)
27
44
 
28
45
  unless end_headers?
46
+ if limit.zero?
47
+ raise ProtocolError, "Too many continuation frames!"
48
+ end
49
+
29
50
  continuation = ContinuationFrame.new
30
51
  continuation.read_header(stream)
31
52
 
@@ -38,12 +59,14 @@ module Protocol
38
59
  raise ProtocolError, "Invalid stream id: #{continuation.stream_id} for continuation of stream id: #{@stream_id}!"
39
60
  end
40
61
 
41
- continuation.read(stream, maximum_frame_size)
62
+ continuation.read(stream, maximum_frame_size, limit - 1)
42
63
 
43
64
  @continuation = continuation
44
65
  end
45
66
  end
46
67
 
68
+ # Write the frame and any continuation frames to the stream.
69
+ # @parameter stream [IO] The stream to write to.
47
70
  def write(stream)
48
71
  super
49
72
 
@@ -54,6 +77,9 @@ module Protocol
54
77
 
55
78
  attr_accessor :continuation
56
79
 
80
+ # Pack data into this frame, creating continuation frames if needed.
81
+ # @parameter data [String] The data to pack.
82
+ # @parameter options [Hash] Options including maximum_size.
57
83
  def pack(data, **options)
58
84
  maximum_size = options[:maximum_size]
59
85
 
@@ -75,6 +101,8 @@ module Protocol
75
101
  end
76
102
  end
77
103
 
104
+ # Unpack data from this frame and any continuation frames.
105
+ # @returns [String] The complete unpacked data.
78
106
  def unpack
79
107
  if @continuation.nil?
80
108
  super
@@ -95,11 +123,21 @@ module Protocol
95
123
 
96
124
  TYPE = 0x9
97
125
 
126
+ # Read the frame and any continuation frames from the stream.
127
+ # @parameter stream [IO] The stream to read from.
128
+ # @parameter maximum_frame_size [Integer] Maximum allowed frame size.
129
+ # @parameter limit [Integer] The maximum number of continuation frames to read.
130
+ def read(stream, maximum_frame_size, limit = 8)
131
+ super
132
+ end
133
+
98
134
  # This is only invoked if the continuation is received out of the normal flow.
99
135
  def apply(connection)
100
136
  connection.receive_continuation(self)
101
137
  end
102
138
 
139
+ # Get a string representation of the continuation frame.
140
+ # @returns [String] Human-readable frame information.
103
141
  def inspect
104
142
  "\#<#{self.class} stream_id=#{@stream_id} flags=#{@flags} length=#{@length || 0}b>"
105
143
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "frame"
7
7
  require_relative "padded"
@@ -25,10 +25,16 @@ module Protocol
25
25
 
26
26
  TYPE = 0x0
27
27
 
28
+ # Check if this frame marks the end of the stream.
29
+ # @returns [Boolean] True if the END_STREAM flag is set.
28
30
  def end_stream?
29
31
  flag_set?(END_STREAM)
30
32
  end
31
33
 
34
+ # Pack data into the frame, handling empty data as stream end.
35
+ # @parameter data [String | Nil] The data to pack into the frame.
36
+ # @parameter arguments [Array] Additional arguments passed to super.
37
+ # @parameter options [Hash] Additional options passed to super.
32
38
  def pack(data, *arguments, **options)
33
39
  if data
34
40
  super
@@ -38,10 +44,14 @@ module Protocol
38
44
  end
39
45
  end
40
46
 
47
+ # Apply this DATA frame to a connection for processing.
48
+ # @parameter connection [Connection] The connection to apply the frame to.
41
49
  def apply(connection)
42
50
  connection.receive_data(self)
43
51
  end
44
52
 
53
+ # Provide a readable representation of the frame for debugging.
54
+ # @returns [String] A formatted string representation of the frame.
45
55
  def inspect
46
56
  "\#<#{self.class} stream_id=#{@stream_id} flags=#{@flags} #{@length || 0}b>"
47
57
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
6
  require "protocol/http/error"
7
7
 
@@ -57,11 +57,14 @@ module Protocol
57
57
  # connection must be aborted.
58
58
  class HandshakeError < Error
59
59
  end
60
-
60
+
61
61
  # Raised by stream or connection handlers, results in GOAWAY frame
62
62
  # which signals termination of the current connection. You *cannot*
63
63
  # recover from this exception, or any exceptions subclassed from it.
64
64
  class ProtocolError < Error
65
+ # Initialize a protocol error with message and error code.
66
+ # @parameter message [String] The error message.
67
+ # @parameter code [Integer] The HTTP/2 error code.
65
68
  def initialize(message, code = PROTOCOL_ERROR)
66
69
  super(message)
67
70
 
@@ -71,26 +74,36 @@ module Protocol
71
74
  attr :code
72
75
  end
73
76
 
77
+ # Represents an error specific to stream operations.
74
78
  class StreamError < ProtocolError
75
79
  end
76
80
 
81
+ # Represents an error for operations on closed streams.
77
82
  class StreamClosed < StreamError
83
+ # Initialize a stream closed error.
84
+ # @parameter message [String] The error message.
78
85
  def initialize(message)
79
86
  super message, STREAM_CLOSED
80
87
  end
81
88
  end
82
89
 
90
+ # Represents a GOAWAY-related protocol error.
83
91
  class GoawayError < ProtocolError
84
92
  end
85
93
 
86
94
  # When the frame payload does not match expectations.
87
95
  class FrameSizeError < ProtocolError
96
+ # Initialize a frame size error.
97
+ # @parameter message [String] The error message.
88
98
  def initialize(message)
89
99
  super message, FRAME_SIZE_ERROR
90
100
  end
91
101
  end
92
102
 
103
+ # Represents a header processing error.
93
104
  class HeaderError < StreamClosed
105
+ # Initialize a header error.
106
+ # @parameter message [String] The error message.
94
107
  def initialize(message)
95
108
  super(message)
96
109
  end
@@ -98,6 +111,8 @@ module Protocol
98
111
 
99
112
  # Raised on invalid flow control frame or command.
100
113
  class FlowControlError < ProtocolError
114
+ # Initialize a flow control error.
115
+ # @parameter message [String] The error message.
101
116
  def initialize(message)
102
117
  super message, FLOW_CONTROL_ERROR
103
118
  end