http 5.2.0 → 5.3.1
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/.rubocop/rspec.yml +9 -0
- data/.rubocop_todo.yml +45 -32
- data/CHANGELOG.md +27 -1
- data/Gemfile +1 -0
- data/http.gemspec +2 -2
- data/lib/http/base64.rb +12 -0
- data/lib/http/chainable.rb +27 -3
- data/lib/http/connection.rb +4 -2
- data/lib/http/errors.rb +16 -0
- data/lib/http/feature.rb +2 -1
- data/lib/http/features/raise_error.rb +22 -0
- data/lib/http/headers/normalizer.rb +69 -0
- data/lib/http/headers.rb +26 -40
- data/lib/http/request/writer.rb +2 -1
- data/lib/http/request.rb +3 -2
- data/lib/http/retriable/client.rb +37 -0
- data/lib/http/retriable/delay_calculator.rb +64 -0
- data/lib/http/retriable/errors.rb +14 -0
- data/lib/http/retriable/performer.rb +153 -0
- data/lib/http/version.rb +1 -1
- data/lib/http.rb +1 -0
- data/spec/lib/http/features/raise_error_spec.rb +62 -0
- data/spec/lib/http/headers/normalizer_spec.rb +52 -0
- data/spec/lib/http/redirector_spec.rb +6 -5
- data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
- data/spec/lib/http/retriable/performer_spec.rb +302 -0
- data/spec/lib/http_spec.rb +29 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/dummy_server/servlet.rb +13 -0
- data/spec/support/dummy_server.rb +2 -1
- metadata +20 -18
@@ -0,0 +1,302 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe HTTP::Retriable::Performer do
|
4
|
+
let(:client) do
|
5
|
+
HTTP::Client.new
|
6
|
+
end
|
7
|
+
|
8
|
+
let(:response) do
|
9
|
+
HTTP::Response.new(
|
10
|
+
status: 200,
|
11
|
+
version: "1.1",
|
12
|
+
headers: {},
|
13
|
+
body: "Hello world!",
|
14
|
+
request: request
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
let(:request) do
|
19
|
+
HTTP::Request.new(
|
20
|
+
verb: :get,
|
21
|
+
uri: "http://example.com"
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
let(:perform_spy) { { counter: 0 } }
|
26
|
+
let(:counter_spy) { perform_spy[:counter] }
|
27
|
+
|
28
|
+
before do
|
29
|
+
stub_const("CustomException", Class.new(StandardError))
|
30
|
+
end
|
31
|
+
|
32
|
+
def perform(options = {}, client_arg = client, request_arg = request, &block)
|
33
|
+
# by explicitly overwriting the default delay, we make a much faster test suite
|
34
|
+
default_options = { delay: 0 }
|
35
|
+
options = default_options.merge(options)
|
36
|
+
|
37
|
+
HTTP::Retriable::Performer
|
38
|
+
.new(options)
|
39
|
+
.perform(client_arg, request_arg) do
|
40
|
+
perform_spy[:counter] += 1
|
41
|
+
block ? yield : response
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def measure_wait
|
46
|
+
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
47
|
+
result = yield
|
48
|
+
t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
49
|
+
[t2 - t1, result]
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#perform" do
|
53
|
+
describe "expected exception" do
|
54
|
+
it "retries the request" do
|
55
|
+
expect do
|
56
|
+
perform(exceptions: [CustomException], tries: 2) do
|
57
|
+
raise CustomException
|
58
|
+
end
|
59
|
+
end.to raise_error HTTP::OutOfRetriesError
|
60
|
+
|
61
|
+
expect(counter_spy).to eq 2
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "unexpected exception" do
|
66
|
+
it "does not retry the request" do
|
67
|
+
expect do
|
68
|
+
perform(exceptions: [], tries: 2) do
|
69
|
+
raise CustomException
|
70
|
+
end
|
71
|
+
end.to raise_error CustomException
|
72
|
+
|
73
|
+
expect(counter_spy).to eq 1
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe "expected status codes" do
|
78
|
+
def response(**options)
|
79
|
+
HTTP::Response.new(
|
80
|
+
{
|
81
|
+
status: 200,
|
82
|
+
version: "1.1",
|
83
|
+
headers: {},
|
84
|
+
body: "Hello world!",
|
85
|
+
request: request
|
86
|
+
}.merge(options)
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
it "retries the request" do
|
91
|
+
expect do
|
92
|
+
perform(retry_statuses: [200], tries: 2)
|
93
|
+
end.to raise_error HTTP::OutOfRetriesError
|
94
|
+
|
95
|
+
expect(counter_spy).to eq 2
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "status codes can be expressed in many ways" do
|
99
|
+
[
|
100
|
+
301,
|
101
|
+
[200, 301, 485],
|
102
|
+
250...400,
|
103
|
+
[250...Float::INFINITY],
|
104
|
+
->(status_code) { status_code == 301 },
|
105
|
+
[->(status_code) { status_code == 301 }]
|
106
|
+
].each do |retry_statuses|
|
107
|
+
it retry_statuses.to_s do
|
108
|
+
expect do
|
109
|
+
perform(retry_statuses: retry_statuses, tries: 2) do
|
110
|
+
response(status: 301)
|
111
|
+
end
|
112
|
+
end.to raise_error HTTP::OutOfRetriesError
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "unexpected status code" do
|
119
|
+
it "does not retry the request" do
|
120
|
+
expect(
|
121
|
+
perform(retry_statuses: [], tries: 2)
|
122
|
+
).to eq response
|
123
|
+
|
124
|
+
expect(counter_spy).to eq 1
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe "on_retry callback" do
|
129
|
+
it "calls the on_retry callback on each retry with exception" do
|
130
|
+
callback_call_spy = 0
|
131
|
+
|
132
|
+
callback_spy = proc do |callback_request, error, callback_response|
|
133
|
+
expect(callback_request).to eq request
|
134
|
+
expect(error).to be_a HTTP::TimeoutError
|
135
|
+
expect(callback_response).to be_nil
|
136
|
+
callback_call_spy += 1
|
137
|
+
end
|
138
|
+
|
139
|
+
expect do
|
140
|
+
perform(tries: 3, on_retry: callback_spy) do
|
141
|
+
raise HTTP::TimeoutError
|
142
|
+
end
|
143
|
+
end.to raise_error HTTP::OutOfRetriesError
|
144
|
+
|
145
|
+
expect(callback_call_spy).to eq 2
|
146
|
+
end
|
147
|
+
|
148
|
+
it "calls the on_retry callback on each retry with response" do
|
149
|
+
callback_call_spy = 0
|
150
|
+
|
151
|
+
callback_spy = proc do |callback_request, error, callback_response|
|
152
|
+
expect(callback_request).to eq request
|
153
|
+
expect(error).to be_nil
|
154
|
+
expect(callback_response).to be response
|
155
|
+
callback_call_spy += 1
|
156
|
+
end
|
157
|
+
|
158
|
+
expect do
|
159
|
+
perform(retry_statuses: [200], tries: 3, on_retry: callback_spy)
|
160
|
+
end.to raise_error HTTP::OutOfRetriesError
|
161
|
+
|
162
|
+
expect(callback_call_spy).to eq 2
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
describe "delay option" do
|
167
|
+
let(:timing_slack) { 0.05 }
|
168
|
+
|
169
|
+
it "can be a positive number" do
|
170
|
+
time, = measure_wait do
|
171
|
+
perform(delay: 0.1, tries: 3, should_retry: ->(*) { true })
|
172
|
+
rescue HTTP::OutOfRetriesError
|
173
|
+
end
|
174
|
+
expect(time).to be_within(timing_slack).of(0.2)
|
175
|
+
end
|
176
|
+
|
177
|
+
it "can be a proc number" do
|
178
|
+
time, = measure_wait do
|
179
|
+
perform(delay: ->(attempt) { attempt / 10.0 }, tries: 3, should_retry: ->(*) { true })
|
180
|
+
rescue HTTP::OutOfRetriesError
|
181
|
+
end
|
182
|
+
expect(time).to be_within(timing_slack).of(0.3)
|
183
|
+
end
|
184
|
+
|
185
|
+
it "receives correct retry number when a proc" do
|
186
|
+
retry_count = 0
|
187
|
+
retry_proc = proc do |attempt|
|
188
|
+
expect(attempt).to eq(retry_count).and(be > 0)
|
189
|
+
0
|
190
|
+
end
|
191
|
+
begin
|
192
|
+
perform(delay: retry_proc, should_retry: ->(*) { true }) do
|
193
|
+
retry_count += 1
|
194
|
+
response
|
195
|
+
end
|
196
|
+
rescue HTTP::OutOfRetriesError
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
describe "should_retry option" do
|
202
|
+
it "decides if the request should be retried" do # rubocop:disable RSpec/MultipleExpectations
|
203
|
+
retry_proc = proc do |req, err, res, attempt|
|
204
|
+
expect(req).to eq request
|
205
|
+
if res
|
206
|
+
expect(err).to be_nil
|
207
|
+
expect(res).to be response
|
208
|
+
else
|
209
|
+
expect(err).to be_a CustomException
|
210
|
+
expect(res).to be_nil
|
211
|
+
end
|
212
|
+
|
213
|
+
attempt < 5
|
214
|
+
end
|
215
|
+
|
216
|
+
begin
|
217
|
+
perform(should_retry: retry_proc) do
|
218
|
+
rand < 0.5 ? response : raise(CustomException)
|
219
|
+
end
|
220
|
+
rescue CustomException
|
221
|
+
end
|
222
|
+
|
223
|
+
expect(counter_spy).to eq 5
|
224
|
+
end
|
225
|
+
|
226
|
+
it "raises the original error if not retryable" do
|
227
|
+
retry_proc = ->(*) { false }
|
228
|
+
|
229
|
+
expect do
|
230
|
+
perform(should_retry: retry_proc) do
|
231
|
+
raise CustomException
|
232
|
+
end
|
233
|
+
end.to raise_error CustomException
|
234
|
+
|
235
|
+
expect(counter_spy).to eq 1
|
236
|
+
end
|
237
|
+
|
238
|
+
it "raises HTTP::OutOfRetriesError if retryable" do
|
239
|
+
retry_proc = ->(*) { true }
|
240
|
+
|
241
|
+
expect do
|
242
|
+
perform(should_retry: retry_proc) do
|
243
|
+
raise CustomException
|
244
|
+
end
|
245
|
+
end.to raise_error HTTP::OutOfRetriesError
|
246
|
+
|
247
|
+
expect(counter_spy).to eq 5
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
describe "connection closing" do
|
253
|
+
let(:client) { double(:client) }
|
254
|
+
|
255
|
+
it "does not close the connection if we get a propper response" do
|
256
|
+
expect(client).not_to receive(:close)
|
257
|
+
perform
|
258
|
+
end
|
259
|
+
|
260
|
+
it "closes the connection after each raiseed attempt" do
|
261
|
+
expect(client).to receive(:close).exactly(3).times
|
262
|
+
begin
|
263
|
+
perform(should_retry: ->(*) { true }, tries: 3)
|
264
|
+
rescue HTTP::OutOfRetriesError
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
it "closes the connection on an unexpected exception" do
|
269
|
+
expect(client).to receive(:close)
|
270
|
+
begin
|
271
|
+
perform do
|
272
|
+
raise CustomException
|
273
|
+
end
|
274
|
+
rescue CustomException
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
describe HTTP::OutOfRetriesError do
|
280
|
+
it "has the original exception as a cause if available" do
|
281
|
+
err = nil
|
282
|
+
begin
|
283
|
+
perform(exceptions: [CustomException]) do
|
284
|
+
raise CustomException
|
285
|
+
end
|
286
|
+
rescue described_class => e
|
287
|
+
err = e
|
288
|
+
end
|
289
|
+
expect(err.cause).to be_a CustomException
|
290
|
+
end
|
291
|
+
|
292
|
+
it "has the last raiseed response as an attribute" do
|
293
|
+
err = nil
|
294
|
+
begin
|
295
|
+
perform(should_retry: ->(*) { true })
|
296
|
+
rescue described_class => e
|
297
|
+
err = e
|
298
|
+
end
|
299
|
+
expect(err.response).to be response
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
data/spec/lib/http_spec.rb
CHANGED
@@ -152,6 +152,35 @@ RSpec.describe HTTP do
|
|
152
152
|
end
|
153
153
|
end
|
154
154
|
|
155
|
+
describe ".retry" do
|
156
|
+
it "ensure endpoint counts retries" do
|
157
|
+
expect(HTTP.get("#{dummy.endpoint}/retry-2").to_s).to eq "retried 1x"
|
158
|
+
expect(HTTP.get("#{dummy.endpoint}/retry-2").to_s).to eq "retried 2x"
|
159
|
+
end
|
160
|
+
|
161
|
+
it "retries the request" do
|
162
|
+
response = HTTP.retriable(delay: 0, retry_statuses: 500...600).get "#{dummy.endpoint}/retry-2"
|
163
|
+
expect(response.to_s).to eq "retried 2x"
|
164
|
+
end
|
165
|
+
|
166
|
+
it "retries the request and gives us access to the failed requests" do
|
167
|
+
err = nil
|
168
|
+
retry_callback = ->(_, _, res) { expect(res.to_s).to match(/^retried \dx$/) }
|
169
|
+
begin
|
170
|
+
HTTP.retriable(
|
171
|
+
should_retry: ->(*) { true },
|
172
|
+
tries: 3,
|
173
|
+
delay: 0,
|
174
|
+
on_retry: retry_callback
|
175
|
+
).get "#{dummy.endpoint}/retry-2"
|
176
|
+
rescue HTTP::Error => e
|
177
|
+
err = e
|
178
|
+
end
|
179
|
+
|
180
|
+
expect(err.response.to_s).to eq "retried 3x"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
155
184
|
context "posting forms to resources" do
|
156
185
|
it "is easy" do
|
157
186
|
response = HTTP.post "#{dummy.endpoint}/form", :form => {:example => "testing-form"}
|
data/spec/spec_helper.rb
CHANGED
@@ -18,6 +18,11 @@ class DummyServer < WEBrick::HTTPServer
|
|
18
18
|
@handlers ||= {}
|
19
19
|
end
|
20
20
|
|
21
|
+
def initialize(server, memo)
|
22
|
+
super(server)
|
23
|
+
@memo = memo
|
24
|
+
end
|
25
|
+
|
21
26
|
%w[get post head].each do |method|
|
22
27
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
23
28
|
def self.#{method}(path, &block)
|
@@ -186,5 +191,13 @@ class DummyServer < WEBrick::HTTPServer
|
|
186
191
|
res["Content-Encoding"] = "deflate"
|
187
192
|
end
|
188
193
|
end
|
194
|
+
|
195
|
+
get "/retry-2" do |_req, res|
|
196
|
+
@memo[:attempts] ||= 0
|
197
|
+
@memo[:attempts] += 1
|
198
|
+
|
199
|
+
res.body = "retried #{@memo[:attempts]}x"
|
200
|
+
res.status = @memo[:attempts] == 2 ? 200 : 500
|
201
|
+
end
|
189
202
|
end
|
190
203
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: http
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.
|
4
|
+
version: 5.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tony Arcieri
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date:
|
14
|
+
date: 2025-06-09 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: addressable
|
@@ -27,20 +27,6 @@ dependencies:
|
|
27
27
|
- - "~>"
|
28
28
|
- !ruby/object:Gem::Version
|
29
29
|
version: '2.8'
|
30
|
-
- !ruby/object:Gem::Dependency
|
31
|
-
name: base64
|
32
|
-
requirement: !ruby/object:Gem::Requirement
|
33
|
-
requirements:
|
34
|
-
- - "~>"
|
35
|
-
- !ruby/object:Gem::Version
|
36
|
-
version: '0.1'
|
37
|
-
type: :runtime
|
38
|
-
prerelease: false
|
39
|
-
version_requirements: !ruby/object:Gem::Requirement
|
40
|
-
requirements:
|
41
|
-
- - "~>"
|
42
|
-
- !ruby/object:Gem::Version
|
43
|
-
version: '0.1'
|
44
30
|
- !ruby/object:Gem::Dependency
|
45
31
|
name: http-cookie
|
46
32
|
requirement: !ruby/object:Gem::Requirement
|
@@ -111,6 +97,7 @@ files:
|
|
111
97
|
- ".rubocop.yml"
|
112
98
|
- ".rubocop/layout.yml"
|
113
99
|
- ".rubocop/metrics.yml"
|
100
|
+
- ".rubocop/rspec.yml"
|
114
101
|
- ".rubocop/style.yml"
|
115
102
|
- ".rubocop_todo.yml"
|
116
103
|
- ".yardopts"
|
@@ -125,6 +112,7 @@ files:
|
|
125
112
|
- SECURITY.md
|
126
113
|
- http.gemspec
|
127
114
|
- lib/http.rb
|
115
|
+
- lib/http/base64.rb
|
128
116
|
- lib/http/chainable.rb
|
129
117
|
- lib/http/client.rb
|
130
118
|
- lib/http/connection.rb
|
@@ -136,9 +124,11 @@ files:
|
|
136
124
|
- lib/http/features/instrumentation.rb
|
137
125
|
- lib/http/features/logging.rb
|
138
126
|
- lib/http/features/normalize_uri.rb
|
127
|
+
- lib/http/features/raise_error.rb
|
139
128
|
- lib/http/headers.rb
|
140
129
|
- lib/http/headers/known.rb
|
141
130
|
- lib/http/headers/mixin.rb
|
131
|
+
- lib/http/headers/normalizer.rb
|
142
132
|
- lib/http/mime_type.rb
|
143
133
|
- lib/http/mime_type/adapter.rb
|
144
134
|
- lib/http/mime_type/json.rb
|
@@ -153,6 +143,10 @@ files:
|
|
153
143
|
- lib/http/response/parser.rb
|
154
144
|
- lib/http/response/status.rb
|
155
145
|
- lib/http/response/status/reasons.rb
|
146
|
+
- lib/http/retriable/client.rb
|
147
|
+
- lib/http/retriable/delay_calculator.rb
|
148
|
+
- lib/http/retriable/errors.rb
|
149
|
+
- lib/http/retriable/performer.rb
|
156
150
|
- lib/http/timeout/global.rb
|
157
151
|
- lib/http/timeout/null.rb
|
158
152
|
- lib/http/timeout/per_operation.rb
|
@@ -166,7 +160,9 @@ files:
|
|
166
160
|
- spec/lib/http/features/auto_inflate_spec.rb
|
167
161
|
- spec/lib/http/features/instrumentation_spec.rb
|
168
162
|
- spec/lib/http/features/logging_spec.rb
|
163
|
+
- spec/lib/http/features/raise_error_spec.rb
|
169
164
|
- spec/lib/http/headers/mixin_spec.rb
|
165
|
+
- spec/lib/http/headers/normalizer_spec.rb
|
170
166
|
- spec/lib/http/headers_spec.rb
|
171
167
|
- spec/lib/http/options/body_spec.rb
|
172
168
|
- spec/lib/http/options/features_spec.rb
|
@@ -185,6 +181,8 @@ files:
|
|
185
181
|
- spec/lib/http/response/parser_spec.rb
|
186
182
|
- spec/lib/http/response/status_spec.rb
|
187
183
|
- spec/lib/http/response_spec.rb
|
184
|
+
- spec/lib/http/retriable/delay_calculator_spec.rb
|
185
|
+
- spec/lib/http/retriable/performer_spec.rb
|
188
186
|
- spec/lib/http/uri/normalizer_spec.rb
|
189
187
|
- spec/lib/http/uri_spec.rb
|
190
188
|
- spec/lib/http_spec.rb
|
@@ -209,7 +207,7 @@ metadata:
|
|
209
207
|
source_code_uri: https://github.com/httprb/http
|
210
208
|
wiki_uri: https://github.com/httprb/http/wiki
|
211
209
|
bug_tracker_uri: https://github.com/httprb/http/issues
|
212
|
-
changelog_uri: https://github.com/httprb/http/blob/v5.
|
210
|
+
changelog_uri: https://github.com/httprb/http/blob/v5.3.1/CHANGELOG.md
|
213
211
|
rubygems_mfa_required: 'true'
|
214
212
|
post_install_message:
|
215
213
|
rdoc_options: []
|
@@ -226,7 +224,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
226
224
|
- !ruby/object:Gem::Version
|
227
225
|
version: '0'
|
228
226
|
requirements: []
|
229
|
-
rubygems_version: 3.
|
227
|
+
rubygems_version: 3.4.19
|
230
228
|
signing_key:
|
231
229
|
specification_version: 4
|
232
230
|
summary: HTTP should be easy
|
@@ -238,7 +236,9 @@ test_files:
|
|
238
236
|
- spec/lib/http/features/auto_inflate_spec.rb
|
239
237
|
- spec/lib/http/features/instrumentation_spec.rb
|
240
238
|
- spec/lib/http/features/logging_spec.rb
|
239
|
+
- spec/lib/http/features/raise_error_spec.rb
|
241
240
|
- spec/lib/http/headers/mixin_spec.rb
|
241
|
+
- spec/lib/http/headers/normalizer_spec.rb
|
242
242
|
- spec/lib/http/headers_spec.rb
|
243
243
|
- spec/lib/http/options/body_spec.rb
|
244
244
|
- spec/lib/http/options/features_spec.rb
|
@@ -257,6 +257,8 @@ test_files:
|
|
257
257
|
- spec/lib/http/response/parser_spec.rb
|
258
258
|
- spec/lib/http/response/status_spec.rb
|
259
259
|
- spec/lib/http/response_spec.rb
|
260
|
+
- spec/lib/http/retriable/delay_calculator_spec.rb
|
261
|
+
- spec/lib/http/retriable/performer_spec.rb
|
260
262
|
- spec/lib/http/uri/normalizer_spec.rb
|
261
263
|
- spec/lib/http/uri_spec.rb
|
262
264
|
- spec/lib/http_spec.rb
|