http.rb 0.21.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 905cbbe9829dc262c67ec3eb4dc5dd37590e377df516ee2550b57c7d2648553e
4
- data.tar.gz: 22983ec7dfd2d28060abe07f3236c183b2da16cd1f49657a1c9b1b5bd6b5c05a
3
+ metadata.gz: 5535114ce1c4c4612aaef5e7eb88777a4807ab671d2f474d38175e3398d37358
4
+ data.tar.gz: e72a5ce3ea83170a4bafc2d6179912d20ff88c8d945d9f8d06f40dae387b6961
5
5
  SHA512:
6
- metadata.gz: 971d00bde4ac4a54010a99332df8264c54f0e737beb98eeb9e76d5ddc9202578c868f643a7153fb07063aacbcc195495ebb8a5703d5bea68f53f6a446dfb6fdc
7
- data.tar.gz: b4de8b52f8bb46a376021c78109649b45820603cb6f1954afb700ab707699157246adabb2296f40510b7e2abf54b99f7c360603b4c109a971f006687921db644
6
+ metadata.gz: cf30ba6e2171e8a005b398cbb467f8437d0653b15f066f610e87bf5d301464695bcd751dbf4b064ded581761c1c2abad25e71418d24d2c4a3001706f1e41f8dc
7
+ data.tar.gz: f62e33d69f7e107c43d717f33bd9130538f0b4ff96a8f337145dfe11fb3ee3dac25ee83fec6777d301fa0eadeb0282760057a58da4736cd2adee0eba47c6552d
data/CHANGELOG CHANGED
@@ -1,15 +1,34 @@
1
1
  # CHANGELOG
2
2
 
3
- # 20260521
4
- # 0.21.0: Add specs for options, trace, and patch verbs.
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
+
5
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.
6
23
  2. + spec/HTTP/trace_spec.rb: Specs for HTTP.trace. Owed since 0.18.0.
7
24
  3. + spec/HTTP/patch_spec.rb: Specs for HTTP.patch. Owed since 0.18.0.
8
25
  4. ~ HTTP::VERSION: /0.20.0/0.21.0/
9
26
  5. ~ CHANGELOG: + 0.21.0 entry
10
27
 
11
- # 20260522
12
- # 0.20.0: Add opt-in retry logic with exponential backoff.
28
+ ## 20260522
29
+
30
+ 0.20.0: Add opt-in retry logic with exponential backoff.
31
+
13
32
  1. + lib/HTTP/RETRY.rb: Retry helpers (with_retries, backoff_delay, retry_after) and constants (HTTP::RETRY::EXCEPTIONS, STATUS_CODES, VERBS).
14
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.
15
34
  3. + spec/HTTP/RETRY_spec.rb: Specs for retry behaviour and the helpers.
@@ -18,8 +37,10 @@
18
37
  6. ~ HTTP::VERSION: /0.19.0/0.20.0/
19
38
  7. ~ CHANGELOG: + 0.20.0 entry
20
39
 
21
- # 20260522
22
- # 0.19.0: Change default verify_mode to VERIFY_PEER.
40
+ ## 20260522
41
+
42
+ 0.19.0: Change default verify_mode to VERIFY_PEER.
43
+
23
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.
24
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.
25
46
  3. ~ spec/HTTP/post_spec.rb: /verify_mode: 0/verify_mode: OpenSSL::SSL::VERIFY_PEER/ in redirect specs.
@@ -29,8 +50,10 @@
29
50
  7. ~ HTTP::VERSION: /0.18.3/0.19.0/
30
51
  8. ~ CHANGELOG: + 0.19.0 entry
31
52
 
32
- # 20260521
33
- # 0.18.3: Fix verb preservation on 307/308 redirects.
53
+ ## 20260521
54
+
55
+ 0.18.3: Fix verb preservation on 307/308 redirects.
56
+
34
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.
35
58
  2. ~ spec/HTTP/post_spec.rb: + specs for 307/308 verb preservation and 307 body preservation.
36
59
  3. ~ spec/HTTP/put_spec.rb: + spec for 307 verb preservation.
@@ -39,8 +62,10 @@
39
62
  6. ~ HTTP::VERSION: /0.18.2/0.18.3/
40
63
  7. ~ CHANGELOG: + 0.18.3 entry
41
64
 
42
- # 20260520
43
- # 0.18.2: Fix relative redirect URL construction.
65
+ ## 20260520
66
+
67
+ 0.18.2: Fix relative redirect URL construction.
68
+
44
69
  1. ~ HTTP.request: Use URI#merge for redirect URL construction. Preserves original scheme, elides default ports, and resolves relative paths per RFC 3986.
