http 5.3.1 → 6.0.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
- data/CHANGELOG.md +241 -41
- data/LICENSE.txt +1 -1
- data/README.md +110 -13
- data/UPGRADING.md +491 -0
- data/http.gemspec +32 -29
- data/lib/http/base64.rb +11 -1
- data/lib/http/chainable/helpers.rb +62 -0
- data/lib/http/chainable/verbs.rb +136 -0
- data/lib/http/chainable.rb +232 -136
- data/lib/http/client.rb +158 -127
- data/lib/http/connection/internals.rb +141 -0
- data/lib/http/connection.rb +126 -97
- data/lib/http/content_type.rb +61 -6
- data/lib/http/errors.rb +25 -1
- data/lib/http/feature.rb +65 -5
- data/lib/http/features/auto_deflate.rb +124 -17
- data/lib/http/features/auto_inflate.rb +38 -15
- data/lib/http/features/caching/entry.rb +178 -0
- data/lib/http/features/caching/in_memory_store.rb +63 -0
- data/lib/http/features/caching.rb +216 -0
- data/lib/http/features/digest_auth.rb +234 -0
- data/lib/http/features/instrumentation.rb +97 -17
- data/lib/http/features/logging.rb +183 -5
- data/lib/http/features/normalize_uri.rb +17 -0
- data/lib/http/features/raise_error.rb +18 -3
- data/lib/http/form_data/composite_io.rb +106 -0
- data/lib/http/form_data/file.rb +95 -0
- data/lib/http/form_data/multipart/param.rb +62 -0
- data/lib/http/form_data/multipart.rb +106 -0
- data/lib/http/form_data/part.rb +52 -0
- data/lib/http/form_data/readable.rb +58 -0
- data/lib/http/form_data/urlencoded.rb +175 -0
- data/lib/http/form_data/version.rb +8 -0
- data/lib/http/form_data.rb +102 -0
- data/lib/http/headers/known.rb +3 -0
- data/lib/http/headers/normalizer.rb +17 -36
- data/lib/http/headers.rb +172 -65
- data/lib/http/mime_type/adapter.rb +24 -9
- data/lib/http/mime_type/json.rb +19 -4
- data/lib/http/mime_type.rb +21 -3
- data/lib/http/options/definitions.rb +189 -0
- data/lib/http/options.rb +172 -125
- data/lib/http/redirector.rb +80 -75
- data/lib/http/request/body.rb +87 -6
- data/lib/http/request/builder.rb +184 -0
- data/lib/http/request/proxy.rb +83 -0
- data/lib/http/request/writer.rb +76 -16
- data/lib/http/request.rb +214 -98
- data/lib/http/response/body.rb +103 -18
- data/lib/http/response/inflater.rb +35 -7
- data/lib/http/response/parser.rb +98 -4
- data/lib/http/response/status/reasons.rb +2 -4
- data/lib/http/response/status.rb +141 -31
- data/lib/http/response.rb +219 -61
- data/lib/http/retriable/delay_calculator.rb +38 -11
- data/lib/http/retriable/errors.rb +21 -0
- data/lib/http/retriable/performer.rb +82 -38
- data/lib/http/session.rb +280 -0
- data/lib/http/timeout/global.rb +147 -34
- data/lib/http/timeout/null.rb +155 -9
- data/lib/http/timeout/per_operation.rb +139 -18
- data/lib/http/uri/normalizer.rb +82 -0
- data/lib/http/uri/parsing.rb +182 -0
- data/lib/http/uri.rb +289 -124
- data/lib/http/version.rb +2 -1
- data/lib/http.rb +11 -2
- data/sig/deps.rbs +122 -0
- data/sig/http.rbs +1619 -0
- data/test/http/base64_test.rb +28 -0
- data/test/http/client_test.rb +739 -0
- data/test/http/connection_test.rb +1533 -0
- data/test/http/content_type_test.rb +190 -0
- data/test/http/errors_test.rb +28 -0
- data/test/http/feature_test.rb +49 -0
- data/test/http/features/auto_deflate_test.rb +317 -0
- data/test/http/features/auto_inflate_test.rb +213 -0
- data/test/http/features/caching_test.rb +942 -0
- data/test/http/features/digest_auth_test.rb +996 -0
- data/test/http/features/instrumentation_test.rb +246 -0
- data/test/http/features/logging_test.rb +654 -0
- data/test/http/features/normalize_uri_test.rb +41 -0
- data/test/http/features/raise_error_test.rb +77 -0
- data/test/http/form_data/composite_io_test.rb +215 -0
- data/test/http/form_data/file_test.rb +255 -0
- data/test/http/form_data/fixtures/the-http-gem.info +1 -0
- data/test/http/form_data/multipart_test.rb +303 -0
- data/test/http/form_data/part_test.rb +90 -0
- data/test/http/form_data/urlencoded_test.rb +164 -0
- data/test/http/form_data_test.rb +232 -0
- data/test/http/headers/normalizer_test.rb +93 -0
- data/test/http/headers_test.rb +888 -0
- data/test/http/mime_type/json_test.rb +39 -0
- data/test/http/mime_type_test.rb +150 -0
- data/test/http/options/base_uri_test.rb +148 -0
- data/test/http/options/body_test.rb +21 -0
- data/test/http/options/features_test.rb +38 -0
- data/test/http/options/form_test.rb +21 -0
- data/test/http/options/headers_test.rb +32 -0
- data/test/http/options/json_test.rb +21 -0
- data/test/http/options/merge_test.rb +78 -0
- data/test/http/options/new_test.rb +37 -0
- data/test/http/options/proxy_test.rb +32 -0
- data/test/http/options_test.rb +575 -0
- data/test/http/redirector_test.rb +639 -0
- data/test/http/request/body_test.rb +318 -0
- data/test/http/request/builder_test.rb +623 -0
- data/test/http/request/writer_test.rb +391 -0
- data/test/http/request_test.rb +1733 -0
- data/test/http/response/body_test.rb +292 -0
- data/test/http/response/parser_test.rb +105 -0
- data/test/http/response/status_test.rb +322 -0
- data/test/http/response_test.rb +502 -0
- data/test/http/retriable/delay_calculator_test.rb +194 -0
- data/test/http/retriable/errors_test.rb +71 -0
- data/test/http/retriable/performer_test.rb +551 -0
- data/test/http/session_test.rb +424 -0
- data/test/http/timeout/global_test.rb +239 -0
- data/test/http/timeout/null_test.rb +218 -0
- data/test/http/timeout/per_operation_test.rb +220 -0
- data/test/http/uri/normalizer_test.rb +89 -0
- data/test/http/uri_test.rb +1140 -0
- data/test/http/version_test.rb +15 -0
- data/test/http_test.rb +818 -0
- data/test/regression_tests.rb +27 -0
- data/test/support/dummy_server/encoding_routes.rb +47 -0
- data/test/support/dummy_server/routes.rb +201 -0
- data/test/support/dummy_server/servlet.rb +81 -0
- data/test/support/dummy_server.rb +200 -0
- data/{spec → test}/support/fakeio.rb +2 -2
- data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
- data/test/support/http_handling_shared/timeout_tests.rb +134 -0
- data/test/support/http_handling_shared.rb +11 -0
- data/test/support/proxy_server.rb +207 -0
- data/test/support/servers/runner.rb +67 -0
- data/{spec → test}/support/simplecov.rb +11 -2
- data/test/support/ssl_helper.rb +108 -0
- data/test/test_helper.rb +38 -0
- metadata +108 -168
- data/.github/workflows/ci.yml +0 -67
- data/.gitignore +0 -15
- data/.rspec +0 -1
- data/.rubocop/layout.yml +0 -8
- data/.rubocop/metrics.yml +0 -4
- data/.rubocop/rspec.yml +0 -9
- data/.rubocop/style.yml +0 -32
- data/.rubocop.yml +0 -11
- data/.rubocop_todo.yml +0 -219
- data/.yardopts +0 -2
- data/CHANGES_OLD.md +0 -1002
- data/Gemfile +0 -51
- data/Guardfile +0 -18
- data/Rakefile +0 -64
- data/lib/http/headers/mixin.rb +0 -34
- data/lib/http/retriable/client.rb +0 -37
- data/logo.png +0 -0
- data/spec/lib/http/client_spec.rb +0 -556
- data/spec/lib/http/connection_spec.rb +0 -88
- data/spec/lib/http/content_type_spec.rb +0 -47
- data/spec/lib/http/features/auto_deflate_spec.rb +0 -77
- data/spec/lib/http/features/auto_inflate_spec.rb +0 -86
- data/spec/lib/http/features/instrumentation_spec.rb +0 -81
- data/spec/lib/http/features/logging_spec.rb +0 -65
- data/spec/lib/http/features/raise_error_spec.rb +0 -62
- data/spec/lib/http/headers/mixin_spec.rb +0 -36
- data/spec/lib/http/headers/normalizer_spec.rb +0 -52
- data/spec/lib/http/headers_spec.rb +0 -527
- data/spec/lib/http/options/body_spec.rb +0 -15
- data/spec/lib/http/options/features_spec.rb +0 -33
- data/spec/lib/http/options/form_spec.rb +0 -15
- data/spec/lib/http/options/headers_spec.rb +0 -24
- data/spec/lib/http/options/json_spec.rb +0 -15
- data/spec/lib/http/options/merge_spec.rb +0 -68
- data/spec/lib/http/options/new_spec.rb +0 -30
- data/spec/lib/http/options/proxy_spec.rb +0 -20
- data/spec/lib/http/options_spec.rb +0 -13
- data/spec/lib/http/redirector_spec.rb +0 -530
- data/spec/lib/http/request/body_spec.rb +0 -211
- data/spec/lib/http/request/writer_spec.rb +0 -121
- data/spec/lib/http/request_spec.rb +0 -234
- data/spec/lib/http/response/body_spec.rb +0 -85
- data/spec/lib/http/response/parser_spec.rb +0 -74
- data/spec/lib/http/response/status_spec.rb +0 -253
- data/spec/lib/http/response_spec.rb +0 -262
- data/spec/lib/http/retriable/delay_calculator_spec.rb +0 -69
- data/spec/lib/http/retriable/performer_spec.rb +0 -302
- data/spec/lib/http/uri/normalizer_spec.rb +0 -95
- data/spec/lib/http/uri_spec.rb +0 -71
- data/spec/lib/http_spec.rb +0 -535
- data/spec/regression_specs.rb +0 -24
- data/spec/spec_helper.rb +0 -89
- data/spec/support/black_hole.rb +0 -13
- data/spec/support/dummy_server/servlet.rb +0 -203
- data/spec/support/dummy_server.rb +0 -44
- data/spec/support/fuubar.rb +0 -21
- data/spec/support/http_handling_shared.rb +0 -190
- data/spec/support/proxy_server.rb +0 -39
- data/spec/support/servers/config.rb +0 -11
- data/spec/support/servers/runner.rb +0 -19
- data/spec/support/ssl_helper.rb +0 -104
- /data/{spec → test}/support/capture_warning.rb +0 -0
data/UPGRADING.md
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
# Upgrading to HTTP.rb 6.0
|
|
2
|
+
|
|
3
|
+
This guide covers all breaking changes between v5.x and v6.0 and shows how to
|
|
4
|
+
update your code.
|
|
5
|
+
|
|
6
|
+
## Ruby version
|
|
7
|
+
|
|
8
|
+
**v6 requires Ruby 3.2+.** Drop support for Ruby 2.x and 3.0/3.1.
|
|
9
|
+
|
|
10
|
+
## Quick reference
|
|
11
|
+
|
|
12
|
+
| What changed | v5 | v6 |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| Chainable return type | `HTTP::Client` | `HTTP::Session` |
|
|
15
|
+
| `HTTP.persistent` return type | `HTTP::Client` | `HTTP::Session` (pools per origin) |
|
|
16
|
+
| `.retriable` return type | `HTTP::Retriable::Client` | `HTTP::Session` |
|
|
17
|
+
| `response.cookies` | `HTTP::CookieJar` | `Array<HTTP::Cookie>` |
|
|
18
|
+
| `response["Header"]` | Works (via `Headers::Mixin`) | Removed — use `response.headers["Header"]` |
|
|
19
|
+
| `request["Header"]` | Works (via `Headers::Mixin`) | Removed — use `request.headers["Header"]` |
|
|
20
|
+
| `status.even?`, `status.zero?`, etc. | Works (via `Delegator`) | Removed — use `status.code.even?` |
|
|
21
|
+
| `build_request` | On `Client`, `Session`, `HTTP` | Removed — use `HTTP::Request::Builder` |
|
|
22
|
+
| Options API | Accepts `Hash` or keywords | Keywords only |
|
|
23
|
+
| `addressable` gem | Required dependency | Optional (only for non-ASCII URIs) |
|
|
24
|
+
| `URI` setters (`scheme=`, etc.) | Available | Removed |
|
|
25
|
+
| `URI#join`, `URI#omit` | Returns `Addressable::URI` | Returns `HTTP::URI` |
|
|
26
|
+
| `readpartial` at EOF | Returns `nil` | Raises `EOFError` |
|
|
27
|
+
| Timeout defaults | 0.25s for omitted operations | No timeout for omitted operations |
|
|
28
|
+
| Global + per-op timeouts | Mutually exclusive | Can be combined |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Breaking changes in detail
|
|
33
|
+
|
|
34
|
+
### 1. Chainable methods return `HTTP::Session` instead of `HTTP::Client`
|
|
35
|
+
|
|
36
|
+
All chainable configuration methods (`.headers`, `.timeout`, `.cookies`, `.auth`,
|
|
37
|
+
`.follow`, `.via`, `.use`, `.encoding`, `.nodelay`, `.basic_auth`, `.accept`)
|
|
38
|
+
now return a thread-safe `HTTP::Session` instead of `HTTP::Client`.
|
|
39
|
+
|
|
40
|
+
`Session` creates a fresh `Client` for each request, making it safe to share
|
|
41
|
+
across threads. The HTTP verb methods (`.get`, `.post`, etc.) and
|
|
42
|
+
`.default_options` work the same way.
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
# v5
|
|
46
|
+
client = HTTP.headers("Accept" => "application/json")
|
|
47
|
+
client.is_a?(HTTP::Client) # => true
|
|
48
|
+
|
|
49
|
+
# v6
|
|
50
|
+
session = HTTP.headers("Accept" => "application/json")
|
|
51
|
+
session.is_a?(HTTP::Session) # => true
|
|
52
|
+
session.get("https://example.com") # works the same
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Action:** Update any `is_a?(HTTP::Client)` checks on the return value of
|
|
56
|
+
chainable methods to check for `HTTP::Session`.
|
|
57
|
+
|
|
58
|
+
### 2. `HTTP.persistent` returns `HTTP::Session` with connection pooling
|
|
59
|
+
|
|
60
|
+
`HTTP.persistent` now returns an `HTTP::Session` that pools one persistent
|
|
61
|
+
`HTTP::Client` per origin. This means cross-origin redirects work automatically
|
|
62
|
+
instead of raising `StateError`.
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
# v5
|
|
66
|
+
client = HTTP.persistent("https://api.example.com")
|
|
67
|
+
client.is_a?(HTTP::Client) # => true
|
|
68
|
+
# Cross-origin redirects raise StateError
|
|
69
|
+
|
|
70
|
+
# v6
|
|
71
|
+
session = HTTP.persistent("https://api.example.com")
|
|
72
|
+
session.is_a?(HTTP::Session) # => true
|
|
73
|
+
# Cross-origin redirects work — each origin gets its own connection
|
|
74
|
+
session.close # shuts down all pooled connections
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Chaining on a persistent session now shares the connection pool:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
# v5 — this broke connection reuse
|
|
81
|
+
HTTP.persistent("https://api.example.com").headers("X-Token" => "abc").get("/users")
|
|
82
|
+
|
|
83
|
+
# v6 — works correctly, shares the parent's connection pool
|
|
84
|
+
HTTP.persistent("https://api.example.com").headers("X-Token" => "abc").get("/users")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 3. `.retriable` returns `HTTP::Session` instead of `HTTP::Retriable::Client`
|
|
88
|
+
|
|
89
|
+
Retry is now a session-level option. The `HTTP::Retriable::Client` and
|
|
90
|
+
`HTTP::Retriable::Session` classes no longer exist.
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
# v5
|
|
94
|
+
client = HTTP.retriable(tries: 3)
|
|
95
|
+
client.is_a?(HTTP::Retriable::Client) # => true
|
|
96
|
+
|
|
97
|
+
# v6
|
|
98
|
+
session = HTTP.retriable(tries: 3)
|
|
99
|
+
session.is_a?(HTTP::Session) # => true
|
|
100
|
+
session.get("https://example.com") # retries up to 3 times
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 4. Options hashes replaced with keyword arguments
|
|
104
|
+
|
|
105
|
+
Methods across the public API now require keyword arguments. Passing an explicit
|
|
106
|
+
`Hash` as a positional argument no longer works, and unrecognized keywords raise
|
|
107
|
+
`ArgumentError`.
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# v5 — both work
|
|
111
|
+
HTTP.get("https://example.com", body: "data")
|
|
112
|
+
HTTP.get("https://example.com", {body: "data"})
|
|
113
|
+
|
|
114
|
+
opts = {body: "data", headers: {"Content-Type" => "text/plain"}}
|
|
115
|
+
HTTP.get("https://example.com", opts)
|
|
116
|
+
|
|
117
|
+
# v6 — keywords only
|
|
118
|
+
HTTP.get("https://example.com", body: "data")
|
|
119
|
+
|
|
120
|
+
opts = {body: "data", headers: {"Content-Type" => "text/plain"}}
|
|
121
|
+
HTTP.get("https://example.com", **opts) # note the double-splat
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
This applies to constructors too:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
# v5
|
|
128
|
+
HTTP::Options.new({response: :body})
|
|
129
|
+
HTTP::Client.new({timeout_class: HTTP::Timeout::Global})
|
|
130
|
+
|
|
131
|
+
# v6
|
|
132
|
+
HTTP::Options.new(response: :body)
|
|
133
|
+
HTTP::Client.new(timeout_class: HTTP::Timeout::Global)
|
|
134
|
+
|
|
135
|
+
# If you have an options hash, use double-splat:
|
|
136
|
+
opts = {response: :body}
|
|
137
|
+
HTTP::Options.new(**opts)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Affected methods: all HTTP verb methods, `request`, `follow`, `retriable`,
|
|
141
|
+
`URI.new`, `Request.new`, `Response.new`, `Options.new`, `Client.new`,
|
|
142
|
+
`Session.new`.
|
|
143
|
+
|
|
144
|
+
### 5. `Headers::Mixin` removed — no more `[]`/`[]=` on Request and Response
|
|
145
|
+
|
|
146
|
+
`Request` and `Response` no longer include `Headers::Mixin`, so you can't use
|
|
147
|
+
bracket access directly on them.
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
# v5
|
|
151
|
+
response = HTTP.get("https://example.com")
|
|
152
|
+
response["Content-Type"] # => "text/html"
|
|
153
|
+
request["Authorization"] = "Bearer token"
|
|
154
|
+
|
|
155
|
+
# v6
|
|
156
|
+
response = HTTP.get("https://example.com")
|
|
157
|
+
response.headers["Content-Type"] # => "text/html"
|
|
158
|
+
request.headers["Authorization"] = "Bearer token"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 6. `Response#cookies` returns `Array` instead of `CookieJar`
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
# v5
|
|
165
|
+
response.cookies # => #<HTTP::CookieJar ...>
|
|
166
|
+
response.cookies.each { |cookie| puts cookie.name }
|
|
167
|
+
jar = response.cookies
|
|
168
|
+
jar["session_id"] # CookieJar lookup
|
|
169
|
+
|
|
170
|
+
# v6
|
|
171
|
+
response.cookies # => [#<HTTP::Cookie ...>, ...]
|
|
172
|
+
response.cookies.each { |cookie| puts cookie.name }
|
|
173
|
+
cookie = response.cookies.find { |c| c.name == "session_id" }
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The `cookies` chainable option also changed — the last `.cookies()` call wins
|
|
177
|
+
(no implicit merging):
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
# v5 — cookies merged across calls
|
|
181
|
+
HTTP.cookies(a: "1").cookies(b: "2") # sends both a=1 and b=2
|
|
182
|
+
|
|
183
|
+
# v6 — last call wins
|
|
184
|
+
HTTP.cookies(a: "1").cookies(b: "2") # sends only b=2
|
|
185
|
+
HTTP.cookies(a: "1", b: "2") # sends both a=1 and b=2
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 7. `Response::Status` no longer delegates to `Integer`
|
|
189
|
+
|
|
190
|
+
`Status` is no longer a `Delegator` subclass. It uses `Comparable` and
|
|
191
|
+
`Forwardable` instead, providing `to_i`, `to_int`, `<=>`, and named predicates.
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
status = response.status
|
|
195
|
+
|
|
196
|
+
# Still works in v6
|
|
197
|
+
status.to_i # => 200
|
|
198
|
+
status == 200 # => true (via Comparable + to_int)
|
|
199
|
+
(200..299).cover?(status) # => true (via to_int)
|
|
200
|
+
status.ok? # => true
|
|
201
|
+
status.success? # => true
|
|
202
|
+
status.to_s # => "200 OK"
|
|
203
|
+
status.code # => 200
|
|
204
|
+
status.reason # => "OK"
|
|
205
|
+
|
|
206
|
+
# v5 only — breaks in v6
|
|
207
|
+
status.even? # NoMethodError — use status.code.even?
|
|
208
|
+
status.between?(200, 299) # NoMethodError — use status.code.between?(200, 299)
|
|
209
|
+
status + 1 # NoMethodError — use status.code + 1
|
|
210
|
+
status.__getobj__ # NoMethodError — use status.code
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Action:** Replace any direct `Integer` method calls on status objects with
|
|
214
|
+
`status.code.<method>`.
|
|
215
|
+
|
|
216
|
+
### 8. `build_request` removed — use `HTTP::Request::Builder`
|
|
217
|
+
|
|
218
|
+
The `build_request` method has been removed from `Client`, `Session`, and the
|
|
219
|
+
top-level `HTTP` module.
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
# v5
|
|
223
|
+
request = HTTP.build_request(:get, "https://example.com")
|
|
224
|
+
request = HTTP.headers("Accept" => "application/json")
|
|
225
|
+
.build_request(:post, "https://example.com", json: {name: "test"})
|
|
226
|
+
|
|
227
|
+
# v6
|
|
228
|
+
options = HTTP::Options.new(headers: {"Accept" => "application/json"},
|
|
229
|
+
json: {name: "test"})
|
|
230
|
+
builder = HTTP::Request::Builder.new(options)
|
|
231
|
+
request = builder.build(:post, "https://example.com")
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### 9. `readpartial` raises `EOFError` instead of returning `nil`
|
|
235
|
+
|
|
236
|
+
`Connection#readpartial`, `Body#readpartial`, and `Inflater#readpartial` now
|
|
237
|
+
raise `EOFError` at end-of-stream instead of returning `nil`, conforming to
|
|
238
|
+
Ruby's `IO#readpartial` contract.
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
# v5
|
|
242
|
+
loop do
|
|
243
|
+
chunk = response.body.readpartial
|
|
244
|
+
break if chunk.nil?
|
|
245
|
+
process(chunk)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# v6
|
|
249
|
+
loop do
|
|
250
|
+
chunk = response.body.readpartial
|
|
251
|
+
process(chunk)
|
|
252
|
+
rescue EOFError
|
|
253
|
+
break
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Or use the simpler iterator API (works in both versions):
|
|
257
|
+
response.body.each { |chunk| process(chunk) }
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### 10. Timeout behavior changes
|
|
261
|
+
|
|
262
|
+
#### No more 0.25s default for omitted per-operation timeouts
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
# v5 — omitted operations default to 0.25s
|
|
266
|
+
HTTP.timeout(read: 30).get(url)
|
|
267
|
+
# write and connect timeouts are 0.25s
|
|
268
|
+
|
|
269
|
+
# v6 — omitted operations have no timeout
|
|
270
|
+
HTTP.timeout(read: 30).get(url)
|
|
271
|
+
# write and connect have no timeout limit
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Action:** If you relied on the implicit 0.25s timeout, set all three
|
|
275
|
+
operations explicitly:
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
HTTP.timeout(read: 0.25, write: 0.25, connect: 0.25).get(url)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
#### Stricter timeout options parsing
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
# v6 — rejects unknown keys
|
|
285
|
+
HTTP.timeout(read: 5, keep_alive: 10)
|
|
286
|
+
# => ArgumentError: unknown timeout options: keep_alive
|
|
287
|
+
|
|
288
|
+
# v6 — rejects mixed short/long forms
|
|
289
|
+
HTTP.timeout(read: 5, write_timeout: 3)
|
|
290
|
+
# => ArgumentError (use one form consistently)
|
|
291
|
+
|
|
292
|
+
# Valid in v6
|
|
293
|
+
HTTP.timeout(read: 5, write: 3, connect: 2)
|
|
294
|
+
HTTP.timeout(read_timeout: 5, write_timeout: 3, connect_timeout: 2)
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
#### Global and per-operation timeouts can be combined
|
|
298
|
+
|
|
299
|
+
```ruby
|
|
300
|
+
# v5 — mutually exclusive, raises ArgumentError
|
|
301
|
+
HTTP.timeout(global: 60, read: 30)
|
|
302
|
+
|
|
303
|
+
# v6 — works: 60s overall, 30s max per read, 10s max per write, 5s max per connect
|
|
304
|
+
HTTP.timeout(global: 60, read: 30, write: 10, connect: 5)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### 11. `addressable` is no longer a runtime dependency
|
|
308
|
+
|
|
309
|
+
`addressable` is lazy-loaded only when parsing non-ASCII (IRI) URIs. If your
|
|
310
|
+
code uses non-ASCII characters in URIs, add `addressable` to your Gemfile:
|
|
311
|
+
|
|
312
|
+
```ruby
|
|
313
|
+
# Gemfile
|
|
314
|
+
gem "addressable"
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
ASCII-only URIs use Ruby's stdlib `URI` parser exclusively and do not need
|
|
318
|
+
`addressable`.
|
|
319
|
+
|
|
320
|
+
### 12. `HTTP::URI` API changes
|
|
321
|
+
|
|
322
|
+
#### Removed setter methods
|
|
323
|
+
|
|
324
|
+
URI objects are now effectively immutable. Setter methods (`scheme=`, `host=`,
|
|
325
|
+
`port=`, `path=`, `query=`, `fragment=`, `user=`, `password=`, etc.) have been
|
|
326
|
+
removed.
|
|
327
|
+
|
|
328
|
+
```ruby
|
|
329
|
+
# v5
|
|
330
|
+
uri = HTTP::URI.parse("https://example.com")
|
|
331
|
+
uri.scheme = "http"
|
|
332
|
+
uri.path = "/api"
|
|
333
|
+
|
|
334
|
+
# v6 — construct a new URI instead
|
|
335
|
+
uri = HTTP::URI.parse("http://example.com/api")
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
#### `join` and `omit` now return `HTTP::URI`
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
# v5
|
|
342
|
+
uri = HTTP::URI.parse("https://example.com")
|
|
343
|
+
joined = uri.join("/path")
|
|
344
|
+
joined.is_a?(Addressable::URI) # => true
|
|
345
|
+
|
|
346
|
+
# v6
|
|
347
|
+
joined = uri.join("/path")
|
|
348
|
+
joined.is_a?(HTTP::URI) # => true
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
#### Removed `query_values` / `query_values=`
|
|
352
|
+
|
|
353
|
+
```ruby
|
|
354
|
+
# v5
|
|
355
|
+
uri.query_values = {"page" => "1", "per" => "10"}
|
|
356
|
+
uri.query_values # => {"page" => "1", "per" => "10"}
|
|
357
|
+
|
|
358
|
+
# v6 — use stdlib
|
|
359
|
+
uri = HTTP::URI.parse("https://example.com")
|
|
360
|
+
query = URI.encode_www_form(page: 1, per: 10)
|
|
361
|
+
uri = HTTP::URI.parse("https://example.com?#{query}")
|
|
362
|
+
|
|
363
|
+
# or use the params option:
|
|
364
|
+
HTTP.get("https://example.com", params: {page: 1, per: 10})
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
#### Removed `form_encode`
|
|
368
|
+
|
|
369
|
+
```ruby
|
|
370
|
+
# v5
|
|
371
|
+
HTTP::URI.form_encode(page: 1, per: 10)
|
|
372
|
+
|
|
373
|
+
# v6
|
|
374
|
+
URI.encode_www_form(page: 1, per: 10)
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
#### `HTTP::URI.new` no longer accepts `Addressable::URI`
|
|
378
|
+
|
|
379
|
+
```ruby
|
|
380
|
+
# v5
|
|
381
|
+
addr = Addressable::URI.parse("https://example.com")
|
|
382
|
+
HTTP::URI.new(addr) # works
|
|
383
|
+
|
|
384
|
+
# v6
|
|
385
|
+
HTTP::URI.new(addr) # ArgumentError
|
|
386
|
+
HTTP::URI.parse("https://example.com") # use parse instead
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### 13. Error class changes
|
|
390
|
+
|
|
391
|
+
#### Malformed URI errors
|
|
392
|
+
|
|
393
|
+
```ruby
|
|
394
|
+
# v5
|
|
395
|
+
HTTP.get("not a uri")
|
|
396
|
+
# => HTTP::UnsupportedSchemeError or Addressable::URI::InvalidURIError
|
|
397
|
+
|
|
398
|
+
# v6
|
|
399
|
+
HTTP.get("not a uri")
|
|
400
|
+
# => HTTP::URI::InvalidError (for malformed URIs)
|
|
401
|
+
|
|
402
|
+
HTTP.get(nil)
|
|
403
|
+
# => ArgumentError (for nil/empty URIs)
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
#### New `ConnectionError` subclasses
|
|
407
|
+
|
|
408
|
+
`ConnectionError` now has more specific subclasses for targeted rescue:
|
|
409
|
+
|
|
410
|
+
- `HTTP::ResponseHeaderError` — header parsing failed
|
|
411
|
+
- `HTTP::SocketReadError` — socket read failed
|
|
412
|
+
- `HTTP::SocketWriteError` — socket write failed
|
|
413
|
+
|
|
414
|
+
```ruby
|
|
415
|
+
# v5
|
|
416
|
+
rescue HTTP::ConnectionError => e
|
|
417
|
+
# all connection errors
|
|
418
|
+
|
|
419
|
+
# v6 — you can be more specific
|
|
420
|
+
rescue HTTP::SocketReadError => e
|
|
421
|
+
# only read failures
|
|
422
|
+
rescue HTTP::ConnectionError => e
|
|
423
|
+
# still catches all connection errors (superclass)
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### 14. Security: credential stripping on cross-origin redirects
|
|
427
|
+
|
|
428
|
+
`Authorization` and `Cookie` headers are now automatically stripped when
|
|
429
|
+
following redirects to a different origin (scheme + host + port). This is a
|
|
430
|
+
security improvement, but may break code that intentionally sends credentials
|
|
431
|
+
across origins.
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## New features (non-breaking)
|
|
436
|
+
|
|
437
|
+
These are new in v6 and require no migration, but are worth knowing about:
|
|
438
|
+
|
|
439
|
+
### Block form for verb methods
|
|
440
|
+
|
|
441
|
+
Auto-closes the connection after the block returns:
|
|
442
|
+
|
|
443
|
+
```ruby
|
|
444
|
+
body = HTTP.get("https://example.com") do |response|
|
|
445
|
+
response.body.to_s
|
|
446
|
+
end
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### `HTTP.base_uri`
|
|
450
|
+
|
|
451
|
+
Set a base URI for relative paths:
|
|
452
|
+
|
|
453
|
+
```ruby
|
|
454
|
+
api = HTTP.base_uri("https://api.example.com/v1")
|
|
455
|
+
api.get("users") # GET https://api.example.com/v1/users
|
|
456
|
+
api.get("posts") # GET https://api.example.com/v1/posts
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### HTTP Digest Authentication
|
|
460
|
+
|
|
461
|
+
```ruby
|
|
462
|
+
HTTP.digest_auth(user: "admin", pass: "secret").get("https://example.com/protected")
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### HTTP Caching (RFC 7234)
|
|
466
|
+
|
|
467
|
+
```ruby
|
|
468
|
+
HTTP.use(:caching).get("https://example.com") # caches with in-memory store
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Pattern matching
|
|
472
|
+
|
|
473
|
+
```ruby
|
|
474
|
+
case response
|
|
475
|
+
in { status: { code: 200..299 }, content_type: { mime_type: "application/json" } }
|
|
476
|
+
response.parse
|
|
477
|
+
in { status: { code: 404 } }
|
|
478
|
+
nil
|
|
479
|
+
end
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### `Feature#on_request` and `Feature#around_request` hooks
|
|
483
|
+
|
|
484
|
+
New feature lifecycle hooks called before/around each request attempt (including
|
|
485
|
+
retries), useful for instrumentation and circuit breakers.
|
|
486
|
+
|
|
487
|
+
### `PURGE` HTTP method
|
|
488
|
+
|
|
489
|
+
```ruby
|
|
490
|
+
HTTP.request(:purge, "https://cdn.example.com/asset")
|
|
491
|
+
```
|
data/http.gemspec
CHANGED
|
@@ -1,45 +1,48 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
|
-
require "http/version"
|
|
3
|
+
require_relative "lib/http/version"
|
|
6
4
|
|
|
7
|
-
Gem::Specification.new do |
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "http"
|
|
7
|
+
spec.version = HTTP::VERSION
|
|
8
|
+
spec.authors = ["Tony Arcieri", "Erik Berlin", "Alexey V. Zapparov", "Zachary Anker"]
|
|
9
|
+
spec.email = ["bascule@gmail.com"]
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
spec.summary = "HTTP should be easy"
|
|
12
|
+
spec.homepage = "https://github.com/httprb/http"
|
|
13
|
+
spec.license = "MIT"
|
|
14
|
+
|
|
15
|
+
spec.description = <<~DESCRIPTION.strip.gsub(/\s+/, " ")
|
|
12
16
|
An easy-to-use client library for making requests from Ruby.
|
|
13
17
|
It uses a simple method chaining system for building requests,
|
|
14
18
|
similar to Python's Requests.
|
|
15
19
|
DESCRIPTION
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
22
|
+
spec.metadata["source_code_uri"] = "#{spec.homepage}/tree/v#{spec.version}"
|
|
23
|
+
spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
|
|
24
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/v#{spec.version}/CHANGELOG.md"
|
|
25
|
+
spec.metadata["documentation_uri"] = "https://www.rubydoc.info/gems/http/#{spec.version}"
|
|
26
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
24
|
-
gem.name = "http"
|
|
25
|
-
gem.require_paths = ["lib"]
|
|
26
|
-
gem.version = HTTP::VERSION
|
|
28
|
+
spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
|
|
29
|
+
extras = %w[CHANGELOG.md CONTRIBUTING.md LICENSE.txt README.md SECURITY.md UPGRADING.md] << File.basename(__FILE__)
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
ls.readlines("\x0", chomp: true).select do |f|
|
|
32
|
+
f.start_with?("lib/", "test/", "sig/") || extras.include?(f)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
gem.add_runtime_dependency "http-cookie", "~> 1.0"
|
|
32
|
-
gem.add_runtime_dependency "http-form_data", "~> 2.2"
|
|
36
|
+
spec.require_paths = ["lib"]
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
spec.required_ruby_version = ">= 3.2"
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
spec.add_dependency "http-cookie", "~> 1.0"
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
}
|
|
42
|
+
if RUBY_ENGINE == "jruby"
|
|
43
|
+
spec.platform = "java" if ENV["HTTP_PLATFORM"] == "java"
|
|
44
|
+
spec.add_dependency "llhttp-ffi", "~> 0.5.1"
|
|
45
|
+
else
|
|
46
|
+
spec.add_dependency "llhttp", "~> 0.6.1"
|
|
47
|
+
end
|
|
45
48
|
end
|
data/lib/http/base64.rb
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module HTTP
|
|
4
|
+
# Strict Base64 encoding utilities
|
|
4
5
|
module Base64
|
|
5
6
|
module_function
|
|
6
7
|
|
|
7
|
-
#
|
|
8
|
+
# Encode data using strict Base64 encoding
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# HTTP::Base64.encode64("hello")
|
|
12
|
+
#
|
|
13
|
+
# @param input [String] data to encode
|
|
14
|
+
#
|
|
15
|
+
# @return [String]
|
|
16
|
+
#
|
|
17
|
+
# @api private
|
|
8
18
|
def encode64(input)
|
|
9
19
|
[input].pack("m0")
|
|
10
20
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTP
|
|
4
|
+
# HTTP verb methods and client configuration DSL
|
|
5
|
+
module Chainable
|
|
6
|
+
# Mapping of proxy argument positions to hash keys and expected types
|
|
7
|
+
PROXY_ARG_MAP = [
|
|
8
|
+
[:proxy_address, 0, String],
|
|
9
|
+
[:proxy_port, 1, Integer],
|
|
10
|
+
[:proxy_username, 2, String],
|
|
11
|
+
[:proxy_password, 3, String],
|
|
12
|
+
[:proxy_headers, 2, Hash],
|
|
13
|
+
[:proxy_headers, 4, Hash]
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# Build proxy configuration hash from positional arguments
|
|
19
|
+
#
|
|
20
|
+
# @param [Array] proxy positional proxy arguments
|
|
21
|
+
# @return [Hash] proxy configuration
|
|
22
|
+
# @api private
|
|
23
|
+
def build_proxy_hash(proxy)
|
|
24
|
+
result = {} #: Hash[Symbol, untyped]
|
|
25
|
+
PROXY_ARG_MAP.each do |key, index, type|
|
|
26
|
+
result[key] = proxy[index] if proxy[index].is_a?(type)
|
|
27
|
+
end
|
|
28
|
+
result
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Resolve a timeout hash into a timeout class and normalized options
|
|
32
|
+
#
|
|
33
|
+
# @example
|
|
34
|
+
# resolve_timeout_hash(global: 60, read: 30)
|
|
35
|
+
#
|
|
36
|
+
# @param [Hash] options timeout options
|
|
37
|
+
# @return [Array(Class, Hash)] timeout class and normalized options
|
|
38
|
+
# @raise [ArgumentError] if options are invalid
|
|
39
|
+
# @api private
|
|
40
|
+
def resolve_timeout_hash(options)
|
|
41
|
+
remaining = options.dup
|
|
42
|
+
global = HTTP::Timeout::PerOperation.send(:extract_global_timeout!, remaining)
|
|
43
|
+
|
|
44
|
+
return resolve_global_only(global) if remaining.empty?
|
|
45
|
+
|
|
46
|
+
per_op = HTTP::Timeout::PerOperation.normalize_options(remaining)
|
|
47
|
+
global ? [HTTP::Timeout::Global, per_op.merge(global_timeout: global)] : [HTTP::Timeout::PerOperation, per_op]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Build options for a global-only timeout from a hash
|
|
51
|
+
#
|
|
52
|
+
# @param [Numeric, nil] global the global timeout value
|
|
53
|
+
# @return [Array(Class, Hash)] timeout class and options
|
|
54
|
+
# @raise [ArgumentError] if no global timeout given
|
|
55
|
+
# @api private
|
|
56
|
+
def resolve_global_only(global)
|
|
57
|
+
raise ArgumentError, "no timeout options given" unless global
|
|
58
|
+
|
|
59
|
+
[HTTP::Timeout::Global, { global_timeout: global }]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|