http.rb 0.19.0 → 0.20.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 +10 -0
- data/README.md +28 -0
- data/lib/HTTP/RETRY.rb +76 -0
- data/lib/HTTP/VERSION.rb +1 -1
- data/lib/HTTP/request.rb +10 -2
- data/spec/HTTP/RETRY_spec.rb +251 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f8926fd82491288941f05e4ff3d38c0c000e31f214bd35f55462befa0ece73a3
|
|
4
|
+
data.tar.gz: ad696135489b778872649c052fb1f23eb05e151e7a04c29237510e997dba7d71
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eb7f2d8945396de4151166fb3a2fc62a0bb47ac2636a9f157b04c67318f0c9c20d302aba00b8a0db21278fbeed559230b236fea674cb2cf4de3880fe22b09dce
|
|
7
|
+
data.tar.gz: bea5d01ffdab8e4a8fd876ec1b7353776b933d5e1259ea8c57aa4cbf4a8a6b3ba35950fa7bdc907b18bff84e5e7cbc972abbd1cc71398f37934834f176e9d86c
|
data/CHANGELOG
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
# 20260522
|
|
4
|
+
# 0.20.0: Add opt-in retry logic with exponential backoff.
|
|
5
|
+
1. + lib/HTTP/RETRY.rb: Retry helpers (with_retries, backoff_delay, retry_after) and constants (HTTP::RETRY::EXCEPTIONS, STATUS_CODES, VERBS).
|
|
6
|
+
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
|
+
3. + spec/HTTP/RETRY_spec.rb: Specs for retry behaviour and the helpers.
|
|
8
|
+
4. ~ README.md: + Retries section.
|
|
9
|
+
5. ~ TODO: Annotate "Add retry logic to HTTP.request" as (Done as of 0.20.0); also annotate three previously-completed items as (Done as of 0.18.2), (Done as of 0.18.3), and (Done as of 0.19.0).
|
|
10
|
+
6. ~ HTTP::VERSION: /0.19.0/0.20.0/
|
|
11
|
+
7. ~ CHANGELOG: + 0.20.0 entry
|
|
12
|
+
|
|
3
13
|
# 20260522
|
|
4
14
|
# 0.19.0: Change default verify_mode to VERIFY_PEER.
|
|
5
15
|
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.
|
data/README.md
CHANGED
|
@@ -84,6 +84,34 @@ HTTP.get('http://example.com', {}, {}, {no_redirect: true})
|
|
|
84
84
|
# => #<Net::HTTPResponse @code=3xx>
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
### Retries
|
|
88
|
+
|
|
89
|
+
Retries are disabled by default. Enable them by passing `retries:` in the options hash.
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
HTTP.get('http://example.com', {}, {}, {retries: 3})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
When enabled, transient network exceptions and retry-worthy HTTP status codes (429, 502, 503, 504) are retried with exponential backoff and jitter. If the response carries a `Retry-After` header, it is honoured in place of the calculated delay.
|
|
96
|
+
|
|
97
|
+
Only idempotent verbs (`get`, `head`, `options`, `put`, `delete`, `trace`) are retried by default. POST and PATCH are not — retrying a non-idempotent write can create duplicate resources against APIs that don't deduplicate. Opt in per-call via `retry_verbs:`.
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
HTTP.post('http://example.com', {a: 1}, {}, {retries: 3, retry_verbs: %i{get post}})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Configurable options:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
options = {
|
|
107
|
+
retries: 3, # max retry attempts; 0 disables
|
|
108
|
+
retry_delay: 1.0, # base delay (seconds) for exponential backoff
|
|
109
|
+
retry_status_codes: [429, 502, 503, 504], # HTTP status codes to retry
|
|
110
|
+
retry_exceptions: HTTP::RETRY::EXCEPTIONS, # exceptions to retry
|
|
111
|
+
retry_verbs: HTTP::RETRY::VERBS # verbs that retry by default
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
87
115
|
### Response status predicate methods
|
|
88
116
|
|
|
89
117
|
```ruby
|
data/lib/HTTP/RETRY.rb
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# HTTP/RETRY.rb
|
|
2
|
+
# HTTP::RETRY (retry helpers)
|
|
3
|
+
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'socket'
|
|
6
|
+
require 'time'
|
|
7
|
+
|
|
8
|
+
module HTTP
|
|
9
|
+
module RETRY
|
|
10
|
+
EXCEPTIONS = [
|
|
11
|
+
Errno::ECONNREFUSED,
|
|
12
|
+
Errno::ECONNRESET,
|
|
13
|
+
Errno::ETIMEDOUT,
|
|
14
|
+
Errno::EHOSTUNREACH,
|
|
15
|
+
Errno::ENETUNREACH,
|
|
16
|
+
Net::OpenTimeout,
|
|
17
|
+
Net::ReadTimeout,
|
|
18
|
+
SocketError,
|
|
19
|
+
EOFError
|
|
20
|
+
].freeze
|
|
21
|
+
STATUS_CODES = [429, 502, 503, 504].freeze
|
|
22
|
+
VERBS = %i{get head options put delete trace}.freeze
|
|
23
|
+
|
|
24
|
+
def self.sleep(seconds)
|
|
25
|
+
Kernel.sleep(seconds)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def retry_config(options)
|
|
30
|
+
{
|
|
31
|
+
retries: options.delete(:retries) || 0,
|
|
32
|
+
delay: options.delete(:retry_delay) || 1.0,
|
|
33
|
+
status_codes: options.delete(:retry_status_codes) || RETRY::STATUS_CODES,
|
|
34
|
+
exceptions: options.delete(:retry_exceptions) || RETRY::EXCEPTIONS,
|
|
35
|
+
verbs: options.delete(:retry_verbs) || RETRY::VERBS
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
module_function :retry_config
|
|
39
|
+
|
|
40
|
+
def with_retries(http, request_object, config)
|
|
41
|
+
attempt = 0
|
|
42
|
+
loop do
|
|
43
|
+
begin
|
|
44
|
+
response = http.request(request_object)
|
|
45
|
+
if config[:status_codes].include?(response.code.to_i) && attempt < config[:retries]
|
|
46
|
+
attempt += 1
|
|
47
|
+
RETRY.sleep(retry_after(response) || backoff_delay(config[:delay], attempt))
|
|
48
|
+
next
|
|
49
|
+
end
|
|
50
|
+
return response
|
|
51
|
+
rescue *config[:exceptions]
|
|
52
|
+
raise unless attempt < config[:retries]
|
|
53
|
+
attempt += 1
|
|
54
|
+
RETRY.sleep(backoff_delay(config[:delay], attempt))
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
module_function :with_retries
|
|
59
|
+
|
|
60
|
+
def backoff_delay(base, attempt)
|
|
61
|
+
base * (2 ** (attempt - 1)) * (1 + (rand - 0.5) * 0.4)
|
|
62
|
+
end
|
|
63
|
+
module_function :backoff_delay
|
|
64
|
+
|
|
65
|
+
def retry_after(response)
|
|
66
|
+
header = response['Retry-After']
|
|
67
|
+
return nil unless header
|
|
68
|
+
if header =~ /\A\d+\z/
|
|
69
|
+
header.to_i
|
|
70
|
+
else
|
|
71
|
+
# Malformed HTTP-date — fall through to caller's backoff.
|
|
72
|
+
Time.httpdate(header) - Time.now rescue nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
module_function :retry_after
|
|
76
|
+
end
|
data/lib/HTTP/VERSION.rb
CHANGED
data/lib/HTTP/request.rb
CHANGED
|
@@ -9,18 +9,27 @@ require_relative '../Net/HTTP/set_options'
|
|
|
9
9
|
require_relative '../Net/HTTPRequest/set_headers'
|
|
10
10
|
require_relative '../Net/HTTPResponse/StatusPredicates'
|
|
11
11
|
require_relative '../URI/Generic/use_sslQ'
|
|
12
|
+
require_relative './RETRY'
|
|
12
13
|
|
|
13
14
|
module HTTP
|
|
14
15
|
def request(uri, request_object, headers = {}, options = {}, &block)
|
|
15
16
|
uri = uri.is_a?(URI) ? uri : URI.parse(uri)
|
|
16
17
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
17
18
|
no_redirect = options.delete(:no_redirect)
|
|
19
|
+
config = retry_config(options)
|
|
18
20
|
options[:use_ssl] ||= uri.use_ssl?
|
|
19
21
|
options[:verify_mode] ||= OpenSSL::SSL::VERIFY_PEER
|
|
20
22
|
http.options = options
|
|
21
23
|
request_object.headers = headers
|
|
22
24
|
request_object.basic_auth(uri.user, uri.password) if uri.user
|
|
23
|
-
|
|
25
|
+
verb = request_object.method.downcase.to_sym
|
|
26
|
+
response = (
|
|
27
|
+
if config[:retries] > 0 && config[:verbs].include?(verb)
|
|
28
|
+
with_retries(http, request_object, config)
|
|
29
|
+
else
|
|
30
|
+
http.request(request_object)
|
|
31
|
+
end
|
|
32
|
+
)
|
|
24
33
|
if response.code =~ /^3/
|
|
25
34
|
if block_given? && no_redirect
|
|
26
35
|
yield response
|
|
@@ -29,7 +38,6 @@ module HTTP
|
|
|
29
38
|
end
|
|
30
39
|
redirect_uri = uri.merge(response.header['location'])
|
|
31
40
|
if response.code =~ /^30[78]$/
|
|
32
|
-
verb = request_object.method.downcase.to_sym
|
|
33
41
|
data = VERBS::WITH_BODY.include?(verb) ? request_object.body : {}
|
|
34
42
|
response = send(verb, redirect_uri.to_s, data, headers, options, &block)
|
|
35
43
|
else
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# spec/HTTP/RETRY_spec.rb
|
|
2
|
+
|
|
3
|
+
require_relative '../spec_helper'
|
|
4
|
+
require 'http'
|
|
5
|
+
|
|
6
|
+
describe "retry behaviour" do
|
|
7
|
+
let(:uri){'http://example.com/path'}
|
|
8
|
+
|
|
9
|
+
before do
|
|
10
|
+
allow(HTTP::RETRY).to receive(:sleep)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe "defaults" do
|
|
14
|
+
it "does not retry by default" do
|
|
15
|
+
stub_request(:get, uri).to_return(status: 503)
|
|
16
|
+
response = HTTP.get(uri)
|
|
17
|
+
expect(response.code.to_i).to eq(503)
|
|
18
|
+
expect(WebMock).to have_requested(:get, uri).times(1)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "does not retry on a transient exception by default" do
|
|
22
|
+
stub_request(:get, uri).to_raise(Errno::ECONNRESET)
|
|
23
|
+
expect{HTTP.get(uri)}.to raise_error(Errno::ECONNRESET)
|
|
24
|
+
expect(WebMock).to have_requested(:get, uri).times(1)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe "retry on transient exception" do
|
|
29
|
+
it "retries and succeeds when the failure is transient" do
|
|
30
|
+
stub_request(:get, uri).
|
|
31
|
+
to_raise(Errno::ECONNRESET).then.
|
|
32
|
+
to_raise(Errno::ECONNRESET).then.
|
|
33
|
+
to_return(status: 200, body: '')
|
|
34
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
35
|
+
expect(response.success?).to eq(true)
|
|
36
|
+
expect(WebMock).to have_requested(:get, uri).times(3)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "re-raises the exception after retries are exhausted" do
|
|
40
|
+
stub_request(:get, uri).to_raise(Errno::ECONNRESET)
|
|
41
|
+
expect{HTTP.get(uri, {}, {}, {retries: 2})}.to raise_error(Errno::ECONNRESET)
|
|
42
|
+
expect(WebMock).to have_requested(:get, uri).times(3)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "retries on SocketError (DNS failure)" 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
|
+
expect(response.success?).to eq(true)
|
|
51
|
+
expect(WebMock).to have_requested(:get, uri).times(2)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "does not retry on a non-listed exception" do
|
|
55
|
+
stub_request(:get, uri).to_raise(OpenSSL::SSL::SSLError)
|
|
56
|
+
expect{HTTP.get(uri, {}, {}, {retries: 3})}.to raise_error(OpenSSL::SSL::SSLError)
|
|
57
|
+
expect(WebMock).to have_requested(:get, uri).times(1)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
describe "retry on status code" do
|
|
62
|
+
it "retries on 503 then succeeds" do
|
|
63
|
+
stub_request(:get, uri).
|
|
64
|
+
to_return({status: 503}, {status: 503}, {status: 200, body: ''})
|
|
65
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
66
|
+
expect(response.success?).to eq(true)
|
|
67
|
+
expect(WebMock).to have_requested(:get, uri).times(3)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "retries on 502" do
|
|
71
|
+
stub_request(:get, uri).to_return({status: 502}, {status: 200, body: ''})
|
|
72
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
73
|
+
expect(response.success?).to eq(true)
|
|
74
|
+
expect(WebMock).to have_requested(:get, uri).times(2)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "retries on 504" do
|
|
78
|
+
stub_request(:get, uri).to_return({status: 504}, {status: 200, body: ''})
|
|
79
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
80
|
+
expect(response.success?).to eq(true)
|
|
81
|
+
expect(WebMock).to have_requested(:get, uri).times(2)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "does not retry on 500 by default" do
|
|
85
|
+
stub_request(:get, uri).to_return(status: 500)
|
|
86
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
87
|
+
expect(response.code.to_i).to eq(500)
|
|
88
|
+
expect(WebMock).to have_requested(:get, uri).times(1)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "does not retry on 404" do
|
|
92
|
+
stub_request(:get, uri).to_return(status: 404)
|
|
93
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
94
|
+
expect(response.code.to_i).to eq(404)
|
|
95
|
+
expect(WebMock).to have_requested(:get, uri).times(1)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "returns the last response when retries are exhausted" do
|
|
99
|
+
stub_request(:get, uri).to_return(status: 503)
|
|
100
|
+
response = HTTP.get(uri, {}, {}, {retries: 2})
|
|
101
|
+
expect(response.code.to_i).to eq(503)
|
|
102
|
+
expect(WebMock).to have_requested(:get, uri).times(3)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
describe "Retry-After header" do
|
|
107
|
+
it "honours integer Retry-After on 429" do
|
|
108
|
+
stub_request(:get, uri).
|
|
109
|
+
to_return({status: 429, headers: {'Retry-After' => '2'}}, {status: 200, body: ''})
|
|
110
|
+
expect(HTTP::RETRY).to receive(:sleep).with(2)
|
|
111
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
112
|
+
expect(response.success?).to eq(true)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "honours integer Retry-After on 503" do
|
|
116
|
+
stub_request(:get, uri).
|
|
117
|
+
to_return({status: 503, headers: {'Retry-After' => '5'}}, {status: 200, body: ''})
|
|
118
|
+
expect(HTTP::RETRY).to receive(:sleep).with(5)
|
|
119
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
120
|
+
expect(response.success?).to eq(true)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
describe "configuration" do
|
|
125
|
+
it "treats retries: 0 as no retries" do
|
|
126
|
+
stub_request(:get, uri).to_return(status: 503)
|
|
127
|
+
response = HTTP.get(uri, {}, {}, {retries: 0})
|
|
128
|
+
expect(response.code.to_i).to eq(503)
|
|
129
|
+
expect(WebMock).to have_requested(:get, uri).times(1)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "respects a custom retry_status_codes list" do
|
|
133
|
+
stub_request(:get, uri).to_return({status: 500}, {status: 200, body: ''})
|
|
134
|
+
response = HTTP.get(uri, {}, {}, {retries: 3, retry_status_codes: [500]})
|
|
135
|
+
expect(response.success?).to eq(true)
|
|
136
|
+
expect(WebMock).to have_requested(:get, uri).times(2)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "respects a custom retry_exceptions list" do
|
|
140
|
+
stub_request(:get, uri).
|
|
141
|
+
to_raise(OpenSSL::SSL::SSLError).then.
|
|
142
|
+
to_return(status: 200, body: '')
|
|
143
|
+
response = HTTP.get(uri, {}, {}, {retries: 3, retry_exceptions: [OpenSSL::SSL::SSLError]})
|
|
144
|
+
expect(response.success?).to eq(true)
|
|
145
|
+
expect(WebMock).to have_requested(:get, uri).times(2)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "does not pass retry options through to Net::HTTP" do
|
|
149
|
+
stub_request(:get, uri).to_return(status: 200, body: '')
|
|
150
|
+
net_http_object = Net::HTTP.new(URI.parse(uri).host, URI.parse(uri).port)
|
|
151
|
+
allow(Net::HTTP).to receive(:new).and_return(net_http_object)
|
|
152
|
+
expect(net_http_object).to receive(:options=) do |opts|
|
|
153
|
+
expect(opts).not_to include(:retries, :retry_delay, :retry_status_codes, :retry_exceptions, :retry_verbs)
|
|
154
|
+
end
|
|
155
|
+
HTTP.get(uri, {}, {}, {
|
|
156
|
+
retries: 3,
|
|
157
|
+
retry_delay: 0.1,
|
|
158
|
+
retry_status_codes: [500],
|
|
159
|
+
retry_exceptions: [Errno::ECONNRESET],
|
|
160
|
+
retry_verbs: %i{get}
|
|
161
|
+
})
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
describe "backoff timing" do
|
|
166
|
+
it "increases the delay between successive retries" do
|
|
167
|
+
delays = []
|
|
168
|
+
allow(HTTP::RETRY).to receive(:sleep){|d| delays << d}
|
|
169
|
+
stub_request(:get, uri).to_return(status: 503)
|
|
170
|
+
HTTP.get(uri, {}, {}, {retries: 3, retry_delay: 1.0})
|
|
171
|
+
expect(delays.length).to eq(3)
|
|
172
|
+
expect(delays[1]).to be > delays[0] * 0.8
|
|
173
|
+
expect(delays[2]).to be > delays[1] * 0.8
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
describe "verb-based retry default" do
|
|
178
|
+
it "does not retry POST by default even when retries are enabled" do
|
|
179
|
+
stub_request(:post, uri).to_return(status: 503)
|
|
180
|
+
HTTP.post(uri, {}, {}, {retries: 3})
|
|
181
|
+
expect(WebMock).to have_requested(:post, uri).times(1)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it "does not retry PATCH by default" do
|
|
185
|
+
stub_request(:patch, uri).to_return(status: 503)
|
|
186
|
+
HTTP.patch(uri, {}, {}, {retries: 3})
|
|
187
|
+
expect(WebMock).to have_requested(:patch, uri).times(1)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it "retries PUT by default (idempotent)" do
|
|
191
|
+
stub_request(:put, uri).to_return({status: 503}, {status: 200, body: ''})
|
|
192
|
+
response = HTTP.put(uri, {}, {}, {retries: 3})
|
|
193
|
+
expect(response.success?).to eq(true)
|
|
194
|
+
expect(WebMock).to have_requested(:put, uri).times(2)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it "retries DELETE by default (idempotent)" do
|
|
198
|
+
stub_request(:delete, uri).to_return({status: 503}, {status: 200, body: ''})
|
|
199
|
+
response = HTTP.delete(uri, {}, {}, {retries: 3})
|
|
200
|
+
expect(response.success?).to eq(true)
|
|
201
|
+
expect(WebMock).to have_requested(:delete, uri).times(2)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it "retries POST when opted in via retry_verbs" do
|
|
205
|
+
stub_request(:post, uri).to_return({status: 503}, {status: 200, body: ''})
|
|
206
|
+
response = HTTP.post(uri, {}, {}, {retries: 3, retry_verbs: %i{get post}})
|
|
207
|
+
expect(response.success?).to eq(true)
|
|
208
|
+
expect(WebMock).to have_requested(:post, uri).times(2)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
describe HTTP, ".retry_after" do
|
|
214
|
+
it "returns integer seconds for a delta-seconds Retry-After header" do
|
|
215
|
+
response = instance_double(Net::HTTPResponse)
|
|
216
|
+
allow(response).to receive(:[]).with('Retry-After').and_return('5')
|
|
217
|
+
expect(HTTP.retry_after(response)).to eq(5)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it "parses an HTTP-date Retry-After header" do
|
|
221
|
+
base = Time.utc(2026, 5, 22, 12, 0, 0)
|
|
222
|
+
retry_at_header = (base + 5).httpdate
|
|
223
|
+
response = instance_double(Net::HTTPResponse)
|
|
224
|
+
allow(response).to receive(:[]).with('Retry-After').and_return(retry_at_header)
|
|
225
|
+
allow(Time).to receive(:now).and_return(base)
|
|
226
|
+
expect(HTTP.retry_after(response)).to be_within(0.001).of(5.0)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it "returns nil when Retry-After is absent" do
|
|
230
|
+
response = instance_double(Net::HTTPResponse)
|
|
231
|
+
allow(response).to receive(:[]).with('Retry-After').and_return(nil)
|
|
232
|
+
expect(HTTP.retry_after(response)).to be_nil
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it "returns nil when Retry-After is malformed" do
|
|
236
|
+
response = instance_double(Net::HTTPResponse)
|
|
237
|
+
allow(response).to receive(:[]).with('Retry-After').and_return('not a date')
|
|
238
|
+
expect(HTTP.retry_after(response)).to be_nil
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
describe HTTP, ".backoff_delay" do
|
|
243
|
+
it "grows exponentially with attempt number" do
|
|
244
|
+
base = 1.0
|
|
245
|
+
delays = (1..4).map{|attempt| HTTP.backoff_delay(base, attempt)}
|
|
246
|
+
expect(delays[0]).to be_within(0.2).of(1.0)
|
|
247
|
+
expect(delays[1]).to be_within(0.4).of(2.0)
|
|
248
|
+
expect(delays[2]).to be_within(0.8).of(4.0)
|
|
249
|
+
expect(delays[3]).to be_within(1.6).of(8.0)
|
|
250
|
+
end
|
|
251
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: http.rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.20.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- thoran
|
|
@@ -80,6 +80,7 @@ files:
|
|
|
80
80
|
- Rakefile
|
|
81
81
|
- http.rb.gemspec
|
|
82
82
|
- lib/HTTP.rb
|
|
83
|
+
- lib/HTTP/RETRY.rb
|
|
83
84
|
- lib/HTTP/VERSION.rb
|
|
84
85
|
- lib/HTTP/request.rb
|
|
85
86
|
- lib/HTTP/verbs.rb
|
|
@@ -93,6 +94,7 @@ files:
|
|
|
93
94
|
- lib/Thoran/Array/FirstX/firstX.rb
|
|
94
95
|
- lib/Thoran/String/ToConst/to_const.rb
|
|
95
96
|
- lib/URI/Generic/use_sslQ.rb
|
|
97
|
+
- spec/HTTP/RETRY_spec.rb
|
|
96
98
|
- spec/HTTP/delete_spec.rb
|
|
97
99
|
- spec/HTTP/get_spec.rb
|
|
98
100
|
- spec/HTTP/head_spec.rb
|