45
70
  2. ~ spec/HTTP/get_spec.rb: Update relative-redirect stubs to elide default port; + context for HTTPS relative redirect.
46
71
  3. ~ spec/HTTP/post_spec.rb: Update relative-redirect stubs to elide default port.
@@ -49,8 +74,10 @@
49
74
  6. ~ HTTP::VERSION: /0.18.1/0.18.2/
50
75
  7. ~ CHANGELOG: + 0.18.2 entry
51
76
 
52
- # 20260508
53
- # 0.18.1: Remove incorrectly added WebDAV verbs.
77
+ ## 20260508
78
+
79
+ 0.18.1: Remove incorrectly added WebDAV verbs.
80
+
54
81
  1. - lib/Net/HTTP/Report.rb
55
82
  2. ~ HTTP::VERBS: - require_relative '../Net/HTTP/Report'
56
83
  3. ~ HTTP::VERBS::WITH_BODY: - propfind, proppatch, mkcol, copy, move, lock, unlock, report
@@ -59,8 +86,9 @@
59
86
  6. ~ HTTP::VERSION: /0.18.0/0.18.1/
60
87
  7. ~ CHANGELOG: + 0.18.1 entry
61
88
 
62
- # 20260507
63
- # 0.18.0: Add all missing HTTP verbs; use meta-programming to define verb methods.
89
+ ## 20260507
90
+ 0.18.0: Add all missing HTTP verbs; use meta-programming to define verb methods.
91
+
64
92
  1. + HTTP/verbs.rb; including:
65
93
  + HTTP::VERBS::WITHOUT_BODY: get, delete, head, options, trace
66
94
  + HTTP::VERBS::WITH_BODY: post, put, patch, propfind, proppatch, mkcol, copy, move, lock, unlock, report
@@ -87,8 +115,10 @@
87
115
  22. ~ .gitignore: Using a common one with lots of entries.
88
116
  23. ~ README.md: /http.rb/http/; Trimmed the Description; Usage has more code blocks, making the comments Markdown sub-sections.
89
117
 
90
- # 20260325
91
- # 0.17.0: Extract HTTP.request method; consolidate set_headers.
118
+ ## 20260325
119
+
120
+ 0.17.0: Extract HTTP.request method; consolidate set_headers.
121
+
92
122
  1. + HTTP.request: Extract common plumbing (connection, SSL, auth, redirect, response) from verb methods.
93
123
  2. ~ HTTP.get: Delegate to HTTP.request.
94
124
  3. ~ HTTP.delete: Delegate to HTTP.request.
@@ -104,8 +134,10 @@
104
134
  13. ~ CHANGELOG.txt: + 0.17.0 entry
105
135
  14. ~ http.rb.gemspec: Change date.
106
136
 
107
- # 20250908
108
- # 0.16.1: Allow any case for the content-type key.
137
+ ## 20250908
138
+
139
+ 0.16.1: Allow any case for the content-type key.
140
+
109
141
  1. ~ HTTP.post: Check for any case for content-type key.
110
142
  2. ~ HTTP.put: Check for any case for content-type key.
111
143
  3. ~ spec/HTTP/post_spec.rb: + specs for different content-type key cases.
@@ -115,8 +147,10 @@
115
147
  7. ~ CHANGELOG.txt: + 0.16.1 entry
116
148
  8. ~ http.rb.gemspec: Change date.
117
149
 
118
- # 20250809
119
- # 0.16.0: Allow passing of a raw payload for POST/PUT.
150
+ ## 20250809
151
+
152
+ 0.16.0: Allow passing of a raw payload for POST/PUT.
153
+
120
154
  1. ~ HTTP.post: Check for Content-Type and whether a string.
121
155
  2. ~ HTTP.put: Check for Content-Type and whether a string.
122
156
  3. ~ spec/HTTP/post_spec.rb: + raw form data example
@@ -127,8 +161,10 @@
127
161
  8. ~ CHANGELOG.txt: + 0.16.0 entry; Small edit on 0.15.1.
128
162
  9. ~ http.rb.gemspec: Change date.
129
163
 
130
- # 20250721
131
- # 0.15.1: Fix PUT; require delete and put in the load file.
164
+ ## 20250721
165
+
166
+ 0.15.1: Fix PUT; require delete and put in the load file.
167
+
132
168
  1. + require 'HTTP/put'
133
169
  2. + require 'HTTP/delete'
134
170
  3. ~ HTTP/put.rb: Params in the body!
@@ -138,8 +174,10 @@
138
174
  5. ~ CHANGELOG.txt
