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 +4 -4
- checksums.yaml.gz.sig +2 -3
- 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 +16 -16
- 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/priority.rb +1 -1
- data/lib/protocol/http/header/te.rb +7 -7
- data/lib/protocol/http/headers.rb +10 -11
- 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 +6 -5
- data/releases.md +17 -5
- data.tar.gz.sig +0 -0
- metadata +2 -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: 46aa99ddc598b23eb7d9670a806ca18d21314fc7e4bb4e42305a4ac4bd92c868
|
|
4
|
+
data.tar.gz: 0dd66a51205e73c5d5bd57fca401ce592f31a21fb09d1bc9126fb084b293d378
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 979dec4d8fedfce87917074576493ff6365e897bd6403f409d0ff862ed1517cb4cbfbc65b0f1c95ef83f77ceb20530e4a90131e0530c737fa7532886f6d431d5
|
|
7
|
+
data.tar.gz: a52fbe05985d5d6d87d37c46b72cc71ff79b8d4023d765c79efaa5e947681dd53a1d10a083ff1e54ad47bd9358ddb7a8e8ede7313c7db36c4c7d88cfad52d3a8
|
checksums.yaml.gz.sig
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
�
|
|
2
|
-
|
|
3
|
-
��"F�i;5+���k�(�: $����|(H^�9
|
|
1
|
+
E��:?[T�3-ӿ���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��s�v�N��]��8�m(���e^�eʘ����H/���<u����Kߧh��4dk)� �[fS��y���0�bC�gf�_��6�k�n�?vV��y7F�1��q�f�p���1�y�� �-kz��
|
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|
|
|
@@ -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
|
|
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|
|
|
@@ -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
|
|
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.
|
|
@@ -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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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,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[
|
|
61
|
-
GRPC_POLICY[
|
|
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[
|
|
66
|
-
response.headers[
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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
|