http.rb 0.20.0 → 0.22.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 +92 -34
- data/Rakefile +4 -4
- data/http.rb.gemspec +8 -7
- data/lib/HTTP/VERSION.rb +1 -1
- data/lib/HTTP/request.rb +1 -1
- data/test/HTTP/RETRY_test.rb +281 -0
- data/test/HTTP/delete_test.rb +265 -0
- data/test/HTTP/get_test.rb +304 -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 -11
- 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/post_spec.rb +0 -447
- data/spec/HTTP/put_spec.rb +0 -406
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5535114ce1c4c4612aaef5e7eb88777a4807ab671d2f474d38175e3398d37358
|
|
4
|
+
data.tar.gz: e72a5ce3ea83170a4bafc2d6179912d20ff88c8d945d9f8d06f40dae387b6961
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cf30ba6e2171e8a005b398cbb467f8437d0653b15f066f610e87bf5d301464695bcd751dbf4b064ded581761c1c2abad25e71418d24d2c4a3001706f1e41f8dc
|
|
7
|
+
data.tar.gz: f62e33d69f7e107c43d717f33bd9130538f0b4ff96a8f337145dfe11fb3ee3dac25ee83fec6777d301fa0eadeb0282760057a58da4736cd2adee0eba47c6552d
|
data/CHANGELOG
CHANGED
|
@@ -1,7 +1,34 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
## 20260522
|
|
4
|
+
|
|
5
|
+
0.22.0: Convert specs from RSpec to Minitest.
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
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).
|
|
9
|
+
3. ~ Rakefile: Use Rake::TestTask instead of RSpec::Core::RakeTask.
|
|
10
|
+
4. ~ http.rb.gemspec: -rspec; +minitest; +minitest-mock (minitest 6 extracted Mock/stub into a separate gem); files glob /spec/test/.
|
|
11
|
+
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.
|
|
12
|
+
6. ~ TODO: Annotate "Convert specs from RSpec to Minitest" as (Done as of 0.22.0).
|
|
13
|
+
7. ~ HTTP::VERSION: /0.21.0/0.22.0/
|
|
14
|
+
8. ~ CHANGELOG: + 0.22.0 entry
|
|
15
|
+
9. ~ CHANGELOG: Reformat all entries with Markdown-style headings — `## YYYYMMDD` date headings, version line no longer `#`-prefixed.
|
|
16
|
+
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.
|
|
17
|
+
|
|
18
|
+
## 20260522
|
|
19
|
+
|
|
20
|
+
0.21.0: Add specs for options, trace, and patch verbs.
|
|
21
|
+
|
|
22
|
+
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.
|
|
23
|
+
2. + spec/HTTP/trace_spec.rb: Specs for HTTP.trace. Owed since 0.18.0.
|
|
24
|
+
3. + spec/HTTP/patch_spec.rb: Specs for HTTP.patch. Owed since 0.18.0.
|
|
25
|
+
4. ~ HTTP::VERSION: /0.20.0/0.21.0/
|
|
26
|
+
5. ~ CHANGELOG: + 0.21.0 entry
|
|
27
|
+
|
|
28
|
+
## 20260522
|
|
29
|
+
|
|
30
|
+
0.20.0: Add opt-in retry logic with exponential backoff.
|
|
31
|
+
|
|
5
32
|
1. + lib/HTTP/RETRY.rb: Retry helpers (with_retries, backoff_delay, retry_after) and constants (HTTP::RETRY::EXCEPTIONS, STATUS_CODES, VERBS).
|
|
6
33
|
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.
|
|
7
34
|
3. + spec/HTTP/RETRY_spec.rb: Specs for retry behaviour and the helpers.
|
|
@@ -10,8 +37,10 @@
|
|
|
10
37
|
6. ~ HTTP::VERSION: /0.19.0/0.20.0/
|
|
11
38
|
7. ~ CHANGELOG: + 0.20.0 entry
|
|
12
39
|
|
|
13
|
-
|
|
14
|
-
|
|
40
|
+
## 20260522
|
|
41
|
+
|
|
42
|
+
0.19.0: Change default verify_mode to VERIFY_PEER.
|
|
43
|
+
|
|
15
44
|
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.
|
|
16
45
|
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.
|
|
17
46
|
3. ~ spec/HTTP/post_spec.rb: /verify_mode: 0/verify_mode: OpenSSL::SSL::VERIFY_PEER/ in redirect specs.
|
|
@@ -21,8 +50,10 @@
|
|
|
21
50
|
7. ~ HTTP::VERSION: /0.18.3/0.19.0/
|
|
22
51
|
8. ~ CHANGELOG: + 0.19.0 entry
|
|
23
52
|
|
|
24
|
-
|
|
25
|
-
|
|
53
|
+
## 20260521
|
|
54
|
+
|
|
55
|
+
0.18.3: Fix verb preservation on 307/308 redirects.
|
|
56
|
+
|
|
26
57
|
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.
|
|
27
58
|
2. ~ spec/HTTP/post_spec.rb: + specs for 307/308 verb preservation and 307 body preservation.
|
|
28
59
|
3. ~ spec/HTTP/put_spec.rb: + spec for 307 verb preservation.
|
|
@@ -31,8 +62,10 @@
|
|
|
31
62
|
6. ~ HTTP::VERSION: /0.18.2/0.18.3/
|
|
32
63
|
7. ~ CHANGELOG: + 0.18.3 entry
|
|
33
64
|
|
|
34
|
-
|
|
35
|
-
|
|
65
|
+
## 20260520
|
|
66
|
+
|
|
67
|
+
0.18.2: Fix relative redirect URL construction.
|
|
68
|
+
|
|
36
69
|
1. ~ HTTP.request: Use URI#merge for redirect URL construction. Preserves original scheme, elides default ports, and resolves relative paths per RFC 3986.
|
|
37
70
|
2. ~ spec/HTTP/get_spec.rb: Update relative-redirect stubs to elide default port; + context for HTTPS relative redirect.
|
|
38
71
|
3. ~ spec/HTTP/post_spec.rb: Update relative-redirect stubs to elide default port.
|
|
@@ -41,8 +74,10 @@
|
|
|
41
74
|
6. ~ HTTP::VERSION: /0.18.1/0.18.2/
|
|
42
75
|
7. ~ CHANGELOG: + 0.18.2 entry
|
|
43
76
|
|
|
44
|
-
|
|
45
|
-
|
|
77
|
+
## 20260508
|
|
78
|
+
|
|
79
|
+
0.18.1: Remove incorrectly added WebDAV verbs.
|
|
80
|
+
|
|
46
81
|
1. - lib/Net/HTTP/Report.rb
|
|
47
82
|
2. ~ HTTP::VERBS: - require_relative '../Net/HTTP/Report'
|
|
48
83
|
3. ~ HTTP::VERBS::WITH_BODY: - propfind, proppatch, mkcol, copy, move, lock, unlock, report
|
|
@@ -51,8 +86,9 @@
|
|
|
51
86
|
6. ~ HTTP::VERSION: /0.18.0/0.18.1/
|
|
52
87
|
7. ~ CHANGELOG: + 0.18.1 entry
|
|
53
88
|
|
|
54
|
-
|
|
55
|
-
|
|
89
|
+
## 20260507
|
|
90
|
+
0.18.0: Add all missing HTTP verbs; use meta-programming to define verb methods.
|
|
91
|
+
|
|
56
92
|
1. + HTTP/verbs.rb; including:
|
|
57
93
|
+ HTTP::VERBS::WITHOUT_BODY: get, delete, head, options, trace
|
|
58
94
|
+ HTTP::VERBS::WITH_BODY: post, put, patch, propfind, proppatch, mkcol, copy, move, lock, unlock, report
|
|
@@ -79,8 +115,10 @@
|
|
|
79
115
|
22. ~ .gitignore: Using a common one with lots of entries.
|
|
80
116
|
23. ~ README.md: /http.rb/http/; Trimmed the Description; Usage has more code blocks, making the comments Markdown sub-sections.
|
|
81
117
|
|
|
82
|
-
|
|
83
|
-
|
|
118
|
+
## 20260325
|
|
119
|
+
|
|
120
|
+
0.17.0: Extract HTTP.request method; consolidate set_headers.
|
|
121
|
+
|
|
84
122
|
1. + HTTP.request: Extract common plumbing (connection, SSL, auth, redirect, response) from verb methods.
|
|
85
123
|
2. ~ HTTP.get: Delegate to HTTP.request.
|
|
86
124
|
3. ~ HTTP.delete: Delegate to HTTP.request.
|
|
@@ -96,8 +134,10 @@
|
|
|
96
134
|
13. ~ CHANGELOG.txt: + 0.17.0 entry
|
|
97
135
|
14. ~ http.rb.gemspec: Change date.
|
|
98
136
|
|
|
99
|
-
|
|
100
|
-
|
|
137
|
+
## 20250908
|
|
138
|
+
|
|
139
|
+
0.16.1: Allow any case for the content-type key.
|
|
140
|
+
|
|
101
141
|
1. ~ HTTP.post: Check for any case for content-type key.
|
|
102
142
|
2. ~ HTTP.put: Check for any case for content-type key.
|
|
103
143
|
3. ~ spec/HTTP/post_spec.rb: + specs for different content-type key cases.
|
|
@@ -107,8 +147,10 @@
|
|
|
107
147
|
7. ~ CHANGELOG.txt: + 0.16.1 entry
|
|
108
148
|
8. ~ http.rb.gemspec: Change date.
|
|
109
149
|
|
|
110
|
-
|
|
111
|
-
|
|
150
|
+
## 20250809
|
|
151
|
+
|
|
152
|
+
0.16.0: Allow passing of a raw payload for POST/PUT.
|
|
153
|
+
|
|
112
154
|
1. ~ HTTP.post: Check for Content-Type and whether a string.
|
|
113
155
|
2. ~ HTTP.put: Check for Content-Type and whether a string.
|
|
114
156
|
3. ~ spec/HTTP/post_spec.rb: + raw form data example
|
|
@@ -119,8 +161,10 @@
|
|
|
119
161
|
8. ~ CHANGELOG.txt: + 0.16.0 entry; Small edit on 0.15.1.
|
|
120
162
|
9. ~ http.rb.gemspec: Change date.
|
|
121
163
|
|
|
122
|
-
|
|
123
|
-
|
|
164
|
+
## 20250721
|
|
165
|
+
|
|
166
|
+
0.15.1: Fix PUT; require delete and put in the load file.
|
|
167
|
+
|
|
124
168
|
1. + require 'HTTP/put'
|
|
125
169
|
2. + require 'HTTP/delete'
|
|
126
170
|
3. ~ HTTP/put.rb: Params in the body!
|
|
@@ -130,8 +174,10 @@
|
|
|
130
174
|
5. ~ CHANGELOG.txt
|
|
131
175
|
6. ~ http.rb.gemspec: Change date.
|
|
132
176
|
|
|
133
|
-
|
|
134
|
-
|
|
177
|
+
## 20250716
|
|
178
|
+
|
|
179
|
+
0.15.0: + PUT
|
|
180
|
+
|
|
135
181
|
1. + HTTP.put
|
|
136
182
|
2. + Net::HTTP::Put#set_headers
|
|
137
183
|
3. + spec/HTTP/put_spec.rb
|
|
@@ -139,8 +185,10 @@
|
|
|
139
185
|
5. ~ CHANGELOG.txt
|
|
140
186
|
6. ~ http.rb.gemspec: Change date.
|
|
141
187
|
|
|
142
|
-
|
|
143
|
-
|
|
188
|
+
## 20250711
|
|
189
|
+
|
|
190
|
+
0.14.0: + DELETE
|
|
191
|
+
|
|
144
192
|
1. + HTTP.delete
|
|
145
193
|
2. + Net::HTTP::Delete#set_headers
|
|
146
194
|
3. + spec/HTTP/delete_spec.rb
|
|
@@ -150,26 +198,34 @@
|
|
|
150
198
|
7. ~ CHANGELOG.txt
|
|
151
199
|
8. ~ http.rb.gemspec: Change date.
|
|
152
200
|
|
|
153
|
-
|
|
154
|
-
|
|
201
|
+
## 20250501
|
|
202
|
+
|
|
203
|
+
0.13.3: Handle when there's a redirect to a URL with arguments, so as to not add an additional '?' at the end.
|
|
204
|
+
|
|
155
205
|
1. ~ HTTP.get: Check if the args hash is empty.
|
|
156
206
|
2. ~ HTTP::VERSION: /0.13.2/0.13.3/
|
|
157
207
|
3. ~ http.rb.gemspec: Change date.
|
|
158
208
|
|
|
159
|
-
|
|
160
|
-
|
|
209
|
+
## 202503030
|
|
210
|
+
|
|
211
|
+
0.13.2: Change repo name to match gem name (/HTTP/http.rb/); + Use HTTP::VERSION; /require/require_relative/
|
|
212
|
+
|
|
161
213
|
1. ~ README.md: /HTTP/http.rb/, /HTTP.rb/http.rb/
|
|
162
214
|
2. + HTTP::VERSION
|
|
163
215
|
3. ~ http.rb.gemspec: Use HTTP::VERSION.
|
|
164
216
|
4. ~ HTTP.get: /require/require_relative/
|
|
165
217
|
5. ~ HTTP.post: /require/require_relative/
|
|
166
218
|
|
|
167
|
-
|
|
168
|
-
|
|
219
|
+
## 20250304
|
|
220
|
+
|
|
221
|
+
0.13.1: /HTTP.rb.gemspec/http.rb.gemspec/
|
|
222
|
+
|
|
169
223
|
1. /HTTP.rb.gemspec/http.rb.gemspec/ (Wonder no more!)
|
|
170
224
|
|
|
171
|
-
|
|
172
|
-
|
|
225
|
+
## 20250304
|
|
226
|
+
|
|
227
|
+
0.13.0: Extend Net::HTTPResponse to allow use of predicate methods for statuses and optionally prevent redirections.
|
|
228
|
+
|
|
173
229
|
1. + lib/Net/HTTPResponse/StatusPredicates.rb
|
|
174
230
|
2. ~ HTTP/get.rb: + require 'Net/HTTPResponse/StatusPredicates'
|
|
175
231
|
3. ~ HTTP/post.rb: + require 'Net/HTTPResponse/StatusPredicates'
|
|
@@ -183,8 +239,10 @@
|
|
|
183
239
|
11. /http.rb.gemspec/HTTP.rb.gemspec/ (I wonder if that's going to cause issues for rubygems.org...)
|
|
184
240
|
12. ~ CHANGELOG.txt
|
|
185
241
|
|
|
186
|
-
|
|
187
|
-
|
|
242
|
+
## 20250207
|
|
243
|
+
|
|
244
|
+
0.12.1: Correctly handle POST'ing JSON data.
|
|
245
|
+
|
|
188
246
|
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.
|
|
189
247
|
2. ~ spec/HTTP/post_spec.rb: + spec for when a request is being made with JSON data
|
|
190
248
|
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/VERSION.rb
CHANGED
data/lib/HTTP/request.rb
CHANGED
|
@@ -36,7 +36,7 @@ module HTTP
|
|
|
36
36
|
elsif no_redirect
|
|
37
37
|
return response
|
|
38
38
|
end
|
|
39
|
-
redirect_uri = uri.merge(response
|
|
39
|
+
redirect_uri = uri.merge(response['location'])
|
|
40
40
|
if response.code =~ /^30[78]$/
|
|
41
41
|
data = VERBS::WITH_BODY.include?(verb) ? request_object.body : {}
|
|
42
42
|
response = send(verb, redirect_uri.to_s, data, headers, options, &block)
|
|
@@ -0,0 +1,281 @@
|
|
|
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
|
+
end
|
|
271
|
+
|
|
272
|
+
describe HTTP, ".backoff_delay" do
|
|
273
|
+
it "grows exponentially with attempt number" do
|
|
274
|
+
base = 1.0
|
|
275
|
+
delays = (1..4).map{|attempt| HTTP.backoff_delay(base, attempt)}
|
|
276
|
+
_(delays[0]).must_be_close_to(1.0, 0.2)
|
|
277
|
+
_(delays[1]).must_be_close_to(2.0, 0.4)
|
|
278
|
+
_(delays[2]).must_be_close_to(4.0, 0.8)
|
|
279
|
+
_(delays[3]).must_be_close_to(8.0, 1.6)
|
|
280
|
+
end
|
|
281
|
+
end
|