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.
Files changed (201) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +241 -41
  3. data/LICENSE.txt +1 -1
  4. data/README.md +110 -13
  5. data/UPGRADING.md +491 -0
  6. data/http.gemspec +32 -29
  7. data/lib/http/base64.rb +11 -1
  8. data/lib/http/chainable/helpers.rb +62 -0
  9. data/lib/http/chainable/verbs.rb +136 -0
  10. data/lib/http/chainable.rb +232 -136
  11. data/lib/http/client.rb +158 -127
  12. data/lib/http/connection/internals.rb +141 -0
  13. data/lib/http/connection.rb +126 -97
  14. data/lib/http/content_type.rb +61 -6
  15. data/lib/http/errors.rb +25 -1
  16. data/lib/http/feature.rb +65 -5
  17. data/lib/http/features/auto_deflate.rb +124 -17
  18. data/lib/http/features/auto_inflate.rb +38 -15
  19. data/lib/http/features/caching/entry.rb +178 -0
  20. data/lib/http/features/caching/in_memory_store.rb +63 -0
  21. data/lib/http/features/caching.rb +216 -0
  22. data/lib/http/features/digest_auth.rb +234 -0
  23. data/lib/http/features/instrumentation.rb +97 -17
  24. data/lib/http/features/logging.rb +183 -5
  25. data/lib/http/features/normalize_uri.rb +17 -0
  26. data/lib/http/features/raise_error.rb +18 -3
  27. data/lib/http/form_data/composite_io.rb +106 -0
  28. data/lib/http/form_data/file.rb +95 -0
  29. data/lib/http/form_data/multipart/param.rb +62 -0
  30. data/lib/http/form_data/multipart.rb +106 -0
  31. data/lib/http/form_data/part.rb +52 -0
  32. data/lib/http/form_data/readable.rb +58 -0
  33. data/lib/http/form_data/urlencoded.rb +175 -0
  34. data/lib/http/form_data/version.rb +8 -0
  35. data/lib/http/form_data.rb +102 -0
  36. data/lib/http/headers/known.rb +3 -0
  37. data/lib/http/headers/normalizer.rb +17 -36
  38. data/lib/http/headers.rb +172 -65
  39. data/lib/http/mime_type/adapter.rb +24 -9
  40. data/lib/http/mime_type/json.rb +19 -4
  41. data/lib/http/mime_type.rb +21 -3
  42. data/lib/http/options/definitions.rb +189 -0
  43. data/lib/http/options.rb +172 -125
  44. data/lib/http/redirector.rb +80 -75
  45. data/lib/http/request/body.rb +87 -6
  46. data/lib/http/request/builder.rb +184 -0
  47. data/lib/http/request/proxy.rb +83 -0
  48. data/lib/http/request/writer.rb +76 -16
  49. data/lib/http/request.rb +214 -98
  50. data/lib/http/response/body.rb +103 -18
  51. data/lib/http/response/inflater.rb +35 -7
  52. data/lib/http/response/parser.rb +98 -4
  53. data/lib/http/response/status/reasons.rb +2 -4
  54. data/lib/http/response/status.rb +141 -31
  55. data/lib/http/response.rb +219 -61
  56. data/lib/http/retriable/delay_calculator.rb +38 -11
  57. data/lib/http/retriable/errors.rb +21 -0
  58. data/lib/http/retriable/performer.rb +82 -38
  59. data/lib/http/session.rb +280 -0
  60. data/lib/http/timeout/global.rb +147 -34
  61. data/lib/http/timeout/null.rb +155 -9
  62. data/lib/http/timeout/per_operation.rb +139 -18
  63. data/lib/http/uri/normalizer.rb +82 -0
  64. data/lib/http/uri/parsing.rb +182 -0
  65. data/lib/http/uri.rb +289 -124
  66. data/lib/http/version.rb +2 -1
  67. data/lib/http.rb +11 -2
  68. data/sig/deps.rbs +122 -0
  69. data/sig/http.rbs +1619 -0
  70. data/test/http/base64_test.rb +28 -0
  71. data/test/http/client_test.rb +739 -0
  72. data/test/http/connection_test.rb +1533 -0
  73. data/test/http/content_type_test.rb +190 -0
  74. data/test/http/errors_test.rb +28 -0
  75. data/test/http/feature_test.rb +49 -0
  76. data/test/http/features/auto_deflate_test.rb +317 -0
  77. data/test/http/features/auto_inflate_test.rb +213 -0
  78. data/test/http/features/caching_test.rb +942 -0
  79. data/test/http/features/digest_auth_test.rb +996 -0
  80. data/test/http/features/instrumentation_test.rb +246 -0
  81. data/test/http/features/logging_test.rb +654 -0
  82. data/test/http/features/normalize_uri_test.rb +41 -0
  83. data/test/http/features/raise_error_test.rb +77 -0
  84. data/test/http/form_data/composite_io_test.rb +215 -0
  85. data/test/http/form_data/file_test.rb +255 -0
  86. data/test/http/form_data/fixtures/the-http-gem.info +1 -0
  87. data/test/http/form_data/multipart_test.rb +303 -0
  88. data/test/http/form_data/part_test.rb +90 -0
  89. data/test/http/form_data/urlencoded_test.rb +164 -0
  90. data/test/http/form_data_test.rb +232 -0
  91. data/test/http/headers/normalizer_test.rb +93 -0
  92. data/test/http/headers_test.rb +888 -0
  93. data/test/http/mime_type/json_test.rb +39 -0
  94. data/test/http/mime_type_test.rb +150 -0
  95. data/test/http/options/base_uri_test.rb +148 -0
  96. data/test/http/options/body_test.rb +21 -0
  97. data/test/http/options/features_test.rb +38 -0
  98. data/test/http/options/form_test.rb +21 -0
  99. data/test/http/options/headers_test.rb +32 -0
  100. data/test/http/options/json_test.rb +21 -0
  101. data/test/http/options/merge_test.rb +78 -0
  102. data/test/http/options/new_test.rb +37 -0
  103. data/test/http/options/proxy_test.rb +32 -0
  104. data/test/http/options_test.rb +575 -0
  105. data/test/http/redirector_test.rb +639 -0
  106. data/test/http/request/body_test.rb +318 -0
  107. data/test/http/request/builder_test.rb +623 -0
  108. data/test/http/request/writer_test.rb +391 -0
  109. data/test/http/request_test.rb +1733 -0
  110. data/test/http/response/body_test.rb +292 -0
  111. data/test/http/response/parser_test.rb +105 -0
  112. data/test/http/response/status_test.rb +322 -0
  113. data/test/http/response_test.rb +502 -0
  114. data/test/http/retriable/delay_calculator_test.rb +194 -0
  115. data/test/http/retriable/errors_test.rb +71 -0
  116. data/test/http/retriable/performer_test.rb +551 -0
  117. data/test/http/session_test.rb +424 -0
  118. data/test/http/timeout/global_test.rb +239 -0
  119. data/test/http/timeout/null_test.rb +218 -0
  120. data/test/http/timeout/per_operation_test.rb +220 -0
  121. data/test/http/uri/normalizer_test.rb +89 -0
  122. data/test/http/uri_test.rb +1140 -0
  123. data/test/http/version_test.rb +15 -0
  124. data/test/http_test.rb +818 -0
  125. data/test/regression_tests.rb +27 -0
  126. data/test/support/dummy_server/encoding_routes.rb +47 -0
  127. data/test/support/dummy_server/routes.rb +201 -0
  128. data/test/support/dummy_server/servlet.rb +81 -0
  129. data/test/support/dummy_server.rb +200 -0
  130. data/{spec → test}/support/fakeio.rb +2 -2
  131. data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
  132. data/test/support/http_handling_shared/timeout_tests.rb +134 -0
  133. data/test/support/http_handling_shared.rb +11 -0
  134. data/test/support/proxy_server.rb +207 -0
  135. data/test/support/servers/runner.rb +67 -0
  136. data/{spec → test}/support/simplecov.rb +11 -2
  137. data/test/support/ssl_helper.rb +108 -0
  138. data/test/test_helper.rb +38 -0
  139. metadata +108 -168
  140. data/.github/workflows/ci.yml +0 -67
  141. data/.gitignore +0 -15
  142. data/.rspec +0 -1
  143. data/.rubocop/layout.yml +0 -8
  144. data/.rubocop/metrics.yml +0 -4
  145. data/.rubocop/rspec.yml +0 -9
  146. data/.rubocop/style.yml +0 -32
  147. data/.rubocop.yml +0 -11
  148. data/.rubocop_todo.yml +0 -219
  149. data/.yardopts +0 -2
  150. data/CHANGES_OLD.md +0 -1002
  151. data/Gemfile +0 -51
  152. data/Guardfile +0 -18
  153. data/Rakefile +0 -64
  154. data/lib/http/headers/mixin.rb +0 -34
  155. data/lib/http/retriable/client.rb +0 -37
  156. data/logo.png +0 -0
  157. data/spec/lib/http/client_spec.rb +0 -556
  158. data/spec/lib/http/connection_spec.rb +0 -88
  159. data/spec/lib/http/content_type_spec.rb +0 -47
  160. data/spec/lib/http/features/auto_deflate_spec.rb +0 -77
  161. data/spec/lib/http/features/auto_inflate_spec.rb +0 -86
  162. data/spec/lib/http/features/instrumentation_spec.rb +0 -81
  163. data/spec/lib/http/features/logging_spec.rb +0 -65
  164. data/spec/lib/http/features/raise_error_spec.rb +0 -62
  165. data/spec/lib/http/headers/mixin_spec.rb +0 -36
  166. data/spec/lib/http/headers/normalizer_spec.rb +0 -52
  167. data/spec/lib/http/headers_spec.rb +0 -527
  168. data/spec/lib/http/options/body_spec.rb +0 -15
  169. data/spec/lib/http/options/features_spec.rb +0 -33
  170. data/spec/lib/http/options/form_spec.rb +0 -15
  171. data/spec/lib/http/options/headers_spec.rb +0 -24
  172. data/spec/lib/http/options/json_spec.rb +0 -15
  173. data/spec/lib/http/options/merge_spec.rb +0 -68
  174. data/spec/lib/http/options/new_spec.rb +0 -30
  175. data/spec/lib/http/options/proxy_spec.rb +0 -20
  176. data/spec/lib/http/options_spec.rb +0 -13
  177. data/spec/lib/http/redirector_spec.rb +0 -530
  178. data/spec/lib/http/request/body_spec.rb +0 -211
  179. data/spec/lib/http/request/writer_spec.rb +0 -121
  180. data/spec/lib/http/request_spec.rb +0 -234
  181. data/spec/lib/http/response/body_spec.rb +0 -85
  182. data/spec/lib/http/response/parser_spec.rb +0 -74
  183. data/spec/lib/http/response/status_spec.rb +0 -253
  184. data/spec/lib/http/response_spec.rb +0 -262
  185. data/spec/lib/http/retriable/delay_calculator_spec.rb +0 -69
  186. data/spec/lib/http/retriable/performer_spec.rb +0 -302
  187. data/spec/lib/http/uri/normalizer_spec.rb +0 -95
  188. data/spec/lib/http/uri_spec.rb +0 -71
  189. data/spec/lib/http_spec.rb +0 -535
  190. data/spec/regression_specs.rb +0 -24
  191. data/spec/spec_helper.rb +0 -89
  192. data/spec/support/black_hole.rb +0 -13
  193. data/spec/support/dummy_server/servlet.rb +0 -203
  194. data/spec/support/dummy_server.rb +0 -44
  195. data/spec/support/fuubar.rb +0 -21
  196. data/spec/support/http_handling_shared.rb +0 -190
  197. data/spec/support/proxy_server.rb +0 -39
  198. data/spec/support/servers/config.rb +0 -11
  199. data/spec/support/servers/runner.rb +0 -19
  200. data/spec/support/ssl_helper.rb +0 -104
  201. /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
