http.rb 0.21.0 → 0.23.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 +97 -36
- data/Rakefile +4 -4
- data/http.rb.gemspec +8 -7
- data/lib/HTTP/RETRY.rb +2 -1
- data/lib/HTTP/VERSION.rb +1 -1
- data/lib/HTTP/request.rb +5 -4
- data/test/HTTP/RETRY_test.rb +295 -0
- data/test/HTTP/delete_test.rb +265 -0
- data/test/HTTP/get_test.rb +367 -0
- data/{spec/HTTP/head_spec.rb → test/HTTP/head_test.rb} +27 -24
- data/test/HTTP/options_test.rb +246 -0
- data/test/HTTP/patch_test.rb +408 -0
- data/test/HTTP/post_test.rb +448 -0
- data/test/HTTP/put_test.rb +408 -0
- data/test/HTTP/trace_test.rb +246 -0
- data/{spec/spec_helper.rb → test/helper.rb} +15 -2
- metadata +28 -14
- data/spec/HTTP/RETRY_spec.rb +0 -251
- data/spec/HTTP/delete_spec.rb +0 -268
- data/spec/HTTP/get_spec.rb +0 -295
- data/spec/HTTP/options_spec.rb +0 -247
- data/spec/HTTP/patch_spec.rb +0 -406
- data/spec/HTTP/post_spec.rb +0 -447
- data/spec/HTTP/put_spec.rb +0 -406
- data/spec/HTTP/trace_spec.rb +0 -247
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b1e4577389b2272eb97f9912694fc3e2951c64cf83758c9a6f05b544dab99f3f
|
|
4
|
+
data.tar.gz: 13574567a8fabe8ac8693cda1cd183703eb8d7f57f94da20ffe249feb4b1a52b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f15f20fb2292d31e28da282fd138463234a2cead1e153a963c32a298b3fee441fcd50958203be30a103eb2c176da18f4beccdd6b362624e1c2769c1ee5b351f8
|
|
7
|
+
data.tar.gz: c8062c4c3f8ea7ffba22ac31f5e53288b8b36941651df1d087b18ec6e9bff46178e45afd421fc6218a910d61dc32e6cf410182336d058f28136b302589e1869a
|
data/CHANGELOG
CHANGED
|
@@ -1,15 +1,45 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
## 20260522
|
|
4
|
+
|
|
5
|
+
0.23.0: Fix cross-scheme redirect SSL leak; clamp negative Retry-After.
|
|
6
|
+
|
|
7
|
+
1. ~ HTTP.request: Compute http-object SSL configuration via options.merge instead of mutating the caller's options hash with auto-derived use_ssl and verify_mode. The previous ||= writes meant an HTTPS→HTTP redirect carried use_ssl: true through to the recursive call against the HTTP host, attempting an SSL handshake on the plain-HTTP port. Cross-scheme redirects in both directions now re-derive use_ssl from each URI.
|
|
8
|
+
2. ~ HTTP.retry_after: Clamp the HTTP-date branch via delta && [delta, 0].max. A past Retry-After HTTP-date previously returned a negative delta; Kernel.sleep raises ArgumentError on negatives.
|
|
9
|
+
3. ~ test/HTTP/get_test.rb: + cross-scheme redirection specs (HTTPS→HTTP and HTTP→HTTPS) using a Net::HTTP.new stub to capture each Net::HTTP instance and assert use_ssl? per hop.
|
|
10
|
+
4. ~ test/HTTP/RETRY_test.rb: + specs for past-date Retry-After (clamps to 0) and negative-integer Retry-After (returns nil).
|
|
11
|
+
5. ~ HTTP::VERSION: /0.22.0/0.23.0/
|
|
12
|
+
6. ~ CHANGELOG: + 0.23.0 entry; fix 0.13.2 date typo (202503030 → 20250330).
|
|
13
|
+
|
|
14
|
+
## 20260522
|
|
15
|
+
|
|
16
|
+
0.22.0: Convert specs from RSpec to Minitest.
|
|
17
|
+
|
|
18
|
+
1. ~ spec/ → test/: All spec files moved to test/ and rewritten in Minitest spec style with `let`, double-quoted descriptions, and `_(...).must_*` expectations. On the TODO since at least 0.17.0; completed before 1.0 to ship into stability on the test framework that's here to stay.
|
|
19
|
+
2. ~ spec/spec_helper.rb → test/helper.rb: Rewrite for Minitest; preserve the webmock http_rb_adapter collision workaround. + MockResponse Struct used as a header-stand-in in HTTP.retry_after specs in place of instance_double(Net::HTTPResponse).
|
|
20
|
+
3. ~ Rakefile: Use Rake::TestTask instead of RSpec::Core::RakeTask.
|
|
21
|
+
4. ~ http.rb.gemspec: -rspec; +minitest; +minitest-mock (minitest 6 extracted Mock/stub into a separate gem); files glob /spec/test/.
|
|
22
|
+
5. ~ HTTP.request: /response.header['location']/response['location']/ — Net::HTTPResponse#header has been deprecated for years; the documented replacement is #[]. Surfaced by Minitest's default reporter showing stderr warnings inline where RSpec was hiding them.
|
|
23
|
+
6. ~ TODO: Annotate "Convert specs from RSpec to Minitest" as (Done as of 0.22.0).
|
|
24
|
+
7. ~ HTTP::VERSION: /0.21.0/0.22.0/
|
|
25
|
+
8. ~ CHANGELOG: + 0.22.0 entry
|
|
26
|
+
9. ~ CHANGELOG: Reformat all entries with Markdown-style headings — `## YYYYMMDD` date headings, version line no longer `#`-prefixed.
|
|
27
|
+
10. ~ http.rb.gemspec: Pin minitest to `~> 6.0` to lock the major and avoid bundler quietly resolving to 5.x off a stale lockfile.
|
|
28
|
+
|
|
29
|
+
## 20260522
|
|
30
|
+
|
|
31
|
+
0.21.0: Add specs for options, trace, and patch verbs.
|
|
32
|
+
|
|
5
33
|
1. + spec/HTTP/options_spec.rb: Specs for HTTP.options. These were owed since 0.18.0 when the verb was added via the metaprogramming refactor.
|
|
6
34
|
2. + spec/HTTP/trace_spec.rb: Specs for HTTP.trace. Owed since 0.18.0.
|
|
7
35
|
3. + spec/HTTP/patch_spec.rb: Specs for HTTP.patch. Owed since 0.18.0.
|
|
8
36
|
4. ~ HTTP::VERSION: /0.20.0/0.21.0/
|
|
9
37
|
5. ~ CHANGELOG: + 0.21.0 entry
|
|
10
38
|
|
|
11
|
-
|
|
12
|
-
|
|
39
|
+
## 20260522
|
|
40
|
+
|
|
41
|
+
0.20.0: Add opt-in retry logic with exponential backoff.
|
|
42
|
+
|
|
13
43
|
1. + lib/HTTP/RETRY.rb: Retry helpers (with_retries, backoff_delay, retry_after) and constants (HTTP::RETRY::EXCEPTIONS, STATUS_CODES, VERBS).
|
|
14
44
|
2. ~ HTTP.request: Retry on transient network exceptions and retry-worthy HTTP status codes (429, 502, 503, 504) when enabled. Exponential backoff with jitter. Honours Retry-After when present. Disabled by default; opt in via the retries option. Configurable via new options: retries (default 0), retry_delay (default 1.0), retry_status_codes, retry_exceptions, retry_verbs. Only idempotent verbs (get, head, options, put, delete, trace) retry by default; opt in to POST/PATCH retries via retry_verbs.
|
|
15
45
|
3. + spec/HTTP/RETRY_spec.rb: Specs for retry behaviour and the helpers.
|
|
@@ -18,8 +48,10 @@
|
|
|
18
48
|
6. ~ HTTP::VERSION: /0.19.0/0.20.0/
|
|
19
49
|
7. ~ CHANGELOG: + 0.20.0 entry
|
|
20
50
|
|
|
21
|
-
|
|
22
|
-
|
|
51
|
+
## 20260522
|
|
52
|
+
|
|
53
|
+
0.19.0: Change default verify_mode to VERIFY_PEER.
|
|
54
|
+
|
|
23
55
|
1. ~ HTTP.request: Default verify_mode changed from OpenSSL::SSL::VERIFY_NONE to OpenSSL::SSL::VERIFY_PEER. Callers needing the old behaviour can pass verify_mode: OpenSSL::SSL::VERIFY_NONE explicitly through the options hash.
|
|
24
56
|
2. ~ spec/HTTP/get_spec.rb: + specs for default verify_mode and explicit VERIFY_NONE override; /verify_mode: 0/verify_mode: OpenSSL::SSL::VERIFY_PEER/ in redirect specs.
|
|
25
57
|
3. ~ spec/HTTP/post_spec.rb: /verify_mode: 0/verify_mode: OpenSSL::SSL::VERIFY_PEER/ in redirect specs.
|
|
@@ -29,8 +61,10 @@
|
|
|
29
61
|
7. ~ HTTP::VERSION: /0.18.3/0.19.0/
|
|
30
62
|
8. ~ CHANGELOG: + 0.19.0 entry
|
|
31
63
|
|
|
32
|
-
|
|
33
|
-
|
|
64
|
+
## 20260521
|
|
65
|
+
|
|
66
|
+
0.18.3: Fix verb preservation on 307/308 redirects.
|
|
67
|
+
|
|
34
68
|
1. ~ HTTP.request: Use original verb when following 307 or 308 redirects, per RFC 7231 §6.4.7 and RFC 7538. 301/302/303 keep legacy GET-on-redirect behaviour.
|
|
35
69
|
2. ~ spec/HTTP/post_spec.rb: + specs for 307/308 verb preservation and 307 body preservation.
|
|
36
70
|
3. ~ spec/HTTP/put_spec.rb: + spec for 307 verb preservation.
|
|
@@ -39,8 +73,10 @@
|
|
|
39
73
|
6. ~ HTTP::VERSION: /0.18.2/0.18.3/
|
|
40
74
|
7. ~ CHANGELOG: + 0.18.3 entry
|
|
41
75
|
|
|
42
|
-
|
|
43
|
-
|
|
76
|
+
## 20260520
|
|
77
|
+
|
|
78
|
+
0.18.2: Fix relative redirect URL construction.
|
|
79
|
+
|
|
44
80
|
1. ~ HTTP.request: Use URI#merge for redirect URL construction. Preserves original scheme, elides default ports, and resolves relative paths per RFC 3986.
|
|
45
81
|
2. ~ spec/HTTP/get_spec.rb: Update relative-redirect stubs to elide default port; + context for HTTPS relative redirect.
|
|
46
82
|
3. ~ spec/HTTP/post_spec.rb: Update relative-redirect stubs to elide default port.
|
|
@@ -49,8 +85,10 @@
|
|
|
49
85
|
6. ~ HTTP::VERSION: /0.18.1/0.18.2/
|
|
50
86
|
7. ~ CHANGELOG: + 0.18.2 entry
|
|
51
87
|
|
|
52
|
-
|
|
53
|
-
|
|
88
|
+
## 20260508
|
|
89
|
+
|
|
90
|
+
0.18.1: Remove incorrectly added WebDAV verbs.
|
|
91
|
+
|
|
54
92
|
1. - lib/Net/HTTP/Report.rb
|
|
55
93
|
2. ~ HTTP::VERBS: - require_relative '../Net/HTTP/Report'
|
|
56
94
|
3. ~ HTTP::VERBS::WITH_BODY: - propfind, proppatch, mkcol, copy, move, lock, unlock, report
|
|
@@ -59,8 +97,9 @@
|
|
|
59
97
|
6. ~ HTTP::VERSION: /0.18.0/0.18.1/
|
|
60
98
|
7. ~ CHANGELOG: + 0.18.1 entry
|
|
61
99
|
|
|
62
|
-
|
|
63
|
-
|
|
100
|
+
## 20260507
|
|
101
|
+
0.18.0: Add all missing HTTP verbs; use meta-programming to define verb methods.
|
|
102
|
+
|
|
64
103
|
1. + HTTP/verbs.rb; including:
|
|
65
104
|
+ HTTP::VERBS::WITHOUT_BODY: get, delete, head, options, trace
|
|
66
105
|
+ HTTP::VERBS::WITH_BODY: post, put, patch, propfind, proppatch, mkcol, copy, move, lock, unlock, report
|
|
@@ -87,8 +126,10 @@
|
|
|
87
126
|
22. ~ .gitignore: Using a common one with lots of entries.
|
|
88
127
|
23. ~ README.md: /http.rb/http/; Trimmed the Description; Usage has more code blocks, making the comments Markdown sub-sections.
|
|
89
128
|
|
|
90
|
-
|
|
91
|
-
|
|
129
|
+
## 20260325
|
|
130
|
+
|
|
131
|
+
0.17.0: Extract HTTP.request method; consolidate set_headers.
|
|
132
|
+
|
|
92
133
|
1. + HTTP.request: Extract common plumbing (connection, SSL, auth, redirect, response) from verb methods.
|
|
93
134
|
2. ~ HTTP.get: Delegate to HTTP.request.
|
|
94
135
|
3. ~ HTTP.delete: Delegate to HTTP.request.
|
|
@@ -104,8 +145,10 @@
|
|
|
104
145
|
13. ~ CHANGELOG.txt: + 0.17.0 entry
|
|
105
146
|
14. ~ http.rb.gemspec: Change date.
|
|
106
147
|
|
|
107
|
-
|
|
108
|
-
|
|
148
|
+
## 20250908
|
|
149
|
+
|
|
150
|
+
0.16.1: Allow any case for the content-type key.
|
|
151
|
+
|
|
109
152
|
1. ~ HTTP.post: Check for any case for content-type key.
|
|
110
153
|
2. ~ HTTP.put: Check for any case for content-type key.
|
|
111
154
|
3. ~ spec/HTTP/post_spec.rb: + specs for different content-type key cases.
|
|
@@ -115,8 +158,10 @@
|
|
|
115
158
|
7. ~ CHANGELOG.txt: + 0.16.1 entry
|
|
116
159
|
8. ~ http.rb.gemspec: Change date.
|
|
117
160
|
|
|
118
|
-
|
|
119
|
-
|
|
161
|
+
## 20250809
|
|
162
|
+
|
|
163
|
+
0.16.0: Allow passing of a raw payload for POST/PUT.
|
|
164
|
+
|
|
120
165
|
1. ~ HTTP.post: Check for Content-Type and whether a string.
|
|
121
166
|
2. ~ HTTP.put: Check for Content-Type and whether a string.
|
|
122
167
|
3. ~ spec/HTTP/post_spec.rb: + raw form data example
|
|
@@ -127,8 +172,10 @@
|
|
|
127
172
|
8. ~ CHANGELOG.txt: + 0.16.0 entry; Small edit on 0.15.1.
|
|
128
173
|
9. ~ http.rb.gemspec: Change date.
|
|
129
174
|
|
|
130
|
-
|
|
131
|
-
|
|
175
|
+
## 20250721
|
|
176
|
+
|
|
177
|
+
0.15.1: Fix PUT; require delete and put in the load file.
|
|
178
|
+
|
|
132
179
|
1. + require 'HTTP/put'
|
|
133
180
|
2. + require 'HTTP/delete'
|
|
134
181
|
3. ~ HTTP/put.rb: Params in the body!
|
|
@@ -138,8 +185,10 @@
|
|
|
138
185
|
5. ~ CHANGELOG.txt
|
|
139
186
|
6. ~ http.rb.gemspec: Change date.
|
|
140
187
|
|
|
141
|
-
|
|
142
|
-
|
|
188
|
+
## 20250716
|
|
189
|
+
|
|
190
|
+
0.15.0: + PUT
|
|
191
|
+
|
|
143
192
|
1. + HTTP.put
|
|
144
193
|
2. + Net::HTTP::Put#set_headers
|
|
145
194
|
3. + spec/HTTP/put_spec.rb
|
|
@@ -147,8 +196,10 @@
|
|
|
147
196
|
5. ~ CHANGELOG.txt
|
|
148
197
|
6. ~ http.rb.gemspec: Change date.
|
|
149
198
|
|
|
150
|
-
|
|
151
|
-
|
|
199
|
+
## 20250711
|
|
200
|
+
|
|
201
|
+
0.14.0: + DELETE
|
|
202
|
+
|
|
152
203
|
1. + HTTP.delete
|
|
153
204
|
2. + Net::HTTP::Delete#set_headers
|
|
154
205
|
3. + spec/HTTP/delete_spec.rb
|
|
@@ -158,26 +209,34 @@
|
|
|
158
209
|
7. ~ CHANGELOG.txt
|
|
159
210
|
8. ~ http.rb.gemspec: Change date.
|
|
160
211
|
|
|
161
|
-
|
|
162
|
-
|
|
212
|
+
## 20250501
|
|
213
|
+
|
|
214
|
+
0.13.3: Handle when there's a redirect to a URL with arguments, so as to not add an additional '?' at the end.
|
|
215
|
+
|
|
163
216
|
1. ~ HTTP.get: Check if the args hash is empty.
|
|
164
217
|
2. ~ HTTP::VERSION: /0.13.2/0.13.3/
|
|
165
218
|
3. ~ http.rb.gemspec: Change date.
|
|
166
219
|
|
|
167
|
-
|
|
168
|
-
|
|
220
|
+
## 20250330
|
|
221
|
+
|
|
222
|
+
0.13.2: Change repo name to match gem name (/HTTP/http.rb/); + Use HTTP::VERSION; /require/require_relative/
|
|
223
|
+
|
|
169
224
|
1. ~ README.md: /HTTP/http.rb/, /HTTP.rb/http.rb/
|
|
170
225
|
2. + HTTP::VERSION
|
|
171
226
|
3. ~ http.rb.gemspec: Use HTTP::VERSION.
|
|
172
227
|
4. ~ HTTP.get: /require/require_relative/
|
|
173
228
|
5. ~ HTTP.post: /require/require_relative/
|
|
174
229
|
|
|
175
|
-
|
|
176
|
-
|
|
230
|
+
## 20250304
|
|
231
|
+
|
|
232
|
+
0.13.1: /HTTP.rb.gemspec/http.rb.gemspec/
|
|
233
|
+
|
|
177
234
|
1. /HTTP.rb.gemspec/http.rb.gemspec/ (Wonder no more!)
|
|
178
235
|
|
|
179
|
-
|
|
180
|
-
|
|
236
|
+
## 20250304
|
|
237
|
+
|
|
238
|
+
0.13.0: Extend Net::HTTPResponse to allow use of predicate methods for statuses and optionally prevent redirections.
|
|
239
|
+
|
|
181
240
|
1. + lib/Net/HTTPResponse/StatusPredicates.rb
|
|
182
241
|
2. ~ HTTP/get.rb: + require 'Net/HTTPResponse/StatusPredicates'
|
|
183
242
|
3. ~ HTTP/post.rb: + require 'Net/HTTPResponse/StatusPredicates'
|
|
@@ -191,8 +250,10 @@
|
|
|
191
250
|
11. /http.rb.gemspec/HTTP.rb.gemspec/ (I wonder if that's going to cause issues for rubygems.org...)
|
|
192
251
|
12. ~ CHANGELOG.txt
|
|
193
252
|
|
|
194
|
-
|
|
195
|
-
|
|
253
|
+
## 20250207
|
|
254
|
+
|
|
255
|
+
0.12.1: Correctly handle POST'ing JSON data.
|
|
256
|
+
|
|
196
257
|
1. ~ HTTP.post(): Check if the Content-Type is 'application/json' and assign the body JSON data otherwise assign the supplied hash to the form data.
|
|
197
258
|
2. ~ spec/HTTP/post_spec.rb: + spec for when a request is being made with JSON data
|
|
198
259
|
3. ~ spec/HTTP/post_spec.rb: Assign a headers variable to make the spec more readable.
|
data/Rakefile
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Rakefile
|
|
2
2
|
|
|
3
|
-
require '
|
|
3
|
+
require 'rake/testtask'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
t.
|
|
5
|
+
Rake::TestTask.new do |t|
|
|
6
|
+
t.test_files = FileList['test/**/*_test.rb']
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
task default: :
|
|
9
|
+
task default: :test
|
data/http.rb.gemspec
CHANGED
|
@@ -31,13 +31,14 @@ Gem::Specification.new do |spec|
|
|
|
31
31
|
'Rakefile',
|
|
32
32
|
'README.md',
|
|
33
33
|
Dir['lib/**/*.rb'],
|
|
34
|
-
Dir['
|
|
34
|
+
Dir['test/**/*.rb'],
|
|
35
35
|
].flatten
|
|
36
36
|
|
|
37
|
-
spec.development_dependencies =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
spec.development_dependencies = [
|
|
38
|
+
['minitest', '~> 6.0'],
|
|
39
|
+
'minitest-mock',
|
|
40
|
+
'pry',
|
|
41
|
+
'rake',
|
|
42
|
+
'webmock',
|
|
43
|
+
]
|
|
43
44
|
end
|
data/lib/HTTP/RETRY.rb
CHANGED
|
@@ -69,7 +69,8 @@ module HTTP
|
|
|
69
69
|
header.to_i
|
|
70
70
|
else
|
|
71
71
|
# Malformed HTTP-date — fall through to caller's backoff.
|
|
72
|
-
Time.httpdate(header) - Time.now rescue nil
|
|
72
|
+
delta = Time.httpdate(header) - Time.now rescue nil
|
|
73
|
+
delta && [delta, 0].max
|
|
73
74
|
end
|
|
74
75
|
end
|
|
75
76
|
module_function :retry_after
|
data/lib/HTTP/VERSION.rb
CHANGED
data/lib/HTTP/request.rb
CHANGED
|
@@ -17,9 +17,10 @@ module HTTP
|
|
|
17
17
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
18
18
|
no_redirect = options.delete(:no_redirect)
|
|
19
19
|
config = retry_config(options)
|
|
20
|
-
options
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
http.options = options.merge(
|
|
21
|
+
use_ssl: (options[:use_ssl] || uri.use_ssl?),
|
|
22
|
+
verify_mode: (options[:verify_mode] || OpenSSL::SSL::VERIFY_PEER)
|
|
23
|
+
)
|
|
23
24
|
request_object.headers = headers
|
|
24
25
|
request_object.basic_auth(uri.user, uri.password) if uri.user
|
|
25
26
|
verb = request_object.method.downcase.to_sym
|
|
@@ -36,7 +37,7 @@ module HTTP
|
|
|
36
37
|
elsif no_redirect
|
|
37
38
|
return response
|
|
38
39
|
end
|
|
39
|
-
redirect_uri = uri.merge(response
|
|
40
|
+
redirect_uri = uri.merge(response['location'])
|
|
40
41
|
if response.code =~ /^30[78]$/
|
|
41
42
|
data = VERBS::WITH_BODY.include?(verb) ? request_object.body : {}
|
|
42
43
|
response = send(verb, redirect_uri.to_s, data, headers, options, &block)
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# test/HTTP/RETRY_test.rb
|
|
2
|
+
|
|
3
|
+
require_relative '../helper'
|
|
4
|
+
|
|
5
|
+
describe "retry behaviour" do
|
|
6
|
+
let(:uri){'http://example.com/path'}
|
|
7
|
+
|
|
8
|
+
describe "defaults" do
|
|
9
|
+
it "does not retry by default" do
|
|
10
|
+
stub_request(:get, uri).to_return(status: 503)
|
|
11
|
+
response = HTTP.get(uri)
|
|
12
|
+
_(response.code.to_i).must_equal(503)
|
|
13
|
+
assert_requested(:get, uri, times: 1)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "does not retry on a transient exception by default" do
|
|
17
|
+
stub_request(:get, uri).to_raise(Errno::ECONNRESET)
|
|
18
|
+
_(->{HTTP.get(uri)}).must_raise(Errno::ECONNRESET)
|
|
19
|
+
assert_requested(:get, uri, times: 1)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe "retry on transient exception" do
|
|
24
|
+
it "retries and succeeds when the failure is transient" do
|
|
25
|
+
HTTP::RETRY.stub(:sleep, nil) do
|
|
26
|
+
stub_request(:get, uri).
|
|
27
|
+
to_raise(Errno::ECONNRESET).then.
|
|
28
|
+
to_raise(Errno::ECONNRESET).then.
|
|
29
|
+
to_return(status: 200, body: '')
|
|
30
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
31
|
+
_(response.success?).must_equal(true)
|
|
32
|
+
assert_requested(:get, uri, times: 3)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "re-raises the exception after retries are exhausted" do
|
|
37
|
+
HTTP::RETRY.stub(:sleep, nil) do
|
|
38
|
+
stub_request(:get, uri).to_raise(Errno::ECONNRESET)
|
|
39
|
+
_(->{HTTP.get(uri, {}, {}, {retries: 2})}).must_raise(Errno::ECONNRESET)
|
|
40
|
+
assert_requested(:get, uri, times: 3)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "retries on SocketError (DNS failure)" do
|
|
45
|
+
HTTP::RETRY.stub(:sleep, nil) do
|
|
46
|
+
stub_request(:get, uri).
|
|
47
|
+
to_raise(SocketError).then.
|
|
48
|
+
to_return(status: 200, body: '')
|
|
49
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
50
|
+
_(response.success?).must_equal(true)
|
|
51
|
+
assert_requested(:get, uri, times: 2)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "does not retry on a non-listed exception" do
|
|
56
|
+
HTTP::RETRY.stub(:sleep, nil) do
|
|
57
|
+
stub_request(:get, uri).to_raise(OpenSSL::SSL::SSLError)
|
|
58
|
+
_(->{HTTP.get(uri, {}, {}, {retries: 3})}).must_raise(OpenSSL::SSL::SSLError)
|
|
59
|
+
assert_requested(:get, uri, times: 1)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe "retry on status code" do
|
|
65
|
+
it "retries on 503 then succeeds" do
|
|
66
|
+
HTTP::RETRY.stub(:sleep, nil) do
|
|
67
|
+
stub_request(:get, uri).
|
|
68
|
+
to_return({status: 503}, {status: 503}, {status: 200, body: ''})
|
|
69
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
70
|
+
_(response.success?).must_equal(true)
|
|
71
|
+
assert_requested(:get, uri, times: 3)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "retries on 502" do
|
|
76
|
+
HTTP::RETRY.stub(:sleep, nil) do
|
|
77
|
+
stub_request(:get, uri).to_return({status: 502}, {status: 200, body: ''})
|
|
78
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
79
|
+
_(response.success?).must_equal(true)
|
|
80
|
+
assert_requested(:get, uri, times: 2)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "retries on 504" do
|
|
85
|
+
HTTP::RETRY.stub(:sleep, nil) do
|
|
86
|
+
stub_request(:get, uri).to_return({status: 504}, {status: 200, body: ''})
|
|
87
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
88
|
+
_(response.success?).must_equal(true)
|
|
89
|
+
assert_requested(:get, uri, times: 2)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "does not retry on 500 by default" do
|
|
94
|
+
stub_request(:get, uri).to_return(status: 500)
|
|
95
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
96
|
+
_(response.code.to_i).must_equal(500)
|
|
97
|
+
assert_requested(:get, uri, times: 1)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "does not retry on 404" do
|
|
101
|
+
stub_request(:get, uri).to_return(status: 404)
|
|
102
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
103
|
+
_(response.code.to_i).must_equal(404)
|
|
104
|
+
assert_requested(:get, uri, times: 1)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "returns the last response when retries are exhausted" do
|
|
108
|
+
HTTP::RETRY.stub(:sleep, nil) do
|
|
109
|
+
stub_request(:get, uri).to_return(status: 503)
|
|
110
|
+
response = HTTP.get(uri, {}, {}, {retries: 2})
|
|
111
|
+
_(response.code.to_i).must_equal(503)
|
|
112
|
+
assert_requested(:get, uri, times: 3)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
describe "Retry-After header" do
|
|
118
|
+
it "honours integer Retry-After on 429" do
|
|
119
|
+
received_seconds = nil
|
|
120
|
+
HTTP::RETRY.stub(:sleep, ->(n){received_seconds = n}) do
|
|
121
|
+
stub_request(:get, uri).
|
|
122
|
+
to_return({status: 429, headers: {'Retry-After' => '2'}}, {status: 200, body: ''})
|
|
123
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
124
|
+
_(response.success?).must_equal(true)
|
|
125
|
+
_(received_seconds).must_equal(2)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "honours integer Retry-After on 503" do
|
|
130
|
+
received_seconds = nil
|
|
131
|
+
HTTP::RETRY.stub(:sleep, ->(n){received_seconds = n}) do
|
|
132
|
+
stub_request(:get, uri).
|
|
133
|
+
to_return({status: 503, headers: {'Retry-After' => '5'}}, {status: 200, body: ''})
|
|
134
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
135
|
+
_(response.success?).must_equal(true)
|
|
136
|
+
_(received_seconds).must_equal(5)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
describe "configuration" do
|
|
142
|
+
it "treats retries: 0 as no retries" do
|
|
143
|
+
stub_request(:get, uri).to_return(status: 503)
|
|
144
|
+
response = HTTP.get(uri, {}, {}, {retries: 0})
|
|
145
|
+
_(response.code.to_i).must_equal(503)
|
|
146
|
+
assert_requested(:get, uri, times: 1)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it "respects a custom retry_status_codes list" do
|
|
150
|
+
HTTP::RETRY.stub(:sleep, nil) do
|
|
151
|
+
stub_request(:get, uri).to_return({status: 500}, {status: 200, body: ''})
|
|
152
|
+
response = HTTP.get(uri, {}, {}, {retries: 3, retry_status_codes: [500]})
|
|
153
|
+
_(response.success?).must_equal(true)
|
|
154
|
+
assert_requested(:get, uri, times: 2)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "respects a custom retry_exceptions list" do
|
|
159
|
+
HTTP::RETRY.stub(:sleep, nil) do
|
|
160
|
+
stub_request(:get, uri).
|
|
161
|
+
to_raise(OpenSSL::SSL::SSLError).then.
|
|
162
|
+
to_return(status: 200, body: '')
|
|
163
|
+
response = HTTP.get(uri, {}, {}, {retries: 3, retry_exceptions: [OpenSSL::SSL::SSLError]})
|
|
164
|
+
_(response.success?).must_equal(true)
|
|
165
|
+
assert_requested(:get, uri, times: 2)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "does not pass retry options through to Net::HTTP" do
|
|
170
|
+
stub_request(:get, uri).to_return(status: 200, body: '')
|
|
171
|
+
net_http_object = Net::HTTP.new(URI.parse(uri).host, URI.parse(uri).port)
|
|
172
|
+
received_opts = nil
|
|
173
|
+
net_http_object.define_singleton_method(:options=){|opts| received_opts = opts}
|
|
174
|
+
Net::HTTP.stub(:new, net_http_object) do
|
|
175
|
+
HTTP.get(uri, {}, {}, {
|
|
176
|
+
retries: 3,
|
|
177
|
+
retry_delay: 0.1,
|
|
178
|
+
retry_status_codes: [500],
|
|
179
|
+
retry_exceptions: [Errno::ECONNRESET],
|
|
180
|
+
retry_verbs: %i{get}
|
|
181
|
+
})
|
|
182
|
+
end
|
|
183
|
+
_(received_opts).wont_include(:retries)
|
|
184
|
+
_(received_opts).wont_include(:retry_delay)
|
|
185
|
+
_(received_opts).wont_include(:retry_status_codes)
|
|
186
|
+
_(received_opts).wont_include(:retry_exceptions)
|
|
187
|
+
_(received_opts).wont_include(:retry_verbs)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
describe "backoff timing" do
|
|
192
|
+
it "increases the delay between successive retries" do
|
|
193
|
+
delays = []
|
|
194
|
+
HTTP::RETRY.stub(:sleep, ->(d){delays << d}) do
|
|
195
|
+
stub_request(:get, uri).to_return(status: 503)
|
|
196
|
+
HTTP.get(uri, {}, {}, {retries: 3, retry_delay: 1.0})
|
|
197
|
+
end
|
|
198
|
+
_(delays.length).must_equal(3)
|
|
199
|
+
_(delays[1]).must_be(:>, delays[0] * 0.8)
|
|
200
|
+
_(delays[2]).must_be(:>, delays[1] * 0.8)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
describe "verb-based retry default" do
|
|
205
|
+
it "does not retry POST by default even when retries are enabled" do
|
|
206
|
+
stub_request(:post, uri).to_return(status: 503)
|
|
207
|
+
HTTP.post(uri, {}, {}, {retries: 3})
|
|
208
|
+
assert_requested(:post, uri, times: 1)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "does not retry PATCH by default" do
|
|
212
|
+
stub_request(:patch, uri).to_return(status: 503)
|
|
213
|
+
HTTP.patch(uri, {}, {}, {retries: 3})
|
|
214
|
+
assert_requested(:patch, uri, times: 1)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it "retries PUT by default (idempotent)" do
|
|
218
|
+
HTTP::RETRY.stub(:sleep, nil) do
|
|
219
|
+
stub_request(:put, uri).to_return({status: 503}, {status: 200, body: ''})
|
|
220
|
+
response = HTTP.put(uri, {}, {}, {retries: 3})
|
|
221
|
+
_(response.success?).must_equal(true)
|
|
222
|
+
assert_requested(:put, uri, times: 2)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it "retries DELETE by default (idempotent)" do
|
|
227
|
+
HTTP::RETRY.stub(:sleep, nil) do
|
|
228
|
+
stub_request(:delete, uri).to_return({status: 503}, {status: 200, body: ''})
|
|
229
|
+
response = HTTP.delete(uri, {}, {}, {retries: 3})
|
|
230
|
+
_(response.success?).must_equal(true)
|
|
231
|
+
assert_requested(:delete, uri, times: 2)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it "retries POST when opted in via retry_verbs" do
|
|
236
|
+
HTTP::RETRY.stub(:sleep, nil) do
|
|
237
|
+
stub_request(:post, uri).to_return({status: 503}, {status: 200, body: ''})
|
|
238
|
+
response = HTTP.post(uri, {}, {}, {retries: 3, retry_verbs: %i{get post}})
|
|
239
|
+
_(response.success?).must_equal(true)
|
|
240
|
+
assert_requested(:post, uri, times: 2)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
describe HTTP, ".retry_after" do
|
|
247
|
+
it "returns integer seconds for a delta-seconds Retry-After header" do
|
|
248
|
+
response = MockResponse.new(headers_hash: {'Retry-After' => '5'})
|
|
249
|
+
_(HTTP.retry_after(response)).must_equal(5)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
it "parses an HTTP-date Retry-After header" do
|
|
253
|
+
base = Time.utc(2026, 5, 22, 12, 0, 0)
|
|
254
|
+
retry_at_header = (base + 5).httpdate
|
|
255
|
+
response = MockResponse.new(headers_hash: {'Retry-After' => retry_at_header})
|
|
256
|
+
Time.stub(:now, base) do
|
|
257
|
+
_(HTTP.retry_after(response)).must_be_close_to(5.0, 0.001)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
it "returns nil when Retry-After is absent" do
|
|
262
|
+
response = MockResponse.new(headers_hash: {})
|
|
263
|
+
_(HTTP.retry_after(response)).must_be_nil
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
it "returns nil when Retry-After is malformed" do
|
|
267
|
+
response = MockResponse.new(headers_hash: {'Retry-After' => 'not a date'})
|
|
268
|
+
_(HTTP.retry_after(response)).must_be_nil
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
it "clamps to 0 when the Retry-After HTTP-date is in the past" do
|
|
272
|
+
base = Time.utc(2026, 5, 22, 12, 0, 0)
|
|
273
|
+
retry_at_header = (base - 60).httpdate
|
|
274
|
+
response = MockResponse.new(headers_hash: {'Retry-After' => retry_at_header})
|
|
275
|
+
Time.stub(:now, base) do
|
|
276
|
+
_(HTTP.retry_after(response)).must_equal(0)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
it "returns nil for a negative integer Retry-After" do
|
|
281
|
+
response = MockResponse.new(headers_hash: {'Retry-After' => '-5'})
|
|
282
|
+
_(HTTP.retry_after(response)).must_be_nil
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
describe HTTP, ".backoff_delay" do
|
|
287
|
+
it "grows exponentially with attempt number" do
|
|
288
|
+
base = 1.0
|
|
289
|
+
delays = (1..4).map{|attempt| HTTP.backoff_delay(base, attempt)}
|
|
290
|
+
_(delays[0]).must_be_close_to(1.0, 0.2)
|
|
291
|
+
_(delays[1]).must_be_close_to(2.0, 0.4)
|
|
292
|
+
_(delays[2]).must_be_close_to(4.0, 0.8)
|
|
293
|
+
_(delays[3]).must_be_close_to(8.0, 1.6)
|
|
294
|
+
end
|
|
295
|
+
end
|