protocol-http 0.56.1 → 0.57.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: 9874b094d1320bc400ff81a991311fa74a9b95d0a89adc9c4a4c41b89380bb2a
4
- data.tar.gz: d2f9206d98a99557152c4758f266ef70d516072e2cfb9e0b6099dc4901bbbe95
3
+ metadata.gz: 46aa99ddc598b23eb7d9670a806ca18d21314fc7e4bb4e42305a4ac4bd92c868
4
+ data.tar.gz: 0dd66a51205e73c5d5bd57fca401ce592f31a21fb09d1bc9126fb084b293d378
5
5
  SHA512:
6
- metadata.gz: ec515579222f78ee8ee4353c346cf8d361bb244deefc4bf0deb5899fe28399c96e273fe8c1fca10e328b9e5872e5e2b40ea09fcf6c4463da704e097a58e795bb
7
- data.tar.gz: d2b739b64da0af2db050ddccfe292e464ed1defc7310e0152ddeeff58b4455b725483dd76aa237f21ff44d0ae3b1ff798269f81ed2c69a7ba6a1f865c1bc3820
6
+ metadata.gz: 979dec4d8fedfce87917074576493ff6365e897bd6403f409d0ff862ed1517cb4cbfbc65b0f1c95ef83f77ceb20530e4a90131e0530c737fa7532886f6d431d5
7
+ data.tar.gz: a52fbe05985d5d6d87d37c46b72cc71ff79b8d4023d765c79efaa5e947681dd53a1d10a083ff1e54ad47bd9358ddb7a8e8ede7313c7db36c4c7d88cfad52d3a8
checksums.yaml.gz.sig CHANGED
@@ -1,3 +1,2 @@
1
- eh����(߄�Rț²lN! �?KHeAe
2
- P��9��KV�hˊ�F�U��wWa��#ʞK�Upoj#n?O���.�Kk_)*�/�7FYI` �����k�?\�5�~dT��4ƥn3P5rG~��˯��4n�:T��-�"W�LW�V��[/�4�'R\Cم�O��gO��b�;����{��"�1��;�lj��(�o�<O��G���g� =�pD��2���[mj�W$♿JJT����q����c�۷xc���߯���8�-WHZ�h����~
3
- ��"F�i;5+���k�(�: $����|(H^�9
1
+ E��:?[T3-ӿ���5�@5.�ͣ:��Z�5X�0�@S�]g秱L�����W��(�8���^4�-S�Jml����7������L˸����⦓.���0`+x�j�jPCq;O�4�vx����_U�ǔ!�r�����Q"��՗!p
2
+ J��%pw���3��{Y��� ������G_�!O��svN��]��8�m(���e^�eʘ����H/���<u����Kߧh��4dk)� �[fS��y���0�bC�gf�_��6�k �n�?vV��y7F1��qfp���1�y�� �-kz��
@@ -205,5 +205,5 @@ interim_response_callback = proc do |status, headers|
205
205
  end
206
206
  end
207
207
 
208
- response = client.post("/upload", {'expect' => '100-continue'}, body, interim_response: interim_response_callback)
208
+ response = client.post("/upload", {"expect" => "100-continue"}, body, interim_response: interim_response_callback)
209
209
  ```
@@ -34,7 +34,7 @@ This gem does not provide any specific client or server implementation, rather i
34
34
  {ruby Protocol::HTTP::Request} represents an HTTP request which can be used both server and client-side.
35
35
 
36
36
  ``` ruby
37
- require 'protocol/http/request'
37
+ require "protocol/http/request"
38
38
 
39
39
  # Short form (recommended):
40
40
  request = Protocol::HTTP::Request["GET", "/index.html", {"accept" => "text/html"}]
@@ -54,7 +54,7 @@ request.headers # => Protocol::HTTP::Headers instance
54
54
  {ruby Protocol::HTTP::Response} represents an HTTP response which can be used both server and client-side.
55
55
 
56
56
  ``` ruby
57
- require 'protocol/http/response'
57
+ require "protocol/http/response"
58
58
 
59
59
  # Short form (recommended):
60
60
  response = Protocol::HTTP::Response[200, {"content-type" => "text/html"}, "Hello, World!"]
@@ -83,15 +83,15 @@ response.failure? # => false (400-599)
83
83
  #### Basic Usage
84
84
 
85
85
  ``` ruby
86
- require 'protocol/http/headers'
86
+ require "protocol/http/headers"
87
87
 
88
88
  headers = Protocol::HTTP::Headers.new
89
89
 
90
90
  # Assignment by title-case key:
91
- headers['Content-Type'] = "image/jpeg"
91
+ headers["Content-Type"] = "image/jpeg"
92
92
 
93
93
  # Lookup by lower-case (normalized) key:
94
- headers['content-type']
94
+ headers["content-type"]
95
95
  # => "image/jpeg"
96
96
  ```
97
97
 
@@ -101,8 +101,8 @@ Many headers receive special semantic processing, automatically splitting comma-
101
101
 
102
102
  ``` ruby
103
103
  # Accept header with quality values:
104
- headers['Accept'] = 'text/html, application/json;q=0.8, */*;q=0.1'
105
- accept = headers['accept']
104
+ headers["Accept"] = "text/html, application/json;q=0.8, */*;q=0.1"
105
+ accept = headers["accept"]
106
106
  # => ["text/html", "application/json;q=0.8", "*/*;q=0.1"]
107
107
 
108
108
  # Access parsed media ranges with quality factors:
@@ -114,17 +114,17 @@ end
114
114
  # */* (q=0.1)
115
115
 
116
116
  # Accept-Encoding automatically splits values:
117
- headers['Accept-Encoding'] = 'gzip, deflate, br;q=0.9'
118
- headers['accept-encoding']
117
+ headers["Accept-Encoding"] = "gzip, deflate, br;q=0.9"
118
+ headers["accept-encoding"]
119
119
  # => ["gzip", "deflate", "br;q=0.9"]
120
120
 
121
121
  # Cache-Control splits directives:
122
- headers['Cache-Control'] = 'max-age=3600, no-cache, must-revalidate'
123
- headers['cache-control']
122
+ headers["Cache-Control"] = "max-age=3600, no-cache, must-revalidate"
123
+ headers["cache-control"]
124
124
  # => ["max-age=3600", "no-cache", "must-revalidate"]
125
125
 
126
126
  # Vary header normalizes field names to lowercase:
127
- headers['Vary'] = 'Accept-Encoding, User-Agent'
128
- headers['vary']
127
+ headers["Vary"] = "Accept-Encoding, User-Agent"
128
+ headers["vary"]
129
129
  # => ["accept-encoding", "user-agent"]
130
130
  ```
data/context/headers.md CHANGED
@@ -15,17 +15,17 @@ This guide explains how to work with HTTP headers using `protocol-http`.
15
15
  The {Protocol::HTTP::Headers} class provides a comprehensive interface for creating and manipulating HTTP headers:
16
16
 
17
17
  ```ruby
18
- require 'protocol/http'
18
+ require "protocol/http"
19
19
 
20
20
  headers = Protocol::HTTP::Headers.new
21
- headers.add('content-type', 'text/html')
22
- headers.add('set-cookie', 'session=abc123')
21
+ headers.add("content-type", "text/html")
22
+ headers.add("set-cookie", "session=abc123")
23
23
 
24
24
  # Access headers
25
- content_type = headers['content-type'] # => "text/html"
25
+ content_type = headers["content-type"] # => "text/html"
26
26
 
27
27
  # Check if header exists
28
- headers.include?('content-type') # => true
28
+ headers.include?("content-type") # => true
29
29
  ```
30
30
 
31
31
  ### Header Policies
@@ -34,11 +34,11 @@ Different header types have different behaviors for merging, validation, and tra
34
34
 
35
35
  ```ruby
36
36
  # Some headers can be specified multiple times
37
- headers.add('set-cookie', 'first=value1')
38
- headers.add('set-cookie', 'second=value2')
37
+ headers.add("set-cookie", "first=value1")
38
+ headers.add("set-cookie", "second=value2")
39
39
 
40
40
  # Others are singletons and will raise errors if duplicated
41
- headers.add('content-length', '100')
41
+ headers.add("content-length", "100")
42
42
  # headers.add('content-length', '200') # Would raise DuplicateHeaderError
43
43
  ```
44
44
 
@@ -48,11 +48,11 @@ Some headers have specialized classes for parsing and formatting:
48
48
 
49
49
  ```ruby
50
50
  # Accept header with media ranges
51
- accept = Protocol::HTTP::Header::Accept.new('text/html,application/json;q=0.9')
51
+ accept = Protocol::HTTP::Header::Accept.new("text/html,application/json;q=0.9")
52
52
  media_ranges = accept.media_ranges
53
53
 
54
54
  # Authorization header
55
- auth = Protocol::HTTP::Header::Authorization.basic('username', 'password')
55
+ auth = Protocol::HTTP::Header::Authorization.basic("username", "password")
56
56
  # => "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
57
57
  ```
58
58
 
@@ -63,20 +63,20 @@ HTTP trailers are headers that appear after the message body. For security reaso
63
63
  ```ruby
64
64
  # Working with trailers
65
65
  headers = Protocol::HTTP::Headers.new([
66
- ['content-type', 'text/html'],
67
- ['content-length', '1000']
66
+ ["content-type", "text/html"],
67
+ ["content-length", "1000"]
68
68
  ])
69
69
 
70
70
  # Start trailer section
71
71
  headers.trailer!
72
72
 
73
73
  # These will be allowed (safe metadata)
74
- headers.add('etag', '"12345"')
75
- headers.add('date', Time.now.httpdate)
74
+ headers.add("etag", '"12345"')
75
+ headers.add("date", Time.now.httpdate)
76
76
 
77
77
  # These will be silently ignored for security
78
- headers.add('authorization', 'Bearer token') # Ignored - credential leakage risk
79
- headers.add('connection', 'close') # Ignored - hop-by-hop header
78
+ headers.add("authorization", "Bearer token") # Ignored - credential leakage risk
79
+ headers.add("connection", "close") # Ignored - hop-by-hop header
80
80
  ```
81
81
 
82
82
  The trailer security system prevents HTTP request smuggling by restricting which headers can appear in trailers:
@@ -9,10 +9,10 @@ This guide explains how to use `Protocol::HTTP::Reference` for constructing and
9
9
  ## Basic Construction
10
10
 
11
11
  ``` ruby
12
- require 'protocol/http/reference'
12
+ require "protocol/http/reference"
13
13
 
14
14
  # Simple reference with parameters:
15
- reference = Protocol::HTTP::Reference.new("/search", nil, nil, {q: 'kittens', limit: 10})
15
+ reference = Protocol::HTTP::Reference.new("/search", nil, nil, {q: "kittens", limit: 10})
16
16
  reference.to_s
17
17
  # => "/search?q=kittens&limit=10"
18
18
 
@@ -97,7 +97,7 @@ body.empty? # => true
97
97
  Use {ruby Protocol::HTTP::Body::File} for serving files efficiently:
98
98
 
99
99
  ``` ruby
100
- require 'protocol/http/body/file'
100
+ require "protocol/http/body/file"
101
101
 
102
102
  # Open a file:
103
103
  body = Protocol::HTTP::Body::File.open("/path/to/file.txt")
@@ -140,7 +140,7 @@ body = Protocol::HTTP::Body::File.new(file, block_size: 8192) # 8KB chunks
140
140
  Use {ruby Protocol::HTTP::Body::Writable} for dynamic content generation:
141
141
 
142
142
  ``` ruby
143
- require 'protocol/http/body/writable'
143
+ require "protocol/http/body/writable"
144
144
 
145
145
  # Create a writable body:
146
146
  body = Protocol::HTTP::Body::Writable.new
@@ -181,7 +181,7 @@ body.write("chunk 1")
181
181
  Use {ruby Protocol::HTTP::Body::Streamable} for computed content:
182
182
 
183
183
  ``` ruby
184
- require 'protocol/http/body/streamable'
184
+ require "protocol/http/body/streamable"
185
185
 
186
186
  # Generate content dynamically:
187
187
  body = Protocol::HTTP::Body::Streamable.new do |output|
@@ -202,7 +202,7 @@ end
202
202
  Use {ruby Protocol::HTTP::Body::Stream} to wrap IO-like objects:
203
203
 
204
204
  ``` ruby
205
- require 'protocol/http/body/stream'
205
+ require "protocol/http/body/stream"
206
206
 
207
207
  # Wrap an IO object:
208
208
  io = StringIO.new("Hello\nWorld\nFrom\nStream")
@@ -224,8 +224,8 @@ rest = body.read # => "Stream"
224
224
  ### Compression Bodies
225
225
 
226
226
  ``` ruby
227
- require 'protocol/http/body/deflate'
228
- require 'protocol/http/body/inflate'
227
+ require "protocol/http/body/deflate"
228
+ require "protocol/http/body/inflate"
229
229
 
230
230
  # Compress a body:
231
231
  original = Protocol::HTTP::Body::Buffered.new(["Hello World"])
@@ -241,7 +241,7 @@ content = decompressed.join # => "Hello World"
241
241
  Create custom body transformations:
242
242
 
243
243
  ``` ruby
244
- require 'protocol/http/body/wrapper'
244
+ require "protocol/http/body/wrapper"
245
245
 
246
246
  class UppercaseBody < Protocol::HTTP::Body::Wrapper
247
247
  def read
@@ -299,7 +299,7 @@ end
299
299
  Make any body rewindable by buffering:
300
300
 
301
301
  ``` ruby
302
- require 'protocol/http/body/rewindable'
302
+ require "protocol/http/body/rewindable"
303
303
 
304
304
  # Wrap a non-rewindable body:
305
305
  file_body = Protocol::HTTP::Body::File.open("data.txt")
@@ -318,7 +318,7 @@ same_chunk = rewindable.read # Same as first_chunk
318
318
  For HEAD requests that need content-length but no body:
319
319
 
320
320
  ``` ruby
321
- require 'protocol/http/body/head'
321
+ require "protocol/http/body/head"
322
322
 
323
323
  # Create head body from another body:
324
324
  original = Protocol::HTTP::Body::File.open("large_file.zip")
@@ -38,7 +38,7 @@ end
38
38
  The `Protocol::HTTP::Middleware` class provides a convenient base for building middleware:
39
39
 
40
40
  ``` ruby
41
- require 'protocol/http/middleware'
41
+ require "protocol/http/middleware"
42
42
 
43
43
  class LoggingMiddleware < Protocol::HTTP::Middleware
44
44
  def call(request)
@@ -60,7 +60,7 @@ app = LoggingMiddleware.new(Protocol::HTTP::Middleware::HelloWorld)
60
60
  Use `Protocol::HTTP::Middleware.build` to construct middleware stacks:
61
61
 
62
62
  ``` ruby
63
- require 'protocol/http/middleware'
63
+ require "protocol/http/middleware"
64
64
 
65
65
  app = Protocol::HTTP::Middleware.build do
66
66
  use LoggingMiddleware
@@ -84,7 +84,7 @@ Convert a block into middleware using `Middleware.for`:
84
84
 
85
85
  ``` ruby
86
86
  middleware = Protocol::HTTP::Middleware.for do |request|
87
- if request.path == '/health'
87
+ if request.path == "/health"
88
88
  Protocol::HTTP::Response[200, {}, ["OK"]]
89
89
  else
90
90
  # This would normally delegate, but this example doesn't have a delegate
@@ -141,7 +141,7 @@ class AuthenticationMiddleware < Protocol::HTTP::Middleware
141
141
  end
142
142
 
143
143
  def call(request)
144
- auth_header = request.headers['authorization']
144
+ auth_header = request.headers["authorization"]
145
145
 
146
146
  unless auth_header == "Bearer #{@api_key}"
147
147
  return Protocol::HTTP::Response[401, {}, ["Unauthorized"]]
@@ -166,8 +166,8 @@ class ContentTypeMiddleware < Protocol::HTTP::Middleware
166
166
  response = super
167
167
 
168
168
  # Add content-type header if not present
169
- unless response.headers.include?('content-type')
170
- response.headers['content-type'] = 'text/plain'
169
+ unless response.headers.include?("content-type")
170
+ response.headers["content-type"] = "text/plain"
171
171
  end
172
172
 
173
173
  response
@@ -189,7 +189,7 @@ describe MyMiddleware do
189
189
  end
190
190
 
191
191
  it "closes properly" do
192
- expect { app.close }.not.to raise_exception
192
+ expect{app.close}.not.to raise_exception
193
193
  end
194
194
  end
195
195
  ```
data/context/streaming.md CHANGED
@@ -9,15 +9,15 @@ The request and response body work independently of each other can stream data i
9
9
  ```ruby
10
10
  #!/usr/bin/env ruby
11
11
 
12
- require 'async'
13
- require 'async/http/client'
14
- require 'async/http/server'
15
- require 'async/http/endpoint'
12
+ require "async"
13
+ require "async/http/client"
14
+ require "async/http/server"
15
+ require "async/http/endpoint"
16
16
 
17
- require 'protocol/http/body/stream'
18
- require 'protocol/http/body/writable'
17
+ require "protocol/http/body/stream"
18
+ require "protocol/http/body/writable"
19
19
 
20
- endpoint = Async::HTTP::Endpoint.parse('http://localhost:3000')
20
+ endpoint = Async::HTTP::Endpoint.parse("http://localhost:3000")
21
21
 
22
22
  Async do
23
23
  server = Async::HTTP::Server.for(endpoint) do |request|
@@ -38,7 +38,7 @@ Async do
38
38
  Protocol::HTTP::Response[200, {}, output]
39
39
  end
40
40
 
41
- server_task = Async{server.run}
41
+ server_task = Async {server.run}
42
42
 
43
43
  client = Async::HTTP::Client.new(endpoint)
44
44
 
@@ -74,15 +74,15 @@ While WebSockets can work on the above streaming interface, it's a bit more conv
74
74
  ```ruby
75
75
  #!/usr/bin/env ruby
76
76
 
77
- require 'async'
78
- require 'async/http/client'
79
- require 'async/http/server'
80
- require 'async/http/endpoint'
77
+ require "async"
78
+ require "async/http/client"
79
+ require "async/http/server"
80
+ require "async/http/endpoint"
81
81
 
82
- require 'protocol/http/body/stream'
83
- require 'protocol/http/body/writable'
82
+ require "protocol/http/body/stream"
83
+ require "protocol/http/body/writable"
84
84
 
85
- endpoint = Async::HTTP::Endpoint.parse('http://localhost:3000')
85
+ endpoint = Async::HTTP::Endpoint.parse("http://localhost:3000")
86
86
 
87
87
  Async do
88
88
  server = Async::HTTP::Server.for(endpoint) do |request|
@@ -104,7 +104,7 @@ Async do
104
104
  Protocol::HTTP::Response[200, {}, output]
105
105
  end
106
106
 
107
- server_task = Async{server.run}
107
+ server_task = Async {server.run}
108
108
 
109
109
  client = Async::HTTP::Client.new(endpoint)
110
110
 
@@ -11,7 +11,7 @@ While basic query parameter encoding follows the `application/x-www-form-urlenco
11
11
  ## Basic Query Parameter Parsing
12
12
 
13
13
  ``` ruby
14
- require 'protocol/http/url'
14
+ require "protocol/http/url"
15
15
 
16
16
  # Parse query parameters from a URL:
17
17
  reference = Protocol::HTTP::Reference.parse("/search?q=ruby&category=programming&page=2")
@@ -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 "middleware"
7
7
 
@@ -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},
@@ -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
  # Copyright, 2020, by Bryan Powell.
6
6
  # Copyright, 2025, by William T. Nelson.
7
7
 
@@ -90,7 +90,7 @@ module Protocol
90
90
 
91
91
  # The length of the body. Will compute and cache the length of the body, if it was not provided.
92
92
  def length
93
- @length ||= @chunks.inject(0) {|sum, chunk| sum + chunk.bytesize}
93
+ @length ||= @chunks.inject(0){|sum, chunk| sum + chunk.bytesize}
94
94
  end
95
95
 
96
96
  # @returns [Boolean] if the body is empty.
@@ -26,5 +26,18 @@ module Protocol
26
26
  # @attribute [String] key The header key that was duplicated.
27
27
  attr :key
28
28
  end
29
+
30
+ # Raised when an invalid trailer header is encountered in headers.
31
+ class InvalidTrailerError < Error
32
+ include BadRequest
33
+
34
+ # @parameter key [String] The trailer key that is invalid.
35
+ def initialize(key)
36
+ super("Invalid trailer key: #{key.inspect}")
37
+ end
38
+
39
+ # @attribute [String] key The trailer key that is invalid.
40
+ attr :key
41
+ end
29
42
  end
30
43
  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 "multiple"
7
7
  require_relative "../cookie"
@@ -24,6 +24,11 @@ module Protocol
24
24
  cookies.map{|cookie| [cookie.name, cookie]}.to_h
25
25
  end
26
26
 
27
+ # Serializes the `cookie` header by joining individual cookie strings with semicolons.
28
+ def to_s
29
+ join(";")
30
+ end
31
+
27
32
  # Whether this header is acceptable in HTTP trailers.
28
33
  # Cookie headers should not appear in trailers as they contain state information needed early in processing.
29
34
  # @returns [Boolean] `false`, as cookie headers are needed during initial request processing.
@@ -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-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "split"
7
7
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2025, by Samuel Williams.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "split"
7
7
  require_relative "../quoted_string"
@@ -105,32 +105,32 @@ module Protocol
105
105
 
106
106
  # @returns [Boolean] whether the `chunked` encoding is accepted.
107
107
  def chunked?
108
- self.any? {|value| value.start_with?(CHUNKED)}
108
+ self.any?{|value| value.start_with?(CHUNKED)}
109
109
  end
110
110
 
111
111
  # @returns [Boolean] whether the `gzip` encoding is accepted.
112
112
  def gzip?
113
- self.any? {|value| value.start_with?(GZIP)}
113
+ self.any?{|value| value.start_with?(GZIP)}
114
114
  end
115
115
 
116
116
  # @returns [Boolean] whether the `deflate` encoding is accepted.
117
117
  def deflate?
118
- self.any? {|value| value.start_with?(DEFLATE)}
118
+ self.any?{|value| value.start_with?(DEFLATE)}
119
119
  end
120
120
 
121
121
  # @returns [Boolean] whether the `compress` encoding is accepted.
122
122
  def compress?
123
- self.any? {|value| value.start_with?(COMPRESS)}
123
+ self.any?{|value| value.start_with?(COMPRESS)}
124
124
  end
125
125
 
126
126
  # @returns [Boolean] whether the `identity` encoding is accepted.
127
127
  def identity?
128
- self.any? {|value| value.start_with?(IDENTITY)}
128
+ self.any?{|value| value.start_with?(IDENTITY)}
129
129
  end
130
130
 
131
131
  # @returns [Boolean] whether trailers are accepted.
132
132
  def trailers?
133
- self.any? {|value| value.start_with?(TRAILERS)}
133
+ self.any?{|value| value.start_with?(TRAILERS)}
134
134
  end
135
135
 
136
136
  # Whether this header is acceptable in HTTP trailers.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2025, by Samuel Williams.
4
+ # Copyright, 2018-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "error"
7
7
 
@@ -191,7 +191,7 @@ module Protocol
191
191
  # @parameter key [String] The header key.
192
192
  # @parameter value [String] The raw header value.
193
193
  def each(&block)
194
- @fields.each(&block)
194
+ self.to_h.each(&block)
195
195
  end
196
196
 
197
197
  # @returns [Boolean] Whether the headers include the specified key.
@@ -279,7 +279,7 @@ module Protocol
279
279
  # @parameter key [String] The header key.
280
280
  # @returns [String | Array | Object] The header value.
281
281
  def [] key
282
- to_h[key]
282
+ self.to_h[key]
283
283
  end
284
284
 
285
285
  # Merge the headers into this instance.
@@ -403,26 +403,23 @@ module Protocol
403
403
  # @parameter hash [Hash] The hash to merge into.
404
404
  # @parameter key [String] The header key.
405
405
  # @parameter value [String] The raw header value.
406
+ # @parameter trailer [Boolean] Whether this header is in the trailer section.
406
407
  protected def merge_into(hash, key, value, trailer = @tail)
407
408
  if policy = @policy[key]
408
409
  # Check if we're adding to trailers and this header is allowed:
409
410
  if trailer && !policy.trailer?
410
- return false
411
+ raise InvalidTrailerError, key
411
412
  end
412
413
 
413
414
  if current_value = hash[key]
414
415
  current_value << value
415
416
  else
416
- if policy.respond_to?(:parse)
417
- hash[key] = policy.parse(value)
418
- else
419
- hash[key] = policy.new(value)
420
- end
417
+ hash[key] = policy.parse(value)
421
418
  end
422
419
  else
423
420
  # By default, headers are not allowed in trailers:
424
421
  if trailer
425
- return false
422
+ raise InvalidTrailerError, key
426
423
  end
427
424
 
428
425
  if hash.key?(key)
@@ -435,6 +432,8 @@ module Protocol
435
432
 
436
433
  # Compute a hash table of headers, where the keys are normalized to lower case and the values are normalized according to the policy for that header.
437
434
  #
435
+ # This will enforce policy rules, such as merging multiple headers into arrays, or raising errors for duplicate headers.
436
+ #
438
437
  # @returns [Hash] A hash table of `{key, value}` pairs.
439
438
  def to_h
440
439
  unless @indexed
@@ -465,7 +464,7 @@ module Protocol
465
464
  def == other
466
465
  case other
467
466
  when Hash
468
- to_h == other
467
+ self.to_h == other
469
468
  when Headers
470
469
  @fields == other.fields
471
470
  else
@@ -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
 
6
6
  require_relative "../middleware"
7
7
 
@@ -25,7 +25,7 @@ module Protocol
25
25
  # @parameter options [Hash] The options to pass to the middleware constructor.
26
26
  # @parameter block [Proc] The block to pass to the middleware constructor.
27
27
  def use(middleware, *arguments, **options, &block)
28
- @use << proc {|app| middleware.new(app, *arguments, **options, &block)}
28
+ @use << proc{|app| middleware.new(app, *arguments, **options, &block)}
29
29
  end
30
30
 
31
31
  # Specify the (default) middleware application to use.
@@ -39,7 +39,7 @@ module Protocol
39
39
  #
40
40
  # @returns [Middleware] The application.
41
41
  def to_app
42
- @use.reverse.inject(@app) {|app, use| use.call(app)}
42
+ @use.reverse.inject(@app){|app, use| use.call(app)}
43
43
  end
44
44
  end
45
45
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2025, by Samuel Williams.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
5
 
6
6
  module Protocol
7
7
  module HTTP
@@ -44,4 +44,4 @@ module Protocol
44
44
  end
45
45
  end
46
46
  end
47
- end
47
+ end
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Protocol
7
7
  module HTTP
8
- VERSION = "0.56.1"
8
+ VERSION = "0.57.0"
9
9
  end
10
10
  end
data/license.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
 
3
- Copyright, 2018-2025, by Samuel Williams.
3
+ Copyright, 2018-2026, by Samuel Williams.
4
4
  Copyright, 2019, by Yuta Iwama.
5
5
  Copyright, 2020, by Olle Jonsson.
6
6
  Copyright, 2020, by Bryan Powell.
data/readme.md CHANGED
@@ -30,6 +30,12 @@ Please see the [project documentation](https://socketry.github.io/protocol-http/
30
30
 
31
31
  Please see the [project releases](https://socketry.github.io/protocol-http/releases/index) for all releases.
32
32
 
33
+ ### v0.57.0
34
+
35
+ - Always use `#parse` when parsing header values from strings to ensure proper normalization and validation.
36
+ - Introduce `Protocol::HTTP::InvalidTrailerError` which is raised when a trailer header is not allowed by the current policy.
37
+ - **Breaking**: `Headers#each` now yields parsed values according to the current policy. For the previous behaviour, use `Headers#fields`.
38
+
33
39
  ### v0.56.0
34
40
 
35
41
  - Introduce `Header::*.parse(value)` which parses a raw header value string into a header instance.
@@ -81,11 +87,6 @@ Please see the [project releases](https://socketry.github.io/protocol-http/relea
81
87
 
82
88
  - Add support for `priority:` header.
83
89
 
84
- ### v0.33.0
85
-
86
- - Clarify behaviour of streaming bodies and copy `Protocol::Rack::Body::Streaming` to `Protocol::HTTP::Body::Streamable`.
87
- - Copy `Async::HTTP::Body::Writable` to `Protocol::HTTP::Body::Writable`.
88
-
89
90
  ## See Also
90
91
 
91
92
  - [protocol-http1](https://github.com/socketry/protocol-http1) — HTTP/1 client/server implementation using this
data/releases.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Releases
2
2
 
3
+ ## v0.57.0
4
+
5
+ - Always use `#parse` when parsing header values from strings to ensure proper normalization and validation.
6
+ - Introduce `Protocol::HTTP::InvalidTrailerError` which is raised when a trailer header is not allowed by the current policy.
7
+ - **Breaking**: `Headers#each` now yields parsed values according to the current policy. For the previous behaviour, use `Headers#fields`.
8
+
3
9
  ## v0.56.0
4
10
 
5
11
  - Introduce `Header::*.parse(value)` which parses a raw header value string into a header instance.
@@ -57,13 +63,13 @@ module GRPCMessage
57
63
  end
58
64
 
59
65
  GRPC_POLICY = Protocol::HTTP::Headers::POLICY.dup
60
- GRPC_POLICY['grpc-status'] = GRPCStatus
61
- GRPC_POLICY['grpc-message'] = GRPCMessage
66
+ GRPC_POLICY["grpc-status"] = GRPCStatus
67
+ GRPC_POLICY["grpc-message"] = GRPCMessage
62
68
 
63
69
  # Reinterpret the headers using the new policy:
64
70
  response.headers.policy = GRPC_POLICY
65
- response.headers['grpc-status'] # => 0
66
- response.headers['grpc-message'] # => "OK"
71
+ response.headers["grpc-status"] # => 0
72
+ response.headers["grpc-message"] # => "OK"
67
73
  ```
68
74
 
69
75
  ## v0.53.0
@@ -117,6 +123,7 @@ client.get("/", headers: {"accept" => "text/html"}, authority: "example.com")
117
123
  # Response keyword arguments:
118
124
  def call(request)
119
125
  return Response[200, headers: {"content-Type" => "text/html"}, body: "Hello, World!"]
126
+ end
120
127
  ```
121
128
 
122
129
  ### Interim Response Handling
@@ -127,7 +134,12 @@ On the client side, you can pass a callback using the `interim_response` keyword
127
134
 
128
135
  ``` ruby
129
136
  client = ...
130
- response = client.get("/index", interim_response: proc{|status, headers| ...})
137
+
138
+ interim_response = proc do |status, headers|
139
+ puts "Received interim response: #{status} -> #{headers.inspect}"
140
+ end
141
+
142
+ response = client.get("/index", interim_response: interim_response)
131
143
  ```
132
144
 
133
145
  On the server side, you can send an interim response using the `#send_interim_response` method:
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.56.1
4
+ version: 0.57.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -134,7 +134,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
134
134
  - !ruby/object:Gem::Version
135
135
  version: '0'
136
136
  requirements: []
137
- rubygems_version: 3.6.9
137
+ rubygems_version: 4.0.3
138
138
  specification_version: 4
139
139
  summary: Provides abstractions to handle HTTP protocols.
140
140
  test_files: []
metadata.gz.sig CHANGED
Binary file