protocol-http1 0.31.0 → 0.32.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: 29db57e08ceb65831410a015b07b643fe3223c119327f001485c4d942195516e
4
- data.tar.gz: 01b83de5925e1d89d4ffb729f22bab647e4fa5ac04d239a2ff81899bacb395f9
3
+ metadata.gz: 1908561ca0c3707f9e64cbb12c5014c03858f2655dbb4fab1fe18cd31113272c
4
+ data.tar.gz: 113d3d3b14b2e4258c4bbcb401ff0fefe07921d85edd231c395d0121f9c8c026
5
5
  SHA512:
6
- metadata.gz: 564dcdb26845995de2c5f05b0b8e6c464615a6cc7345685c30e5d28bca09ccd03f3b58075da67e7afb3aab5ecb8213db76fb3fad1e59643899f68e0415ca915b
7
- data.tar.gz: a10dbcdcf55d5687eb3e94476869b2ce4af1502a056e5f70b4dfb1c751ba2bf2bad46e11cd8d8ae666314390eec95daef1c39a5ad93debab744718fadd568535
6
+ metadata.gz: 47d9919aa52376eb982b92a08dc33038e8fd6f201efee3c6b4da0b9f9b4acf2d7dee5bc6ae3023ab28067ec377ac302fc63d679f7113819e907272b5234b8b93
7
+ data.tar.gz: 47e13c906b80c14b9bb85abfc367daef71392454c1f7d1b2629afa8b1fbbdf51cf0c80d3e1d65599d25e46cc2cac517c00580b7ae7b8a55a09e5fe9521b0534f
checksums.yaml.gz.sig CHANGED
Binary file
@@ -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
  # Copyright, 2023, by Thomas Morgan.
6
6
 
7
7
  require "protocol/http/body/readable"
@@ -9,9 +9,16 @@ require "protocol/http/body/readable"
9
9
  module Protocol
10
10
  module HTTP1
11
11
  module Body
12
+ # Represents a chunked body, which is a series of chunks, each with a length prefix.
13
+ #
14
+ # See https://tools.ietf.org/html/rfc7230#section-4.1 for more details on the chunked transfer encoding.
12
15
  class Chunked < HTTP::Body::Readable
13
16
  CRLF = "\r\n"
14
17
 
18
+ # Initialize the chunked body.
19
+ #
20
+ # @parameter connection [Protocol::HTTP1::Connection] the connection to read the body from.
21
+ # @parameter headers [Protocol::HTTP::Headers] the headers to read the trailer into, if any.
15
22
  def initialize(connection, headers)
16
23
  @connection = connection
17
24
  @finished = false
@@ -22,19 +29,25 @@ module Protocol
22
29
  @count = 0
23
30
  end
24
31
 
32
+ # @attribute [Integer] the number of chunks read so far.
25
33
  attr :count
26
34
 
35
+ # @attribute [Integer] the length of the body if known.
27
36
  def length
28
- # We only know the length once we've read everything. This is because the length is not known until the final chunk is read.
37
+ # We only know the length once we've read the final chunk:
29
38
  if @finished
30
39
  @length
31
40
  end
32
41
  end
33
42
 
43
+ # @returns [Boolean] true if the body is empty, in other words {read} will return `nil`.
34
44
  def empty?
35
45
  @connection.nil?
36
46
  end
37
47
 
48
+ # Close the connection and mark the body as finished.
49
+ #
50
+ # @parameter error [Exception | Nil] the error that caused the body to be closed, if any.
38
51
  def close(error = nil)
39
52
  if connection = @connection
40
53
  @connection = nil
@@ -49,7 +62,12 @@ module Protocol
49
62
 
50
63
  VALID_CHUNK_LENGTH = /\A[0-9a-fA-F]+\z/
51
64
 
65
+ # Read a chunk of data.
66
+ #
52
67
  # Follows the procedure outlined in https://tools.ietf.org/html/rfc7230#section-4.1.3
68
+ #
69
+ # @returns [String | Nil] the next chunk of data, or `nil` if the body is finished.
70
+ # @raises [EOFError] if the connection is closed before the expected length is read.
53
71
  def read
54
72
  if !@finished
55
73
  if @connection
@@ -96,12 +114,14 @@ module Protocol
96
114
  end
97
115
  end
98
116
 
117
+ # @returns [String] a human-readable representation of the body.
99
118
  def inspect
100
119
  "\#<#{self.class} #{@length} bytes read in #{@count} chunks>"
101
120
  end
102
121
 
103
122
  private
104
123
 
124
+ # Read the trailer from the connection, and add any headers to the trailer.
105
125
  def read_trailer
106
126
  while line = @connection.read_line?
107
127
  # Empty line indicates end of trailer:
