protocol-http 0.51.1 → 0.53.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.
@@ -0,0 +1,130 @@
1
+ # URL Parsing
2
+
3
+ This guide explains how to use `Protocol::HTTP::URL` for parsing and manipulating URL components, particularly query strings and parameters.
4
+
5
+ ## Overview
6
+
7
+ {ruby Protocol::HTTP::URL} provides utilities for parsing and manipulating URL components, particularly query strings and parameters. It offers robust encoding/decoding capabilities for complex parameter structures.
8
+
9
+ While basic query parameter encoding follows the `application/x-www-form-urlencoded` standard, there is no universal standard for serializing complex nested structures (arrays, nested objects) in URLs. Different frameworks use varying conventions for these cases, and this implementation follows common patterns where possible.
10
+
11
+ ## Basic Query Parameter Parsing
12
+
13
+ ``` ruby
14
+ require 'protocol/http/url'
15
+
16
+ # Parse query parameters from a URL:
17
+ reference = Protocol::HTTP::Reference.parse("/search?q=ruby&category=programming&page=2")
18
+ parameters = Protocol::HTTP::URL.decode(reference.query)
19
+ # => {"q" => "ruby", "category" => "programming", "page" => "2"}
20
+
21
+ # Symbolize keys for easier access:
22
+ parameters = Protocol::HTTP::URL.decode(reference.query, symbolize_keys: true)
23
+ # => {:q => "ruby", :category => "programming", :page => "2"}
24
+ ```
25
+
26
+ ## Complex Parameter Structures
27
+
28
+ The URL module handles nested parameters, arrays, and complex data structures:
29
+
30
+ ``` ruby
31
+ # Array parameters:
32
+ query = "tags[]=ruby&tags[]=programming&tags[]=web"
33
+ parameters = Protocol::HTTP::URL.decode(query)
34
+ # => {"tags" => ["ruby", "programming", "web"]}
35
+
36
+ # Nested hash parameters:
37
+ query = "user[name]=John&user[email]=john@example.com&user[preferences][theme]=dark"
38
+ parameters = Protocol::HTTP::URL.decode(query)
39
+ # => {"user" => {"name" => "John", "email" => "john@example.com", "preferences" => {"theme" => "dark"}}}
40
+
41
+ # Mixed structures:
42
+ query = "filters[categories][]=books&filters[categories][]=movies&filters[price][min]=10&filters[price][max]=100"
43
+ parameters = Protocol::HTTP::URL.decode(query)
44
+ # => {"filters" => {"categories" => ["books", "movies"], "price" => {"min" => "10", "max" => "100"}}}
45
+ ```
46
+
47
+ ## Encoding Parameters to Query Strings
48
+
49
+ ``` ruby
50
+ # Simple parameters:
51
+ parameters = {"search" => "protocol-http", "limit" => "20"}
52
+ query = Protocol::HTTP::URL.encode(parameters)
53
+ # => "search=protocol-http&limit=20"
54
+
55
+ # Array parameters:
56
+ parameters = {"tags" => ["ruby", "http", "protocol"]}
57
+ query = Protocol::HTTP::URL.encode(parameters)
58
+ # => "tags[]=ruby&tags[]=http&tags[]=protocol"
59
+
60
+ # Nested parameters:
61
+ parameters = {
62
+ user: {
63
+ profile: {
64
+ name: "Alice",
65
+ settings: {
66
+ notifications: true,
67
+ theme: "light"
68
+ }
69
+ }
70
+ }
71
+ }
72
+ query = Protocol::HTTP::URL.encode(parameters)
73
+ # => "user[profile][name]=Alice&user[profile][settings][notifications]=true&user[profile][settings][theme]=light"
74
+ ```
75
+
76
+ ## URL Escaping and Unescaping
77
+
78
+ ``` ruby
79
+ # Escape special characters:
80
+ Protocol::HTTP::URL.escape("hello world!")
81
+ # => "hello%20world%21"
82
+
83
+ # Escape path components (preserves path separators):
84
+ Protocol::HTTP::URL.escape_path("/path/with spaces/file.html")
85
+ # => "/path/with%20spaces/file.html"
86
+
87
+ # Unescape percent-encoded strings:
88
+ Protocol::HTTP::URL.unescape("hello%20world%21")
89
+ # => "hello world!"
90
+
91
+ # Handle Unicode characters:
92
+ Protocol::HTTP::URL.escape("café")
93
+ # => "caf%C3%A9"
94
+
95
+ Protocol::HTTP::URL.unescape("caf%C3%A9")
96
+ # => "café"
97
+ ```
98
+
99
+ ## Scanning and Processing Query Strings
100
+
101
+ For custom processing, you can scan query strings directly:
102
+
103
+ ``` ruby
104
+ query = "name=John&age=30&active=true"
105
+
106
+ Protocol::HTTP::URL.scan(query) do |key, value|
107
+ puts "#{key}: #{value}"
108
+ end
109
+ # Output:
110
+ # name: John
111
+ # age: 30
112
+ # active: true
113
+ ```
114
+
115
+ ## Security and Limits
116
+
117
+ The URL module includes built-in protection against deeply nested parameter attacks:
118
+
119
+ ``` ruby
120
+ # This will raise an error to prevent excessive nesting:
121
+ begin
122
+ Protocol::HTTP::URL.decode("a[b][c][d][e][f][g][h][i]=value")
123
+ rescue ArgumentError => error
124
+ puts error.message
125
+ # => "Key length exceeded limit!"
126
+ end
127
+
128
+ # You can adjust the maximum nesting level:
129
+ Protocol::HTTP::URL.decode("a[b][c]=value", 5) # Allow up to 5 levels of nesting
130
+ ```
@@ -21,7 +21,7 @@ module Protocol
21
21
  # The default wrappers to use for decoding content.