- lib = File.expand_path("lib", __dir__)
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 |gem|
8
- gem.authors = ["Tony Arcieri", "Erik Michaels-Ober", "Alexey V. Zapparov", "Zachary Anker"]
9
- gem.email = ["bascule@gmail.com"]
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
- gem.description = <<-DESCRIPTION.strip.gsub(/\s+/, " ")
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
- gem.summary = "HTTP should be easy"
18
- gem.homepage = "https://github.com/httprb/http"
19
- gem.licenses = ["MIT"]
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
- gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
22
- gem.files = `git ls-files`.split("\n")
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
- gem.required_ruby_version = ">= 2.6"
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
- gem.add_runtime_dependency "addressable", "~> 2.8"
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
- gem.add_runtime_dependency "llhttp-ffi", "~> 0.5.0"
38
+ spec.required_ruby_version = ">= 3.2"
35
39
 
36
- gem.add_development_dependency "bundler", "~> 2.0"
40
+ spec.add_dependency "http-cookie", "~> 1.0"
37
41
 
38
- gem.metadata = {
39
- "source_code_uri" => "https://github.com/httprb/http",
40
- "wiki_uri" => "https://github.com/httprb/http/wiki",
41
- "bug_tracker_uri" => "https://github.com/httprb/http/issues",
42
- "changelog_uri" => "https://github.com/httprb/http/blob/v#{HTTP::VERSION}/CHANGELOG.md",
43
- "rubygems_mfa_required" => "true"
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
- # Equivalent to Base64.strict_encode64
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