@@ -1,14 +1,19 @@
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/body/readable"
7
7
 
8
8
  module Protocol
9
9
  module HTTP1
10
10
  module Body
11
+ # Represents a fixed length body.
11
12
  class Fixed < HTTP::Body::Readable
13
+ # Initialize the body with the given connection and length.
14
+ #
15
+ # @parameter connection [Protocol::HTTP1::Connection] the connection to read the body from.
16
+ # @parameter length [Integer] the length of the body.
12
17
  def initialize(connection, length)
13
18
  @connection = connection
14
19
 
@@ -16,13 +21,20 @@ module Protocol
16
21
  @remaining = length
17
22
  end
18
23
 
24
+ # @attribute [Integer] the length of the body.
19
25
  attr :length
26
+
27
+ # @attribute [Integer] the remaining bytes to read.
20
28
  attr :remaining
21
29
 
30
+ # @returns [Boolean] true if the body is empty.
22
31
  def empty?
23
32
  @connection.nil? or @remaining == 0
24
33
  end
25
34
 
35
+ # Close the connection.
36
+ #
37
+ # @parameter error [Exception | Nil] the error that caused the connection to be closed, if any.
26
38
  def close(error = nil)
27
39
  if connection = @connection
28
40
  @connection = nil
@@ -35,7 +47,10 @@ module Protocol
35
47
  super
36
48
  end
37
49
 
38
- # @raises EOFError if the connection is closed before the expected length is read.
50
+ # Read a chunk of data.
51
+ #
52
+ # @returns [String | Nil] the next chunk of data.
53
+ # @raises [EOFError] if the connection is closed before the expected length is read.
39
54
  def read
40
55
  if @remaining > 0
41
56
  if @connection
@@ -57,6 +72,7 @@ module Protocol
57
72
  end
58
73
  end
59
74
 
75
+ # @returns [String] a human-readable representation of the body.
60
76
  def inspect
61
77
  "\#<#{self.class} length=#{@length} remaining=#{@remaining} state=#{@connection ? 'open' : 'closed'}>"
62
78
  end
@@ -1,26 +1,31 @@
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/body/readable"
7
7
 
8
8
  module Protocol
9
9
  module HTTP1
10
10
  module Body
11
- # A body that reads all remaining data from the connection.
11
+ # Represents the remainder of the body, which reads all the data from the connection until it is finished.
12
12
  class Remainder < HTTP::Body::Readable
13
13
  BLOCK_SIZE = 1024 * 64
14
14
 
15
- # block_size may be removed in the future. It is better managed by connection.
16
- def initialize(connection)
15
+ # Initialize the body with the given connection.
16
+ #
17
+ # @parameter connection [Protocol::HTTP1::Connection] the connection to read the body from.
18
+ def initialize(connection, block_size: BLOCK_SIZE)
17
19
  @connection = connection
20
+ @block_size = block_size
18
21
  end
19
22
 
23
+ # @returns [Boolean] true if the body is empty.
20
24
  def empty?
21
25
  @connection.nil?
22
26
  end
23
27
 
28
+ # Discard the body, which will close the connection and prevent further reads.
24
29
  def discard
25
30
  if connection = @connection
26
31
  @connection = nil
@@ -30,14 +35,20 @@ module Protocol
30
35
  end
31
36
  end
32
37
 
38
+ # Close the connection.
39
+ #
40
+ # @parameter error [Exception | Nil] the error that caused the connection to be closed, if any.
33
41
  def close(error = nil)
34
42
  self.discard
35
43
 
36
44
  super
37
45
  end
38
46
 
47
+ # Read a chunk of data.
48
+ #
49
+ # @returns [String | Nil] the next chunk of data.
39
50
  def read
40
- @connection&.readpartial(BLOCK_SIZE)
51
+ @connection&.readpartial(@block_size)
41
52
  rescue EOFError
42
53
  if connection = @connection
43
54
  @connection = nil
@@ -47,6 +58,7 @@ module Protocol
47
58
  return nil
48
59
  end
49
60
 
61
+ # @returns [String] a human-readable representation of the body.
50
62
  def inspect
51
63
  "\#<#{self.class} state=#{@connection ? 'open' : 'closed'}>"
52
64
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require_relative "body/chunked"
7
+ require_relative "body/fixed"
8
+ require_relative "body/remainder"
9
+
10
+ module Protocol
11
+ module HTTP1
12
+ # A collection of classes for handling HTTP/1.1 request and response bodies.
13
+ module Body
14
+ end
15
+ end
16
+ end
@@ -11,10 +11,8 @@ require "protocol/http/headers"
11
11
 
12
12
  require_relative "reason"
13
13
  require_relative "error"
14
+ require_relative "body"
14
15
 