139
175
  6. ~ http.rb.gemspec: Change date.
140
176
 
141
- # 20250716
142
- # 0.15.0: + PUT
177
+ ## 20250716
178
+
179
+ 0.15.0: + PUT
180
+
143
181
  1. + HTTP.put
144
182
  2. + Net::HTTP::Put#set_headers
145
183
  3. + spec/HTTP/put_spec.rb
@@ -147,8 +185,10 @@
147
185
  5. ~ CHANGELOG.txt
148
186
  6. ~ http.rb.gemspec: Change date.
149
187
 
150
- # 20250711
151
- # 0.14.0: + DELETE
188
+ ## 20250711
189
+
190
+ 0.14.0: + DELETE
191
+
152
192
  1. + HTTP.delete
153
193
  2. + Net::HTTP::Delete#set_headers
154
194
  3. + spec/HTTP/delete_spec.rb
@@ -158,26 +198,34 @@
158
198
  7. ~ CHANGELOG.txt
159
199
  8. ~ http.rb.gemspec: Change date.
160
200
 
161
- # 20250501
162
- # 0.13.3: Handle when there's a redirect to a URL with arguments, so as to not add an additional '?' at the end.
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
+
163
205
  1. ~ HTTP.get: Check if the args hash is empty.
164
206
  2. ~ HTTP::VERSION: /0.13.2/0.13.3/
165
207
  3. ~ http.rb.gemspec: Change date.
166
208
 
167
- # 202503030
168
- # 0.13.2: Change repo name to match gem name (/HTTP/http.rb/); + Use HTTP::VERSION; /require/require_relative/
209
+ ## 202503030
210
+
211
+ 0.13.2: Change repo name to match gem name (/HTTP/http.rb/); + Use HTTP::VERSION; /require/require_relative/
212
+
169
213
  1. ~ README.md: /HTTP/http.rb/, /HTTP.rb/http.rb/
170
214
  2. + HTTP::VERSION
171
215
  3. ~ http.rb.gemspec: Use HTTP::VERSION.
172
216
  4. ~ HTTP.get: /require/require_relative/
173
217
  5. ~ HTTP.post: /require/require_relative/
174
218
 
175
- # 20250304
176
- # 0.13.1: /HTTP.rb.gemspec/http.rb.gemspec/
219
+ ## 20250304
220
+
221
+ 0.13.1: /HTTP.rb.gemspec/http.rb.gemspec/
222
+
177
223
  1. /HTTP.rb.gemspec/http.rb.gemspec/ (Wonder no more!)
178
224
 
179
- # 20250304
180
- # 0.13.0: Extend Net::HTTPResponse to allow use of predicate methods for statuses and optionally prevent redirections.
225
+ ## 20250304
226
+
227
+ 0.13.0: Extend Net::HTTPResponse to allow use of predicate methods for statuses and optionally prevent redirections.
228
+
181
229
  1. + lib/Net/HTTPResponse/StatusPredicates.rb
182
230
  2. ~ HTTP/get.rb: + require 'Net/HTTPResponse/StatusPredicates'
183
231
  3. ~ HTTP/post.rb: + require 'Net/HTTPResponse/StatusPredicates'
@@ -191,8 +239,10 @@
191
239
  11. /http.rb.gemspec/HTTP.rb.gemspec/ (I wonder if that's going to cause issues for rubygems.org...)
192
240
  12. ~ CHANGELOG.txt
193
241
 
194
- # 20250207
195
- # 0.12.1: Correctly handle POST'ing JSON data.
242
+ ## 20250207
243
+
244
+ 0.12.1: Correctly handle POST'ing JSON data.
245
+
196
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.
197
247
  2. ~ spec/HTTP/post_spec.rb: + spec for when a request is being made with JSON data
198
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 'rspec/core/rake_task'
3
+ require 'rake/testtask'
4
4
 
5
- RSpec::Core::RakeTask.new(:spec) do |t|
6
- t.verbose = false
5
+ Rake::TestTask.new do |t|
6
+ t.test_files = FileList['test/**/*_test.rb']
7
7
  end
8
8
 
9
- task default: :spec
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['spec/**/*.rb'],
34
+ Dir['test/**/*.rb'],
35
35
  ].flatten
36
36
 
37
- spec.development_dependencies = %w{
38
- pry
39
- rake
40
- rspec
41
- webmock
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
@@ -2,5 +2,5 @@
2
2
  # HTTP::VERSION
3
3
 
4
4
  module HTTP
5
- VERSION = '0.21.0'
5
+ VERSION = '0.22.0'
6
6
  end
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.header['location'])
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