22
22
  DEFAULT_WRAPPERS = {
23
23
  "gzip" => Body::Inflate.method(:for),
24
- "identity" => ->(body) { body }, # Identity means no encoding
24
+ "identity" => ->(body) {body}, # Identity means no encoding
25
25
 
26
26
  # There is no point including this:
27
27
  # 'identity' => ->(body){body},
@@ -153,8 +153,10 @@ module Protocol
153
153
  #
154
154
  # @returns [String] a string representation of the buffered body.
155
155
  def inspect
156
- if @chunks
157
- "\#<#{self.class} #{@chunks.size} chunks, #{self.length} bytes>"
156
+ if @chunks and @chunks.size > 0
157
+ "#<#{self.class} #{@index}/#{@chunks.size} chunks, #{self.length} bytes>"
158
+ else
159
+ "#<#{self.class} empty>"
158
160
  end
159
161
  end
160
162
  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 "wrapper"
7
7
 
@@ -53,6 +53,23 @@ module Protocol
53
53
 
54
54
  super
55
55
  end
56
+
57
+ # Convert the body to a hash suitable for serialization.
58
+ #
59
+ # @returns [Hash] The body as a hash.
60
+ def as_json(...)
61
+ super.merge(
62
+ callback: @callback&.to_s
63
+ )
64
+ end
65
+
66
+ # Inspect the completable body.
67
+ #
68
+ # @returns [String] a string representation of the completable body.
69
+ def inspect
70
+ callback_status = @callback ? "callback pending" : "callback completed"
71
+ return "#{super} | #<#{self.class} #{callback_status}>"
72
+ end
56
73
  end
57
74
  end
58
75
  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 "wrapper"
7
7
 
@@ -75,11 +75,22 @@ module Protocol
75
75
  end
76
76
  end
77
77
 
78
+ # Convert the body to a hash suitable for serialization.
79
+ #
80
+ # @returns [Hash] The body as a hash.
81
+ def as_json(...)
82
+ super.merge(
83
+ input_length: @input_length,
84
+ output_length: @output_length,
85
+ compression_ratio: (ratio * 100).round(2)
86
+ )
87
+ end
88
+
78
89
  # Inspect the body, including the compression ratio.
79
90
  #
80
91
  # @returns [String] a string representation of the body.
81
92
  def inspect
82
- "#{super} | \#<#{self.class} #{(ratio*100).round(2)}%>"
93
+ "#{super} | #<#{self.class} #{(ratio*100).round(2)}%>"
83
94
  end
84
95
  end
85
96
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2024, by Samuel Williams.
4
+ # Copyright, 2020-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "wrapper"
7
7
 
@@ -34,7 +34,7 @@ module Protocol
34
34
  @digest = digest
35
35
  @callback = callback
36
36
  end
37
-
37
+
38
38
  # @attribute [Digest] digest the digest object.
39
39
  attr :digest
40
40
 
@@ -64,6 +64,16 @@ module Protocol
64
64
  return nil
65
65
  end
66
66
  end
67
+
68
+ # Convert the body to a hash suitable for serialization.
69
+ #
70
+ # @returns [Hash] The body as a hash.
71
+ def as_json(...)
72
+ super.merge(
73
+ digest_class: @digest.class.name,
74
+ callback: @callback&.to_s
75
+ )
76
+ end
67
77
  end
68
78
  end
69
79
  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 "readable"
7
7
 
@@ -135,7 +135,11 @@ module Protocol
135
135
  #
136
136
  # @returns [String] a string representation of the file body.
137
137
  def inspect
138
- "\#<#{self.class} file=#{@file.inspect} offset=#{@offset} remaining=#{@remaining}>"
138
+ if @offset > 0
139
+ "#<#{self.class} #{@file.inspect} +#{@offset}, #{@remaining} bytes remaining>"
140
+ else
141
+ "#<#{self.class} #{@file.inspect}, #{@remaining} bytes remaining>"
142
+ end
139
143
  end
140
144
  end
141
145
  end
@@ -53,6 +53,13 @@ module Protocol
53
53
  def length
54
54
  @length
55
55
  end
56
+
57
+ # Inspect the head body.
58
+ #
59
+ # @returns [String] a string representation of the head body.
60
+ def inspect
61
+ "#<#{self.class} #{@length} bytes (empty)>"
62
+ end
56
63
  end
57
64
  end
58
65
  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 "zlib"
7
7
 
@@ -34,7 +34,7 @@ module Protocol
34
34
 
35
35
  break unless chunk&.empty?
36
36
  end
37
-
37
+
38
38
  if chunk
39
39
  @output_length += chunk.bytesize
40
40
  elsif !stream.closed?
@@ -82,11 +82,21 @@ module Protocol
82
82
  true
83
83
  end
84
84
 
85
+ # Convert the body to a hash suitable for serialization.
86
+ #
87
+ # @returns [Hash] The body as a hash.
88
+ def as_json(...)
89
+ super.merge(
90
+ index: @index,
91
+ chunks: @chunks.size
92
+ )
93
+ end
94
+
85
95
  # Inspect the rewindable body.
86
96
  #
87
97
  # @returns [String] a string representation of the body.
88
98
  def inspect
89
- "\#<#{self.class} #{@index}/#{@chunks.size} chunks read>"
99
+ "#{super} | #<#{self.class} #{@index}/#{@chunks.size} chunks read>"
90
100
  end
91
101
  end
92
102
  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
  # Copyright, 2023, by Genki Takiuchi.
6
6
 
7
7
  require_relative "buffered"
@@ -123,7 +123,7 @@ module Protocol
123
123
  if buffer.bytesize > length
124
124
  # This ensures the subsequent `slice!` works correctly.
125
125
  buffer.force_encoding(Encoding::BINARY)
126
-
126
+
127
127
  @buffer = buffer.byteslice(length, buffer.bytesize)
128
128
  buffer.slice!(length, buffer.bytesize)
129
129
  end
@@ -386,6 +386,21 @@ module Protocol
386
386
  @closed
387
387
  end
388
388
 
389
+ # Inspect the stream.
390
+ #
391
+ # @returns [String] a string representation of the stream.
392
+ def inspect
393
+ buffer_info = @buffer ? "#{@buffer.bytesize} bytes buffered" : "no buffer"
394
+
395
+ status = []
396
+ status << "closed" if @closed
397
+ status << "read-closed" if @closed_read
398
+
399
+ status_info = status.empty? ? "open" : status.join(", ")
400
+
401
+ return "#<#{self.class} #{buffer_info}, #{status_info}>"
402
+ end
403
+
389
404
  # @returns [Boolean] Whether there are any output chunks remaining.
390
405
  def empty?
391
406
  @output.empty?
@@ -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 "readable"
7
7
  require_relative "writable"
@@ -147,7 +147,23 @@ module Protocol
147
147
  #
148
148
  # @parameter error [Exception | Nil] The error that caused this stream to be closed, if any.
149
149
  def close_output(error = nil)
150
- @output&.close(error)
150
+ if output = @output
151
+ @output = nil
152
+ output.close(error)
153
+ end
154
+ end
155
+
156
+ # Inspect the streaming body.
157
+ #
158
+ # @returns [String] a string representation of the streaming body.
159
+ def inspect
160
+ if @block
161
+ "#<#{self.class} block available, not consumed>"
162
+ elsif @output
163
+ "#<#{self.class} block consumed, output active>"
164
+ else
165
+ "#<#{self.class} block consumed, output closed>"
166
+ end
151
167
  end
152
168
  end
153
169
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024, by Samuel Williams.
4
+ # Copyright, 2024-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "readable"
7
7
 
@@ -166,9 +166,9 @@ module Protocol
166
166
  # @returns [String] A string representation of the body.
167
167
  def inspect
168
168
  if @error
169
- "\#<#{self.class} #{@count} chunks written, #{status}, error=#{@error}>"
169
+ "#<#{self.class} #{@count} chunks written, #{status}, error=#{@error}>"
170
170
  else
171
- "\#<#{self.class} #{@count} chunks written, #{status}>"
171
+ "#<#{self.class} #{@count} chunks written, #{status}>"
172
172
  end
173
173
  end
174
174
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2023, by Samuel Williams.
4
+ # Copyright, 2018-2025, by Samuel Williams.
5
5
 
6
6
  module Protocol
7
7
  module HTTP
@@ -120,7 +120,7 @@ module Protocol
120
120
  # @parameter value_name [String] the directive name to search for (e.g., "max-age").
121
121
  # @returns [Integer | Nil] the parsed integer value, or `nil` if not found or invalid.
122
122
  def find_integer_value(value_name)
123
- if value = self.find { |value| value.start_with?(value_name) }
123
+ if value = self.find{|value| value.start_with?(value_name)}
124
124
  _, age = value.split("=", 2)
125
125
 
126
126
  if age =~ /\A[0-9]+\z/
@@ -64,13 +64,15 @@ module Protocol
64
64
  # Initialize the headers with the specified fields.
65
65
  #
66
66
  # @parameter fields [Array] An array of `[key, value]` pairs.
67
- # @parameter indexed [Hash] A hash table of normalized headers, if available.
68
- def initialize(fields = [], indexed = nil)
67
+ # @parameter tail [Integer | Nil] The index of the trailer start in the @fields array.
68
+ def initialize(fields = [], tail = nil, indexed: nil)
69
69
  @fields = fields
70
- @indexed = indexed
71
70
 
72
- # Marks where trailer start in the @fields array.
73
- @tail = nil
71
+ # Marks where trailer start in the @fields array:
72
+ @tail = tail
73
+
74
+ # The cached index of headers:
75
+ @indexed = nil
74
76
  end
75
77
 
76
78
  # Initialize a copy of the headers.
@@ -86,8 +88,8 @@ module Protocol
86
88
  # Clear all headers.
87
89
  def clear
88
90
  @fields.clear
89
- @indexed = nil
90
91
  @tail = nil
92
+ @indexed = nil
91
93
  end
92
94
 
93
95
  # Flatten trailer into the headers, in-place.
@@ -108,6 +110,14 @@ module Protocol
108
110
  # @attribute [Array] An array of `[key, value]` pairs.
109
111
  attr :fields
110
112
 
113
+ # @attribute [Integer | Nil] The index where trailers begin.
114
+ attr :tail
115
+
116
+ # @returns [Array] The fields of the headers.
117
+ def to_a
118
+ @fields
119
+ end
120
+
111
121
  # @returns [Boolean] Whether there are any trailers.
112
122
  def trailer?
113
123
  @tail != nil
@@ -142,24 +142,38 @@ module Protocol
142
142
  end
143
143
 
144
144
  # Update the reference with the given path, parameters and fragment.
145
- # @argument path [String] Append the string to this reference similar to `File.join`.
146
- # @argument parameters [Hash] Append the parameters to this reference.
147
- # @argument fragment [String] Set the fragment to this value.
148
- # @argument pop [Boolean] If the path contains a trailing filename, pop the last component of the path before appending the new path.
149
- # @argument merge [Boolean] If the parameters are specified, merge them with the existing parameters.
150
- def with(path: nil, parameters: nil, fragment: @fragment, pop: false, merge: true)
151
- if @parameters
152
- if parameters and merge
153
- parameters = @parameters.merge(parameters)
154
- else
145
+ #
146
+ # @parameter path [String] Append the string to this reference similar to `File.join`.
147
+ # @parameter parameters [Hash] Append the parameters to this reference.
148
+ # @parameter fragment [String] Set the fragment to this value.
149
+ # @parameter pop [Boolean] If the path contains a trailing filename, pop the last component of the path before appending the new path.
150
+ # @parameter merge [Boolean] If the parameters are specified, merge them with the existing parameters, otherwise replace them (including query string).
151
+ def with(path: nil, parameters: false, fragment: @fragment, pop: false, merge: true)
152
+ if merge
153
+ # Merge mode: combine new parameters with existing, keep query:
154
+ # parameters = (@parameters || {}).merge(parameters || {})
155
+ if @parameters
156
+ if parameters
157
+ parameters = @parameters.merge(parameters)
158
+ else
159
+ parameters = @parameters
160
+ end
161
+ elsif !parameters
155
162
  parameters = @parameters
156
163
  end
157
- end
158
-
159
- if @query and !merge
160
- query = nil
161
- else
164
+
162
165
  query = @query
166
+ else
167
+ # Replace mode: use new parameters if provided, clear query when replacing:
168
+ if parameters == false
169
+ # No new parameters provided, keep existing:
170
+ parameters = @parameters
171
+ query = @query
172
+ else
173
+ # New parameters provided, replace and clear query:
174
+ # parameters = parameters
175
+ query = nil
176
+ end
163
177
  end
164
178
 
165
179
  if path
@@ -68,7 +68,7 @@ module Protocol
68
68
 
69
69
  # @attribute [Body::Readable] the request body. It should only be read once (it may not be idempotent).
70
70
  attr_accessor :body
71
-
71
+
72
72
  # @attribute [String | Array(String) | Nil] the request protocol, usually empty, but occasionally `"websocket"` or `"webtransport"`. In HTTP/1, it is used to request a connection upgrade, and in HTTP/2 it is used to indicate a specfic protocol for the stream.
73
73
  attr_accessor :protocol
74
74
 
@@ -1,10 +1,11 @@
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 "body/buffered"
7
7
  require_relative "body/reader"
8
+ require_relative "headers"
8
9
 
9
10
  module Protocol
10
11
  module HTTP
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Protocol
7
7
  module HTTP
8
- VERSION = "0.51.1"
8
+ VERSION = "0.53.0"
9
9
  end
10
10
  end
data/readme.md CHANGED
@@ -14,16 +14,35 @@ Provides abstractions for working with the HTTP protocol.
14
14
 
15
15
  Please see the [project documentation](https://socketry.github.io/protocol-http/) for more details.
16
16
 
17
- - [Streaming](https://socketry.github.io/protocol-http/guides/streaming/index) - This guide gives an overview of how to implement streaming requests and responses.
18
-
19
17
  - [Getting Started](https://socketry.github.io/protocol-http/guides/getting-started/index) - This guide explains how to use `protocol-http` for building abstract HTTP interfaces.
20
18
 
19
+ - [Message Body](https://socketry.github.io/protocol-http/guides/message-body/index) - This guide explains how to work with HTTP request and response message bodies using `Protocol::HTTP::Body` classes.
20
+
21
+ - [Middleware](https://socketry.github.io/protocol-http/guides/middleware/index) - This guide explains how to build and use HTTP middleware with `Protocol::HTTP::Middleware`.
22
+
23
+ - [Hypertext References](https://socketry.github.io/protocol-http/guides/hypertext-references/index) - This guide explains how to use `Protocol::HTTP::Reference` for constructing and manipulating hypertext references (URLs with parameters).
24
+
25
+ - [URL Parsing](https://socketry.github.io/protocol-http/guides/url-parsing/index) - This guide explains how to use `Protocol::HTTP::URL` for parsing and manipulating URL components, particularly query strings and parameters.
26
+
27
+ - [Streaming](https://socketry.github.io/protocol-http/guides/streaming/index) - This guide gives an overview of how to implement streaming requests and responses.
28
+
21
29
  - [Design Overview](https://socketry.github.io/protocol-http/guides/design-overview/index) - This guide explains the high level design of `protocol-http` in the context of wider design patterns that can be used to implement HTTP clients and servers.
22
30
 
23
31
  ## Releases
24
32
 
25
33
  Please see the [project releases](https://socketry.github.io/protocol-http/releases/index) for all releases.
26
34
 
35
+ ### v0.53.0
36
+
37
+ - Improve consistency of Body `#inspect`.
38
+ - Improve `as_json` support for Body wrappers.
39
+
40
+ ### v0.52.0
41
+
42
+ - Add `Protocol::HTTP::Headers#to_a` method that returns the fields array, providing compatibility with standard Ruby array conversion pattern.
43
+ - Expose `tail` in `Headers.new` so that trailers can be accurately reproduced.
44
+ - Add agent context.
45
+
27
46
  ### v0.51.0
28
47
 
29
48
  - `Protocol::HTTP::Headers` now raise a `DuplicateHeaderError` when a duplicate singleton header (e.g. `content-length`) is added.
data/releases.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Releases
2
2
 
3
+ ## v0.53.0
4
+
5
+ - Improve consistency of Body `#inspect`.
6
+ - Improve `as_json` support for Body wrappers.
7
+
8
+ ## v0.52.0
9
+
10
+ - Add `Protocol::HTTP::Headers#to_a` method that returns the fields array, providing compatibility with standard Ruby array conversion pattern.
11
+ - Expose `tail` in `Headers.new` so that trailers can be accurately reproduced.
12
+ - Add agent context.
13
+
3
14
  ## v0.51.0
4
15
 
5
16
  - `Protocol::HTTP::Headers` now raise a `DuplicateHeaderError` when a duplicate singleton header (e.g. `content-length`) is added.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protocol-http
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.51.1
4
+ version: 0.53.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -53,6 +53,14 @@ executables: []
53
53
  extensions: []
54
54
  extra_rdoc_files: []
55
55
  files:
56
+ - context/design-overview.md
57
+ - context/getting-started.md
58
+ - context/hypertext-references.md
59
+ - context/index.yaml
60
+ - context/message-body.md
61
+ - context/middleware.md
62
+ - context/streaming.md
63
+ - context/url-parsing.md
56
64
  - lib/protocol/http.rb
57
65
  - lib/protocol/http/accept_encoding.rb
58
66
  - lib/protocol/http/body.rb