15
- require_relative "body/chunked"
16
- require_relative "body/fixed"
17
- require_relative "body/remainder"
18
16
  require "protocol/http/body/head"
19
17
 
20
18
  require "protocol/http/methods"
@@ -39,19 +37,32 @@ module Protocol
39
37
 
40
38
  # HTTP/1.x header parser:
41
39
  FIELD_NAME = TOKEN
42
- FIELD_VALUE = /[^\000-\037]*/.freeze
43
- HEADER = /\A(#{FIELD_NAME}):\s*(#{FIELD_VALUE})\s*\z/.freeze
40
+ WS = /[ \t]/ # Whitespace.
41
+ OWS = /#{WS}*/ # Optional whitespace.
42
+ VCHAR = /[!-~]/ # Match visible characters from ASCII 33 to 126.
43
+ FIELD_VALUE = /#{VCHAR}+(?:#{WS}+#{VCHAR}+)*/.freeze
44
+ HEADER = /\A(#{FIELD_NAME}):#{OWS}(?:(#{FIELD_VALUE})#{OWS})?\z/.freeze
44
45
 
45
46
  VALID_FIELD_NAME = /\A#{FIELD_NAME}\z/.freeze
46
47
  VALID_FIELD_VALUE = /\A#{FIELD_VALUE}\z/.freeze
47
48
 
48
49
  DEFAULT_MAXIMUM_LINE_LENGTH = 8192
49
50
 
51
+ # Represents a single HTTP/1.x connection, which may be used to send and receive multiple requests and responses.
50
52
  class Connection
51
53
  CRLF = "\r\n"
54
+
55
+ # The HTTP/1.0 version string.
52
56
  HTTP10 = "HTTP/1.0"
57
+
58
+ # The HTTP/1.1 version string.
53
59
  HTTP11 = "HTTP/1.1"
54
60
 
61
+ # Initialize the connection with the given stream.
62
+ #
63
+ # @parameter stream [IO] the stream to read and write data from.
64
+ # @parameter persistent [Boolean] whether the connection is persistent.
65
+ # @parameter state [Symbol] the initial state of the connection, typically idle.
55
66
  def initialize(stream, persistent: true, state: :idle, maximum_line_length: DEFAULT_MAXIMUM_LINE_LENGTH)
56
67
  @stream = stream
57
68
 
@@ -63,16 +74,12 @@ module Protocol
63
74
  @maximum_line_length = maximum_line_length
64
75
  end
65
76
 
77
+ # The underlying IO stream.
66
78
  attr :stream
67
79
 
68
- # Whether the connection is persistent.
69
- # This determines what connection headers are sent in the response and whether
70
- # the connection can be reused after the response is sent.
71
- # This setting is automatically managed according to the nature of the request
72
- # and response.
73
- # Changing to false is safe.
74
- # Changing to true from outside this class should generally be avoided and,
75
- # depending on the response semantics, may be reset to false anyway.
80
+ # @attribute [Boolean] true if the connection is persistent.
81
+ #
82
+ # This determines what connection headers are sent in the response and whether the connection can be reused after the response is sent. This setting is automatically managed according to the nature of the request and response. Changing to false is safe. Changing to true from outside this class should generally be avoided and, depending on the response semantics, may be reset to false anyway.
76
83
  attr_accessor :persistent
77
84
 
78
85
  # The current state of the connection.
@@ -114,29 +121,40 @@ module Protocol
114
121
  # State transition methods use a trailing "!".
115
122
  attr_accessor :state
116
123
 
124
+ # @return [Boolean] whether the connection is in the idle state.
117
125
  def idle?
118
126
  @state == :idle
119
127
  end
120
128
 
129
+ # @return [Boolean] whether the connection is in the open state.
121
130
  def open?
122
131
  @state == :open
123
132
  end
124
133
 
134
+ # @return [Boolean] whether the connection is in the half-closed local state.
125
135
  def half_closed_local?
126
136
  @state == :half_closed_local
127
137
  end
128
138
 
139
+ # @return [Boolean] whether the connection is in the half-closed remote state.
129
140
  def half_closed_remote?
130
141
  @state == :half_closed_remote
131
142
  end
132
143
 
144
+ # @return [Boolean] whether the connection is in the closed state.
133
145
  def closed?
134
146
  @state == :closed
135
147
  end
136
148
 
137
- # The number of requests processed.
149
+ # @attribute [Integer] the number of requests and responses processed by this connection.
138
150
  attr :count
139
151
 
152
+ # Indicates whether the connection is persistent given the version, method, and headers.
153
+ #
154
+ # @parameter version [String] the HTTP version.
155
+ # @parameter method [String] the HTTP method.
156
+ # @parameter headers [Hash] the HTTP headers.
157
+ # @return [Boolean] whether the connection can be persistent.
140
158
  def persistent?(version, method, headers)
141
159
  if method == HTTP::Methods::CONNECT
142
160
  return false
@@ -166,19 +184,21 @@ module Protocol
166
184
  end
167
185
  end
168
186
 
187
+ # Write the appropriate header for connection upgrade.
169
188
  def write_upgrade_header(upgrade)
170
189
  @stream.write("connection: upgrade\r\nupgrade: #{upgrade}\r\n")
171
190
  end
172
191
 
173
- # Indicates whether the connection has been hijacked meaning its
174
- # IO has been handed over and is not usable anymore.
175
- # @return [Boolean] hijack status
192
+ # Indicates whether the connection has been hijacked meaning its IO has been handed over and is not usable anymore.
193
+ #
194
+ # @returns [Boolean] hijack status
176
195
  def hijacked?
177
196
  @stream.nil?
178
197
  end
179
198
 
180
- # Effectively close the connection and return the underlying IO.
181
- # @return [IO] the underlying non-blocking IO.
199
+ # Hijack the connection - that is, take over the underlying IO and close the connection.
200
+ #
201
+ # @returns [IO | Nil] the underlying non-blocking IO.
182
202
  def hijack!
183
203
  @persistent = false
184
204
 
@@ -193,13 +213,14 @@ module Protocol
193
213
  end
194
214
  end
195
215
 
216
+ # Close the read end of the connection and transition to the half-closed remote state (or closed if already in the half-closed local state).
196
217
  def close_read
197
218
  @persistent = false
198
219
  @stream&.close_read
199
220
  self.receive_end_stream!
200
221
  end
201
222
 
202
- # Close the connection and underlying stream.
223
+ # Close the connection and underlying stream and transition to the closed state.
203
224
  def close(error = nil)
204
225
  @persistent = false
205
226
 
@@ -214,6 +235,9 @@ module Protocol
214
235
  end
215
236
  end
216
237
 
238
+ # Force a transition to the open state.
239
+ #
240
+ # @raises [ProtocolError] if the connection is not in the idle state.
217
241
  def open!
218
242
  unless @state == :idle
219
243
  raise ProtocolError, "Cannot open connection in state: #{@state}!"
@@ -224,6 +248,16 @@ module Protocol
224
248
  return self
225
249
  end
226
250
 
251
+ # Write a request to the connection. It is expected you will write the body after this method.
252
+ #
253
+ # Transitions to the open state.
254
+ #
255
+ # @parameter authority [String] the authority of the request.
256
+ # @parameter method [String] the HTTP method.
257
+ # @parameter target [String] the request target.
258
+ # @parameter version [String] the HTTP version.
259
+ # @parameter headers [Hash] the HTTP headers.
260
+ # @raises [ProtocolError] if the connection is not in the idle state.
227
261
  def write_request(authority, method, target, version, headers)
228
262
  open!
229
263
 
@@ -233,6 +267,12 @@ module Protocol
233
267
  write_headers(headers)
234
268
  end
235
269
 
270
+ # Write a response to the connection. It is expected you will write the body after this method.
271
+ #
272
+ # @parameter version [String] the HTTP version.
273
+ # @parameter status [Integer] the HTTP status code.
274
+ # @parameter headers [Hash] the HTTP headers.
275
+ # @parameter reason [String] the reason phrase, defaults to the standard reason phrase for the status code.
236
276
  def write_response(version, status, headers, reason = Reason::DESCRIPTIONS[status])
237
277
  unless @state == :open or @state == :half_closed_remote
238
278
  raise ProtocolError, "Cannot write response in state: #{@state}!"
@@ -244,6 +284,13 @@ module Protocol
244
284
  write_headers(headers)
245
285
  end
246
286
 
287
+ # Write an interim response to the connection. It is expected you will eventually write the final response after this method.
288
+ #
289
+ # @parameter version [String] the HTTP version.
290
+ # @parameter status [Integer] the HTTP status code.
291
+ # @parameter headers [Hash] the HTTP headers.
292
+ # @parameter reason [String] the reason phrase, defaults to the standard reason phrase for the status code.
293
+ # @raises [ProtocolError] if the connection is not in the open or half-closed remote state.
247
294
  def write_interim_response(version, status, headers, reason = Reason::DESCRIPTIONS[status])
248
295
  unless @state == :open or @state == :half_closed_remote
249
296
  raise ProtocolError, "Cannot write interim response in state: #{@state}!"
@@ -257,6 +304,10 @@ module Protocol
257
304
  @stream.flush
258
305
  end
259
306
 
307
+ # Write headers to the connection.
308
+ #
309
+ # @parameter headers [Hash] the headers to write.
310
+ # @raises [BadHeader] if the header name or value is invalid.
260
311
  def write_headers(headers)
261
312
  headers.each do |name, value|
262
313
  # Convert it to a string:
@@ -277,14 +328,25 @@ module Protocol
277
328
  end
278
329
  end
279
330
 
331
+ # Read some data from the connection.
332
+ #
333
+ # @parameter length [Integer] the maximum number of bytes to read.
280
334
  def readpartial(length)
281
335
  @stream.readpartial(length)
282
336
  end
283
337
 
338
+ # Read some data from the connection.
339
+ #
340
+ # @parameter length [Integer] the number of bytes to read.
284
341
  def read(length)
285
342
  @stream.read(length)
286
343
  end
287
344
 
345
+ # Read a line from the connection.
346
+ #
347
+ # @returns [String | Nil] the line read, or nil if the connection is closed.
348
+ # @raises [EOFError] if the connection is closed.
349
+ # @raises [LineLengthError] if the line is too long.
288
350
  def read_line?
289
351
  if line = @stream.gets(CRLF, @maximum_line_length)
290
352
  unless line.chomp!(CRLF)
@@ -296,10 +358,17 @@ module Protocol
296
358
  return line
297
359
  end
298
360
 
361
+ # Read a line from the connection.
362
+ #
363
+ # @raises [EOFError] if a line could not be read.
364
+ # @raises [LineLengthError] if the line is too long.
299
365
  def read_line
300
366
  read_line? or raise EOFError
301
367
  end
302
368
 
369
+ # Read a request line from the connection.
370
+ #
371
+ # @returns [Tuple(String, String, String) | Nil] the method, path, and version of the request, or nil if the connection is closed.
303
372
  def read_request_line
304
373
  return unless line = read_line?
305
374
 
@@ -312,6 +381,13 @@ module Protocol
312
381
  return method, path, version
313
382
  end
314
383
 
384
+ # Read a request from the connection, including the request line and request headers, and prepares to read the request body.
385
+ #
386
+ # Transitions to the open state.
387
+ #
388
+ # @yields {|host, method, path, version, headers, body| ...} if a block is given.
389
+ # @returns [Tuple(String, String, String, String, HTTP::Headers, Protocol::HTTP1::Body) | Nil] the host, method, path, version, headers, and body of the request, or `nil` if the connection is closed.
390
+ # @raises [ProtocolError] if the connection is not in the idle state.
315
391
  def read_request
316
392
  open!
317
393
 
@@ -341,6 +417,10 @@ module Protocol
341
417
  end
342
418
  end
343
419
 
420
+ # Read a response line from the connection.
421
+ #
422
+ # @returns [Tuple(String, Integer, String)] the version, status, and reason of the response.
423
+ # @raises [EOFError] if the connection is closed.
344
424
  def read_response_line
345
425
  version, status, reason = read_line.split(/\s+/, 3)
346
426
 
@@ -349,10 +429,21 @@ module Protocol
349
429
  return version, status, reason
350
430
  end
351
431
 
352
- private def interim_status?(status)
432
+ # Indicates whether the status code is an interim status code.
433
+ #
434
+ # @parameter status [Integer] the status code.
435
+ # @returns [Boolean] whether the status code is an interim status code.
436
+ def interim_status?(status)
353
437
  status != 101 and status >= 100 and status < 200
354
438
  end
355
439
 
440
+ # Read a response from the connection.
441
+ #
442
+ # @parameter method [String] the HTTP method.
443
+ # @yields {|version, status, reason, headers, body| ...} if a block is given.
444
+ # @returns [Tuple(String, Integer, String, HTTP::Headers, Protocol::HTTP1::Body)] the version, status, reason, headers, and body of the response.
445
+ # @raises [ProtocolError] if the connection is not in the open or half-closed local state.
446
+ # @raises [EOFError] if the connection is closed.
356
447
  def read_response(method)
357
448
  unless @state == :open or @state == :half_closed_local
358
449
  raise ProtocolError, "Cannot read response in state: #{@state}!"
@@ -383,6 +474,11 @@ module Protocol
383
474
  end
384
475
  end
385
476
 
477
+ # Read headers from the connection until an empty line is encountered.
478
+ #
479
+ # @returns [HTTP::Headers] the headers read.
480
+ # @raises [EOFError] if the connection is closed.
481
+ # @raises [BadHeader] if a header could not be parsed.
386
482
  def read_headers
387
483
  fields = []
388
484
 
@@ -391,7 +487,7 @@ module Protocol
391
487
  break if line.empty?
392
488
 
393
489
  if match = line.match(HEADER)
394
- fields << [match[1], match[2]]
490
+ fields << [match[1], match[2] || ""]
395
491
  else
396
492
  raise BadHeader, "Could not parse header: #{line.inspect}"
397
493
  end
@@ -400,6 +496,11 @@ module Protocol
400
496
  return HTTP::Headers.new(fields)
401
497
  end
402
498
 
499
+ # Transition to the half-closed local state, in other words, the connection is closed for writing.
500
+ #
501
+ # If the connection is already in the half-closed remote state, it will transition to the closed state.
502
+ #
503
+ # @raises [ProtocolError] if the connection is not in the open state.
403
504
  def send_end_stream!
404
505
  if @state == :open
405
506
  @state = :half_closed_local
@@ -410,7 +511,15 @@ module Protocol
410
511
  end
411
512
  end
412
513
 
413
- # @param protocol [String] the protocol to upgrade to.
514
+ # Write an upgrade body to the connection.
515
+ #
516
+ # This writes the upgrade header and the body to the connection. If the body is `nil`, you should coordinate writing to the stream.
517
+ #
518
+ # The connection will not be persistent after this method is called.
519
+ #
520
+ # @parameter protocol [String] the protocol to upgrade to.
521
+ # @parameter body [Object | Nil] the body to write.
522
+ # @returns [IO] the underlying IO stream.
414
523
  def write_upgrade_body(protocol, body = nil)
415
524
  # Once we upgrade the connection, it can no longer handle other requests:
416
525
  @persistent = false
@@ -434,6 +543,15 @@ module Protocol
434
543
  self.send_end_stream!
435
544
  end
436
545
 
546
+ # Write a tunnel body to the connection.
547
+ #
548
+ # This writes the connection header and the body to the connection. If the body is `nil`, you should coordinate writing to the stream.
549
+ #
550
+ # The connection will not be persistent after this method is called.
551
+ #
552
+ # @parameter version [String] the HTTP version.
553
+ # @parameter body [Object | Nil] the body to write.
554
+ # @returns [IO] the underlying IO stream.
437
555
  def write_tunnel_body(version, body = nil)
438
556
  @persistent = false
439
557
 
@@ -456,7 +574,12 @@ module Protocol
456
574
  self.send_end_stream!
457
575
  end
458
576
 
459
- def write_empty_body(body)
577
+ # Write an empty body to the connection.
578
+ #
579
+ # If given, the body will be closed.
580
+ #
581
+ # @parameter body [Object | Nil] the body to write.
582
+ def write_empty_body(body = nil)
460
583
  @stream.write("content-length: 0\r\n\r\n")
461
584
  @stream.flush
462
585
 
@@ -465,6 +588,14 @@ module Protocol
465
588
  self.send_end_stream!
466
589
  end
467
590
 
591
+ # Write a fixed length body to the connection.
592
+ #
593
+ # If the request was a `HEAD` request, the body will be closed, and no data will be written.
594
+ #
595
+ # @parameter body [Object] the body to write.
596
+ # @parameter length [Integer] the length of the body.
597
+ # @parameter head [Boolean] whether the request was a `HEAD` request.
598
+ # @raises [ContentLengthError] if the body length does not match the content length specified.
468
599
  def write_fixed_length_body(body, length, head)
469
600
  @stream.write("content-length: #{length}\r\n\r\n")
470
601
 
@@ -499,6 +630,15 @@ module Protocol
499
630
  self.send_end_stream!
500
631
  end
501
632
 
633
+ # Write a chunked body to the connection.
634
+ #
635
+ # If the request was a `HEAD` request, the body will be closed, and no data will be written.
636
+ #
637
+ # If trailers are given, they will be written after the body.
638
+ #
639
+ # @parameter body [Object] the body to write.
640
+ # @parameter head [Boolean] whether the request was a `HEAD` request.
641
+ # @parameter trailer [Hash | Nil] the trailers to write.
502
642
  def write_chunked_body(body, head, trailer = nil)
503
643
  @stream.write("transfer-encoding: chunked\r\n\r\n")
504
644
 
@@ -535,6 +675,10 @@ module Protocol
535
675
  self.send_end_stream!
536
676
  end
537
677
 
678
+ # Write the body to the connection and close the connection.
679
+ #
680
+ # @parameter body [Object] the body to write.
681
+ # @parameter head [Boolean] whether the request was a `HEAD` request.
538
682
  def write_body_and_close(body, head)
539
683
  # We can't be persistent because we don't know the data length:
540
684
  @persistent = false
@@ -559,6 +703,10 @@ module Protocol
559
703
  end
560
704
 
561
705
  # The connection (stream) was closed. It may now be in the idle state.
706
+ #
707
+ # Sub-classes may override this method to perform additional cleanup.
708
+ #
709
+ # @parameter error [Exception | Nil] the error that caused the connection to be closed, if any.
562
710
  def closed(error = nil)
563
711
  end
564
712
 
@@ -578,6 +726,14 @@ module Protocol
578
726
  self.closed(error)
579
727
  end
580
728
 
729
+ # Write a body to the connection.
730
+ #
731
+ # The behavior of this method is determined by the HTTP version, the body, and the request method. We try to choose the best approach possible, given the constraints, connection persistence, whether the length is known, etc.
732
+ #
733
+ # @parameter version [String] the HTTP version.
734
+ # @parameter body [Object] the body to write.
735
+ # @parameter head [Boolean] whether the request was a `HEAD` request.
736
+ # @parameter trailer [Hash | Nil] the trailers to write.
581
737
  def write_body(version, body, head = false, trailer = nil)
582
738
  # HTTP/1.0 cannot in any case handle trailers.
583
739
  if version == HTTP10 # or te: trailers was not present (strictly speaking not required.)
@@ -609,6 +765,11 @@ module Protocol
609
765
  end
610
766
  end
611
767
 
768
+ # Indicate that the end of the stream (body) has been received.
769
+ #
770
+ # This will transition to the half-closed remote state if the connection is open, or the closed state if the connection is half-closed local.
771
+ #
772
+ # @raises [ProtocolError] if the connection is not in the open or half-closed remote state.
612
773
  def receive_end_stream!
613
774
  if @state == :open
614
775
  @state = :half_closed_remote
@@ -619,19 +780,34 @@ module Protocol
619
780
  end
620
781
  end
621
782
 
783
+ # Read the body, assuming it is using the chunked transfer encoding.
784
+ #
785
+ # @parameters headers [Hash] the headers of the request.
786
+ # @returns [Protocol::HTTP1::Body::Chunked] the body.
622
787
  def read_chunked_body(headers)
623
788
  Body::Chunked.new(self, headers)
624
789
  end
625
790
 
791
+ # Read the body, assuming it has a fixed length.
792
+ #
793
+ # @parameters length [Integer] the length of the body.
794
+ # @returns [Protocol::HTTP1::Body::Fixed] the body.
626
795
  def read_fixed_body(length)
627
796
  Body::Fixed.new(self, length)
628
797
  end
629
798
 
799
+ # Read the body, assuming that we read until the connection is closed.
800
+ #
801
+ # @returns [Protocol::HTTP1::Body::Remainder] the body.
630
802
  def read_remainder_body
631
803
  @persistent = false
632
804
  Body::Remainder.new(self)
633
805
  end
634
806
 
807
+ # Read the body, assuming that we are not receiving any actual data, but just the length.
808
+ #
809
+ # @parameters length [Integer] the length of the body.
810
+ # @returns [Protocol::HTTP::Body::Head] the body.
635
811
  def read_head_body(length)
636
812
  # We are not receiving any body:
637
813
  self.receive_end_stream!
@@ -639,21 +815,41 @@ module Protocol
639
815
  Protocol::HTTP::Body::Head.new(length)
640
816
  end
641
817
 
818
+ # Read the body, assuming it is a tunnel.
819
+ #
820
+ # Invokes {read_remainder_body}.
821
+ #
822
+ # @returns [Protocol::HTTP::Body::Remainder] the body.
642
823
  def read_tunnel_body
643
824
  read_remainder_body
644
825
  end
645
826
 
827
+ # Read the body, assuming it is an upgrade.
828
+ #
829
+ # Invokes {read_remainder_body}.
830
+ #
831
+ # @returns [Protocol::HTTP::Body::Remainder] the body.
646
832
  def read_upgrade_body
647
833
  # When you have an incoming upgrade request body, we must be extremely careful not to start reading it until the upgrade has been confirmed, otherwise if the upgrade was rejected and we started forwarding the incoming request body, it would desynchronize the connection (potential security issue).
648
834
  # We mitigate this issue by setting @persistent to false, which will prevent the connection from being reused, even if the upgrade fails (potential performance issue).
649
835
  read_remainder_body
650
836
  end
651
837
 
838
+ # The HTTP `HEAD` method.
652
839
  HEAD = "HEAD"
840
+
841
+ # The HTTP `CONNECT` method.
653
842
  CONNECT = "CONNECT"
654
843
 
844
+ # The pattern for valid content length values.
655
845
  VALID_CONTENT_LENGTH = /\A\d+\z/
656
846
 
847
+ # Extract the content length from the headers, if possible.
848
+ #
849
+ # @parameter headers [Hash] the headers.
850
+ # @yields {|length| ...} if a content length is found.
851
+ # @parameter length [Integer] the content length.
852
+ # @raises [BadRequest] if the content length is invalid.
657
853
  def extract_content_length(headers)
658
854
  if content_length = headers.delete(CONTENT_LENGTH)
659
855
  if content_length =~ VALID_CONTENT_LENGTH
@@ -664,6 +860,17 @@ module Protocol
664
860
  end
665
861
  end
666
862
 
863
+ # Read the body of the response.
864
+ #
865
+ # - The `HEAD` method is used to retrieve the headers of the response without the body, so {read_head_body} is invoked if there is a content length, otherwise nil is returned.
866
+ # - A 101 status code indicates that the connection will be upgraded, so {read_upgrade_body} is invoked.
867
+ # - Interim status codes (1xx), no content (204) and not modified (304) status codes do not have a body, so nil is returned.
868
+ # - The `CONNECT` method is used to establish a tunnel, so {read_tunnel_body} is invoked.
869
+ # - Otherwise, the body is read according to {read_body}.
870
+ #
871
+ # @parameter method [String] the HTTP method.
872
+ # @parameter status [Integer] the HTTP status code.
873
+ # @parameter headers [Hash] the headers of the response.
667
874
  def read_response_body(method, status, headers)
668
875
  # RFC 7230 3.3.3
669
876
  # 1. Any response to a HEAD request and any response with a 1xx
@@ -704,6 +911,14 @@ module Protocol
704
911
  return read_body(headers, true)
705
912
  end
706
913
 
914
+ # Read the body of the request.
915
+ #
916
+ # - The `CONNECT` method is used to establish a tunnel, so the body is read until the connection is closed.
917
+ # - The `UPGRADE` method is used to upgrade the connection to a different protocol (typically WebSockets), so the body is read until the connection is closed.
918
+ # - Otherwise, the body is read according to {read_body}.
919
+ #
920
+ # @parameter method [String] the HTTP method.
921
+ # @parameter headers [Hash] the headers of the request.
707
922
  def read_request_body(method, headers)
708
923
  # 2. Any 2xx (Successful) response to a CONNECT request implies that
709
924
  # the connection will become a tunnel immediately after the empty
@@ -724,6 +939,15 @@ module Protocol
724
939
  return read_body(headers)
725
940
  end
726
941
 
942
+ # Read the body of the message.
943
+ #
944
+ # - The `transfer-encoding` header is used to determine if the body is chunked.
945
+ # - Otherwise, if the `content-length` is present, the body is read until the content length is reached.
946
+ # - Otherwise, if `remainder` is true, the body is read until the connection is closed.
947
+ #
948
+ # @parameter headers [Hash] the headers of the message.
949
+ # @parameter remainder [Boolean] whether to read the remainder of the body.
950
+ # @returns [Object] the body.
727
951
  def read_body(headers, remainder = false)
728
952
  # 3. If a Transfer-Encoding header field is present and the chunked
729
953
  # transfer coding (Section 4.1) is the final encoding, the message
@@ -1,12 +1,13 @@
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
 
8
8
  module Protocol
9
9
  module HTTP1
10
+ # The base class for all HTTP/1.x errors.
10
11
  class Error < HTTP::Error
11
12
  end
12
13
 
@@ -14,6 +15,7 @@ module Protocol
14
15
  class ProtocolError < Error
15
16
  end
16
17
 
18
+ # The request line was too long.
17
19
  class LineLengthError < Error
18
20
  end
19
21
 
@@ -1,13 +1,15 @@
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
 
8
8
  module Protocol
9
9
  module HTTP1
10
+ # Reason phrases for HTTP status codes.
10
11
  module Reason
12
+ # Get the reason phrase for the given status code.
11
13
  DESCRIPTIONS = {
12
14
  100 => "Continue",
13
15
  101 => "Switching Protocols",
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Protocol
7
7
  module HTTP1
8
- VERSION = "0.31.0"
8
+ VERSION = "0.32.0"
9
9
  end
10
10
  end
@@ -1,7 +1,14 @@
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 "http1/version"
7
7
  require_relative "http1/connection"
8
+
9
+ # @namespace
10
+ module Protocol
11
+ # @namespace
12
+ module HTTP1
13
+ end
14
+ end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protocol-http1
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.31.0
4
+ version: 0.32.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -41,7 +41,7 @@ cert_chain:
41
41
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
42
42
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
43
43
  -----END CERTIFICATE-----
44
- date: 2025-03-13 00:00:00.000000000 Z
44
+ date: 2025-03-20 00:00:00.000000000 Z
45
45
  dependencies:
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: protocol-http
@@ -62,6 +62,7 @@ extensions: []
62
62
  extra_rdoc_files: []
63
63
  files:
64
64
  - lib/protocol/http1.rb
65
+ - lib/protocol/http1/body.rb
65
66
  - lib/protocol/http1/body/chunked.rb
66
67
  - lib/protocol/http1/body/fixed.rb
67
68
  - lib/protocol/http1/body/remainder.rb
metadata.gz.sig CHANGED
Binary file