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.
@@ -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
@@ -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
@@ -5,6 +5,7 @@ require_relative "./support/fuubar" unless ENV["CI"]
5
5
 
6
6
  require "http"
7
7
  require "rspec/its"
8
+ require "rspec/memory"
8
9
  require "support/capture_warning"
9
10
  require "support/fakeio"
10
11
 
@@ -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
@@ -26,7 +26,8 @@ class DummyServer < WEBrick::HTTPServer
26
26
 
27
27
  def initialize(options = {})
28
28
  super(options[:ssl] ? SSL_CONFIG : CONFIG)
29
- mount("/", Servlet)
29
+ @memo = {}
30
+ mount("/", Servlet, @memo)
30
31
  end
31
32
 
32
33
  def endpoint
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.2.0
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: 2024-02-05 00:00:00.000000000 Z
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.2.0/CHANGELOG.md
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.5.4
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