protocol-http 0.56.1 → 0.58.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/design-overview.md +1 -1
- data/context/getting-started.md +13 -13
- data/context/headers.md +16 -16
- data/context/hypertext-references.md +2 -2
- data/context/message-body.md +9 -9
- data/context/middleware.md +7 -7
- data/context/streaming.md +14 -14
- data/context/url-parsing.md +1 -1
- data/lib/protocol/http/accept_encoding.rb +2 -2
- data/lib/protocol/http/body/buffered.rb +2 -2
- data/lib/protocol/http/error.rb +13 -0
- data/lib/protocol/http/header/cookie.rb +6 -1
- data/lib/protocol/http/header/generic.rb +30 -0
- data/lib/protocol/http/header/priority.rb +1 -1
- data/lib/protocol/http/header/te.rb +7 -7
- data/lib/protocol/http/headers.rb +57 -27
- data/lib/protocol/http/middleware/builder.rb +3 -3
- data/lib/protocol/http/quoted_string.rb +2 -2
- data/lib/protocol/http/version.rb +1 -1
- data/license.md +1 -1
- data/readme.md +12 -9
- data/releases.md +23 -5
- data.tar.gz.sig +0 -0
- metadata +3 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 971bece5e113d3f945faf8b8ce44858e97f0e6282e3a264b6e6fca1e57c8e71c
|
|
4
|
+
data.tar.gz: 8d1139a2022dbe96710d7e0286a5c30c2384e14fcba3e0907428361b25c3a703
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 69d17f997389daf2332cacc0d5ac407897e2dc2a45c3e329f23dfc9b18f7075b425e10c43fff27ff1728f64e92a6843e3291be5103e9ee8c72d1f810c2603c01
|
|
7
|
+
data.tar.gz: '09676f19f6b3bd4d6290b5dccd8f93d2c6409e82926fa74a9b21f39f4fa92df0d289ef8fac6b0460a9de2784d78d7114d262f9a5088f66518ef60b9d60a877f4'
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/context/design-overview.md
CHANGED
|
@@ -205,5 +205,5 @@ interim_response_callback = proc do |status, headers|
|
|
|
205
205
|
end
|
|
206
206
|
end
|
|
207
207
|
|
|
208
|
-
response = client.post("/upload", {
|
|
208
|
+
response = client.post("/upload", {"expect" => "100-continue"}, body, interim_response: interim_response_callback)
|
|
209
209
|
```
|
data/context/getting-started.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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[
|
|
91
|
+
headers["Content-Type"] = "image/jpeg"
|
|
92
92
|
|
|
93
93
|
# Lookup by lower-case (normalized) key:
|
|
94
|
-
headers[
|
|
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[
|
|
105
|
-
accept = headers[
|
|
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[
|
|
118
|
-
headers[
|
|
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[
|
|
123
|
-
headers[
|
|
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[
|
|
128
|
-
headers[
|
|
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
|
|
18
|
+
require "protocol/http"
|
|
19
19
|
|
|
20
20
|
headers = Protocol::HTTP::Headers.new
|
|
21
|
-
headers.add(
|
|
22
|
-
headers.add(
|
|
21
|
+
headers.add("content-type", "text/html")
|
|
22
|
+
headers.add("set-cookie", "session=abc123")
|
|
23
23
|
|
|
24
24
|
# Access headers
|
|
25
|
-
content_type = headers[
|
|
25
|
+
content_type = headers["content-type"] # => "text/html"
|
|
26
26
|
|
|
27
27
|
# Check if header exists
|
|
28
|
-
headers.include?(
|
|
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(
|
|
38
|
-
headers.add(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
67
|
-
|
|
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(
|
|
75
|
-
headers.add(
|
|
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(
|
|
79
|
-
headers.add(
|
|
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
|
|
12
|
+
require "protocol/http/reference"
|
|
13
13
|
|
|
14
14
|
# Simple reference with parameters:
|
|
15
|
-
reference = Protocol::HTTP::Reference.new("/search", nil, nil, {q:
|
|
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
|
|
data/context/message-body.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
228
|
-
require
|
|
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
|
|
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
|
|
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
|
|
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")
|
data/context/middleware.md
CHANGED
|
@@ -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
|
|
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
|
|
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 ==
|
|
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[
|
|
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?(
|
|
170
|
-
response.headers[
|
|
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
|
|
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
|
|
13
|
-
require
|
|
14
|
-
require
|
|
15
|
-
require
|
|
12
|
+
require "async"
|
|
13
|
+
require "async/http/client"
|
|
14
|
+
require "async/http/server"
|
|
15
|
+
require "async/http/endpoint"
|
|
16
16
|
|
|
17
|
-
require
|
|
18
|
-
require
|
|
17
|
+
require "protocol/http/body/stream"
|
|
18
|
+
require "protocol/http/body/writable"
|
|
19
19
|
|
|
20
|
-
endpoint = Async::HTTP::Endpoint.parse(
|
|
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|
|
|
@@ -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
|
|
78
|
-
require
|
|
79
|
-
require
|
|
80
|
-
require
|
|
77
|
+
require "async"
|
|
78
|
+
require "async/http/client"
|
|
79
|
+
require "async/http/server"
|
|
80
|
+
require "async/http/endpoint"
|
|
81
81
|
|
|
82
|
-
require
|
|
83
|
-
require
|
|
82
|
+
require "protocol/http/body/stream"
|
|
83
|
+
require "protocol/http/body/writable"
|
|
84
84
|
|
|
85
|
-
endpoint = Async::HTTP::Endpoint.parse(
|
|
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|
|
data/context/url-parsing.md
CHANGED
|
@@ -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
|
|
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-
|
|
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)
|
|
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-
|
|
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)
|
|
93
|
+
@length ||= @chunks.inject(0){|sum, chunk| sum + chunk.bytesize}
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
# @returns [Boolean] if the body is empty.
|
data/lib/protocol/http/error.rb
CHANGED
|
@@ -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-
|
|
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.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "split"
|
|
7
|
+
|
|
8
|
+
module Protocol
|
|
9
|
+
module HTTP
|
|
10
|
+
module Header
|
|
11
|
+
# Represents generic or custom headers that can be used in trailers.
|
|
12
|
+
#
|
|
13
|
+
# This class is used as the default policy for headers not explicitly defined in the POLICY hash.
|
|
14
|
+
#
|
|
15
|
+
# It allows generic headers to be used in HTTP trailers, which is important for:
|
|
16
|
+
# - Custom application headers.
|
|
17
|
+
# - gRPC status headers (grpc-status, grpc-message).
|
|
18
|
+
# - Headers used by proxies and middleware.
|
|
19
|
+
# - Future HTTP extensions.
|
|
20
|
+
class Generic < Split
|
|
21
|
+
# Whether this header is acceptable in HTTP trailers.
|
|
22
|
+
# Generic headers are allowed in trailers by default to support extensibility.
|
|
23
|
+
# @returns [Boolean] `true`, generic headers are allowed in trailers.
|
|
24
|
+
def self.trailer?
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -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?
|
|
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?
|
|
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?
|
|
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?
|
|
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?
|
|
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?
|
|
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-
|
|
4
|
+
# Copyright, 2018-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "error"
|
|
7
7
|
|
|
@@ -20,6 +20,7 @@ require_relative "header/priority"
|
|
|
20
20
|
require_relative "header/trailer"
|
|
21
21
|
require_relative "header/server_timing"
|
|
22
22
|
require_relative "header/digest"
|
|
23
|
+
require_relative "header/generic"
|
|
23
24
|
|
|
24
25
|
require_relative "header/accept"
|
|
25
26
|
require_relative "header/accept_charset"
|
|
@@ -158,7 +159,26 @@ module Protocol
|
|
|
158
159
|
return trailer(&block)
|
|
159
160
|
end
|
|
160
161
|
|
|
162
|
+
# Enumerate all the headers in the header, if there are any.
|
|
163
|
+
#
|
|
164
|
+
# @yields {|key, value| ...} The header key and value.
|
|
165
|
+
# @parameter key [String] The header key.
|
|
166
|
+
# @parameter value [String] The raw header value.
|
|
167
|
+
def header(&block)
|
|
168
|
+
return to_enum(:header) unless block_given?
|
|
169
|
+
|
|
170
|
+
if @tail and @tail < @fields.size
|
|
171
|
+
@fields.first(@tail).each(&block)
|
|
172
|
+
else
|
|
173
|
+
@fields.each(&block)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
161
177
|
# Enumerate all headers in the trailer, if there are any.
|
|
178
|
+
#
|
|
179
|
+
# @yields {|key, value| ...} The header key and value.
|
|
180
|
+
# @parameter key [String] The header key.
|
|
181
|
+
# @parameter value [String] The raw header value.
|
|
162
182
|
def trailer(&block)
|
|
163
183
|
return to_enum(:trailer) unless block_given?
|
|
164
184
|
|
|
@@ -227,9 +247,18 @@ module Protocol
|
|
|
227
247
|
#
|
|
228
248
|
# @parameter key [String] the header key.
|
|
229
249
|
# @parameter value [String] the header value to assign.
|
|
230
|
-
|
|
250
|
+
# @parameter trailer [Boolean] whether this header is being added as a trailer.
|
|
251
|
+
def add(key, value, trailer: self.trailer?)
|
|
231
252
|
value = value.to_s
|
|
232
253
|
|
|
254
|
+
if trailer
|
|
255
|
+
policy = @policy[key.downcase]
|
|
256
|
+
|
|
257
|
+
if !policy or !policy.trailer?
|
|
258
|
+
raise InvalidTrailerError, key
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
233
262
|
if @indexed
|
|
234
263
|
merge_into(@indexed, key.downcase, value)
|
|
235
264
|
end
|
|
@@ -279,7 +308,7 @@ module Protocol
|
|
|
279
308
|
# @parameter key [String] The header key.
|
|
280
309
|
# @returns [String | Array | Object] The header value.
|
|
281
310
|
def [] key
|
|
282
|
-
to_h[key]
|
|
311
|
+
self.to_h[key]
|
|
283
312
|
end
|
|
284
313
|
|
|
285
314
|
# Merge the headers into this instance.
|
|
@@ -297,6 +326,10 @@ module Protocol
|
|
|
297
326
|
end
|
|
298
327
|
|
|
299
328
|
# The policy for various headers, including how they are merged and normalized.
|
|
329
|
+
#
|
|
330
|
+
# A policy may be `false` to indicate that the header may only be specified once and is a simple string.
|
|
331
|
+
#
|
|
332
|
+
# Otherwise, the policy is a class which implements the header normalization logic, including `parse` and `coerce` class methods.
|
|
300
333
|
POLICY = {
|
|
301
334
|
# Headers which may only be specified once:
|
|
302
335
|
"content-disposition" => false,
|
|
@@ -315,8 +348,11 @@ module Protocol
|
|
|
315
348
|
"user-agent" => false,
|
|
316
349
|
"trailer" => Header::Trailer,
|
|
317
350
|
|
|
318
|
-
#
|
|
351
|
+
# Connection handling:
|
|
319
352
|
"connection" => Header::Connection,
|
|
353
|
+
"upgrade" => Header::Split,
|
|
354
|
+
|
|
355
|
+
# Cache handling:
|
|
320
356
|
"cache-control" => Header::CacheControl,
|
|
321
357
|
"te" => Header::TE,
|
|
322
358
|
"vary" => Header::Vary,
|
|
@@ -354,16 +390,21 @@ module Protocol
|
|
|
354
390
|
|
|
355
391
|
# Accept headers:
|
|
356
392
|
"accept" => Header::Accept,
|
|
393
|
+
"accept-ranges" => Header::Split,
|
|
357
394
|
"accept-charset" => Header::AcceptCharset,
|
|
358
395
|
"accept-encoding" => Header::AcceptEncoding,
|
|
359
396
|
"accept-language" => Header::AcceptLanguage,
|
|
360
397
|
|
|
398
|
+
# Content negotiation headers:
|
|
399
|
+
"content-encoding" => Header::Split,
|
|
400
|
+
"content-range" => false,
|
|
401
|
+
|
|
361
402
|
# Performance headers:
|
|
362
403
|
"server-timing" => Header::ServerTiming,
|
|
363
404
|
|
|
364
405
|
# Content integrity headers:
|
|
365
406
|
"digest" => Header::Digest,
|
|
366
|
-
}.tap{|hash| hash.default =
|
|
407
|
+
}.tap{|hash| hash.default = Header::Generic}
|
|
367
408
|
|
|
368
409
|
# Delete all header values for the given key, and return the merged value.
|
|
369
410
|
#
|
|
@@ -403,28 +444,14 @@ module Protocol
|
|
|
403
444
|
# @parameter hash [Hash] The hash to merge into.
|
|
404
445
|
# @parameter key [String] The header key.
|
|
405
446
|
# @parameter value [String] The raw header value.
|
|
406
|
-
protected def merge_into(hash, key, value
|
|
447
|
+
protected def merge_into(hash, key, value)
|
|
407
448
|
if policy = @policy[key]
|
|
408
|
-
# Check if we're adding to trailers and this header is allowed:
|
|
409
|
-
if trailer && !policy.trailer?
|
|
410
|
-
return false
|
|
411
|
-
end
|
|
412
|
-
|
|
413
449
|
if current_value = hash[key]
|
|
414
450
|
current_value << value
|
|
415
451
|
else
|
|
416
|
-
|
|
417
|
-
hash[key] = policy.parse(value)
|
|
418
|
-
else
|
|
419
|
-
hash[key] = policy.new(value)
|
|
420
|
-
end
|
|
452
|
+
hash[key] = policy.parse(value)
|
|
421
453
|
end
|
|
422
454
|
else
|
|
423
|
-
# By default, headers are not allowed in trailers:
|
|
424
|
-
if trailer
|
|
425
|
-
return false
|
|
426
|
-
end
|
|
427
|
-
|
|
428
455
|
if hash.key?(key)
|
|
429
456
|
raise DuplicateHeaderError, key
|
|
430
457
|
end
|
|
@@ -435,16 +462,19 @@ module Protocol
|
|
|
435
462
|
|
|
436
463
|
# 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
464
|
#
|
|
465
|
+
# This will enforce policy rules, such as merging multiple headers into arrays, or raising errors for duplicate headers.
|
|
466
|
+
#
|
|
438
467
|
# @returns [Hash] A hash table of `{key, value}` pairs.
|
|
439
468
|
def to_h
|
|
440
469
|
unless @indexed
|
|
441
|
-
|
|
470
|
+
indexed = {}
|
|
442
471
|
|
|
443
|
-
@fields.
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
merge_into(@indexed, key.downcase, value, trailer)
|
|
472
|
+
@fields.each do |key, value|
|
|
473
|
+
merge_into(indexed, key.downcase, value)
|
|
447
474
|
end
|
|
475
|
+
|
|
476
|
+
# Deferred assignment so that exceptions in `merge_into` don't leave us in an inconsistent state:
|
|
477
|
+
@indexed = indexed
|
|
448
478
|
end
|
|
449
479
|
|
|
450
480
|
return @indexed
|
|
@@ -465,7 +495,7 @@ module Protocol
|
|
|
465
495
|
def == other
|
|
466
496
|
case other
|
|
467
497
|
when Hash
|
|
468
|
-
to_h == other
|
|
498
|
+
self.to_h == other
|
|
469
499
|
when Headers
|
|
470
500
|
@fields == other.fields
|
|
471
501
|
else
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2019-
|
|
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
|
|
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)
|
|
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
|
data/license.md
CHANGED
data/readme.md
CHANGED
|
@@ -30,6 +30,18 @@ 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.58.0
|
|
34
|
+
|
|
35
|
+
- Move trailer validation to `Headers#add` method to ensure all additions are checked at the time of addition as this is a hard requirement.
|
|
36
|
+
- Introduce `Headers#header` method to enumerate only the main headers, excluding trailers. This can be used after invoking `Headers#trailer!` to avoid race conditions.
|
|
37
|
+
- Fix `Headers#to_h` so that indexed headers are not left in an inconsistent state if errors occur during processing.
|
|
38
|
+
|
|
39
|
+
### v0.57.0
|
|
40
|
+
|
|
41
|
+
- Always use `#parse` when parsing header values from strings to ensure proper normalization and validation.
|
|
42
|
+
- Introduce `Protocol::HTTP::InvalidTrailerError` which is raised when a trailer header is not allowed by the current policy.
|
|
43
|
+
- **Breaking**: `Headers#each` now yields parsed values according to the current policy. For the previous behaviour, use `Headers#fields`.
|
|
44
|
+
|
|
33
45
|
### v0.56.0
|
|
34
46
|
|
|
35
47
|
- Introduce `Header::*.parse(value)` which parses a raw header value string into a header instance.
|
|
@@ -77,15 +89,6 @@ Please see the [project releases](https://socketry.github.io/protocol-http/relea
|
|
|
77
89
|
|
|
78
90
|
- Add support for parsing `accept`, `accept-charset`, `accept-encoding` and `accept-language` headers into structured values.
|
|
79
91
|
|
|
80
|
-
### v0.46.0
|
|
81
|
-
|
|
82
|
-
- Add support for `priority:` header.
|
|
83
|
-
|
|
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
92
|
## See Also
|
|
90
93
|
|
|
91
94
|
- [protocol-http1](https://github.com/socketry/protocol-http1) — HTTP/1 client/server implementation using this
|
data/releases.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.58.0
|
|
4
|
+
|
|
5
|
+
- Move trailer validation to `Headers#add` method to ensure all additions are checked at the time of addition as this is a hard requirement.
|
|
6
|
+
- Introduce `Headers#header` method to enumerate only the main headers, excluding trailers. This can be used after invoking `Headers#trailer!` to avoid race conditions.
|
|
7
|
+
- Fix `Headers#to_h` so that indexed headers are not left in an inconsistent state if errors occur during processing.
|
|
8
|
+
|
|
9
|
+
## v0.57.0
|
|
10
|
+
|
|
11
|
+
- Always use `#parse` when parsing header values from strings to ensure proper normalization and validation.
|
|
12
|
+
- Introduce `Protocol::HTTP::InvalidTrailerError` which is raised when a trailer header is not allowed by the current policy.
|
|
13
|
+
- **Breaking**: `Headers#each` now yields parsed values according to the current policy. For the previous behaviour, use `Headers#fields`.
|
|
14
|
+
|
|
3
15
|
## v0.56.0
|
|
4
16
|
|
|
5
17
|
- Introduce `Header::*.parse(value)` which parses a raw header value string into a header instance.
|
|
@@ -57,13 +69,13 @@ module GRPCMessage
|
|
|
57
69
|
end
|
|
58
70
|
|
|
59
71
|
GRPC_POLICY = Protocol::HTTP::Headers::POLICY.dup
|
|
60
|
-
GRPC_POLICY[
|
|
61
|
-
GRPC_POLICY[
|
|
72
|
+
GRPC_POLICY["grpc-status"] = GRPCStatus
|
|
73
|
+
GRPC_POLICY["grpc-message"] = GRPCMessage
|
|
62
74
|
|
|
63
75
|
# Reinterpret the headers using the new policy:
|
|
64
76
|
response.headers.policy = GRPC_POLICY
|
|
65
|
-
response.headers[
|
|
66
|
-
response.headers[
|
|
77
|
+
response.headers["grpc-status"] # => 0
|
|
78
|
+
response.headers["grpc-message"] # => "OK"
|
|
67
79
|
```
|
|
68
80
|
|
|
69
81
|
## v0.53.0
|
|
@@ -117,6 +129,7 @@ client.get("/", headers: {"accept" => "text/html"}, authority: "example.com")
|
|
|
117
129
|
# Response keyword arguments:
|
|
118
130
|
def call(request)
|
|
119
131
|
return Response[200, headers: {"content-Type" => "text/html"}, body: "Hello, World!"]
|
|
132
|
+
end
|
|
120
133
|
```
|
|
121
134
|
|
|
122
135
|
### Interim Response Handling
|
|
@@ -127,7 +140,12 @@ On the client side, you can pass a callback using the `interim_response` keyword
|
|
|
127
140
|
|
|
128
141
|
``` ruby
|
|
129
142
|
client = ...
|
|
130
|
-
|
|
143
|
+
|
|
144
|
+
interim_response = proc do |status, headers|
|
|
145
|
+
puts "Received interim response: #{status} -> #{headers.inspect}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
response = client.get("/index", interim_response: interim_response)
|
|
131
149
|
```
|
|
132
150
|
|
|
133
151
|
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.
|
|
4
|
+
version: 0.58.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -94,6 +94,7 @@ files:
|
|
|
94
94
|
- lib/protocol/http/header/digest.rb
|
|
95
95
|
- lib/protocol/http/header/etag.rb
|
|
96
96
|
- lib/protocol/http/header/etags.rb
|
|
97
|
+
- lib/protocol/http/header/generic.rb
|
|
97
98
|
- lib/protocol/http/header/multiple.rb
|
|
98
99
|
- lib/protocol/http/header/priority.rb
|
|
99
100
|
- lib/protocol/http/header/server_timing.rb
|
|
@@ -134,7 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
134
135
|
- !ruby/object:Gem::Version
|
|
135
136
|
version: '0'
|
|
136
137
|
requirements: []
|
|
137
|
-
rubygems_version:
|
|
138
|
+
rubygems_version: 4.0.3
|
|
138
139
|
specification_version: 4
|
|
139
140
|
summary: Provides abstractions to handle HTTP protocols.
|
|
140
141
|
test_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|