http 3.1.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.
Files changed (93) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +67 -0
  3. data/.gitignore +6 -9
  4. data/.rspec +0 -4
  5. data/.rubocop/layout.yml +8 -0
  6. data/.rubocop/metrics.yml +4 -0
  7. data/.rubocop/rspec.yml +9 -0
  8. data/.rubocop/style.yml +32 -0
  9. data/.rubocop.yml +9 -108
  10. data/.rubocop_todo.yml +219 -0
  11. data/.yardopts +1 -1
  12. data/CHANGELOG.md +67 -0
  13. data/{CHANGES.md → CHANGES_OLD.md} +358 -0
  14. data/Gemfile +19 -10
  15. data/LICENSE.txt +1 -1
  16. data/README.md +53 -85
  17. data/Rakefile +3 -11
  18. data/SECURITY.md +17 -0
  19. data/http.gemspec +15 -6
  20. data/lib/http/base64.rb +12 -0
  21. data/lib/http/chainable.rb +71 -41
  22. data/lib/http/client.rb +73 -52
  23. data/lib/http/connection.rb +28 -18
  24. data/lib/http/content_type.rb +12 -7
  25. data/lib/http/errors.rb +19 -0
  26. data/lib/http/feature.rb +18 -1
  27. data/lib/http/features/auto_deflate.rb +27 -6
  28. data/lib/http/features/auto_inflate.rb +32 -6
  29. data/lib/http/features/instrumentation.rb +69 -0
  30. data/lib/http/features/logging.rb +53 -0
  31. data/lib/http/features/normalize_uri.rb +17 -0
  32. data/lib/http/features/raise_error.rb +22 -0
  33. data/lib/http/headers/known.rb +3 -0
  34. data/lib/http/headers/normalizer.rb +69 -0
  35. data/lib/http/headers.rb +72 -49
  36. data/lib/http/mime_type/adapter.rb +3 -1
  37. data/lib/http/mime_type/json.rb +1 -0
  38. data/lib/http/options.rb +31 -28
  39. data/lib/http/redirector.rb +56 -4
  40. data/lib/http/request/body.rb +31 -0
  41. data/lib/http/request/writer.rb +29 -9
  42. data/lib/http/request.rb +76 -41
  43. data/lib/http/response/body.rb +6 -4
  44. data/lib/http/response/inflater.rb +1 -1
  45. data/lib/http/response/parser.rb +78 -26
  46. data/lib/http/response/status.rb +4 -3
  47. data/lib/http/response.rb +45 -27
  48. data/lib/http/retriable/client.rb +37 -0
  49. data/lib/http/retriable/delay_calculator.rb +64 -0
  50. data/lib/http/retriable/errors.rb +14 -0
  51. data/lib/http/retriable/performer.rb +153 -0
  52. data/lib/http/timeout/global.rb +29 -47
  53. data/lib/http/timeout/null.rb +12 -8
  54. data/lib/http/timeout/per_operation.rb +32 -57
  55. data/lib/http/uri.rb +75 -1
  56. data/lib/http/version.rb +1 -1
  57. data/lib/http.rb +2 -2
  58. data/spec/lib/http/client_spec.rb +189 -36
  59. data/spec/lib/http/connection_spec.rb +31 -6
  60. data/spec/lib/http/features/auto_inflate_spec.rb +40 -23
  61. data/spec/lib/http/features/instrumentation_spec.rb +81 -0
  62. data/spec/lib/http/features/logging_spec.rb +65 -0
  63. data/spec/lib/http/features/raise_error_spec.rb +62 -0
  64. data/spec/lib/http/headers/normalizer_spec.rb +52 -0
  65. data/spec/lib/http/headers_spec.rb +53 -18
  66. data/spec/lib/http/options/headers_spec.rb +6 -2
  67. data/spec/lib/http/options/merge_spec.rb +16 -16
  68. data/spec/lib/http/redirector_spec.rb +147 -3
  69. data/spec/lib/http/request/body_spec.rb +71 -4
  70. data/spec/lib/http/request/writer_spec.rb +45 -2
  71. data/spec/lib/http/request_spec.rb +11 -5
  72. data/spec/lib/http/response/body_spec.rb +5 -5
  73. data/spec/lib/http/response/parser_spec.rb +74 -0
  74. data/spec/lib/http/response/status_spec.rb +3 -3
  75. data/spec/lib/http/response_spec.rb +83 -7
  76. data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
  77. data/spec/lib/http/retriable/performer_spec.rb +302 -0
  78. data/spec/lib/http/uri/normalizer_spec.rb +95 -0
  79. data/spec/lib/http/uri_spec.rb +39 -0
  80. data/spec/lib/http_spec.rb +121 -68
  81. data/spec/regression_specs.rb +7 -0
  82. data/spec/spec_helper.rb +22 -21
  83. data/spec/support/black_hole.rb +1 -1
  84. data/spec/support/dummy_server/servlet.rb +42 -11
  85. data/spec/support/dummy_server.rb +9 -8
  86. data/spec/support/fuubar.rb +21 -0
  87. data/spec/support/http_handling_shared.rb +62 -66
  88. data/spec/support/simplecov.rb +19 -0
  89. data/spec/support/ssl_helper.rb +4 -4
  90. metadata +66 -27
  91. data/.coveralls.yml +0 -1
  92. data/.ruby-version +0 -1
  93. data/.travis.yml +0 -36
@@ -1,5 +1,5 @@
1
- # frozen_string_literal: true
2
1
  # coding: utf-8
2
+ # frozen_string_literal: true
3
3
 
4
4
  RSpec.describe HTTP::Request do
5
5
  let(:proxy) { {} }
@@ -8,10 +8,10 @@ RSpec.describe HTTP::Request do
8
8
 
9
9
  subject :request do
10
10
  HTTP::Request.new(
11
- :verb => :get,
12
- :uri => request_uri,
13
- :headers => headers,
14
- :proxy => proxy
11
+ :verb => :get,
12
+ :uri => request_uri,
13
+ :headers => headers,
14
+ :proxy => proxy
15
15
  )
16
16
  end
17
17
 
@@ -225,4 +225,10 @@ RSpec.describe HTTP::Request do
225
225
  end
226
226
  end
227
227
  end
228
+
229
+ describe "#inspect" do
230
+ subject { request.inspect }
231
+
232
+ it { is_expected.to eq "#<HTTP::Request/1.1 GET #{request_uri}>" }
233
+ end
228
234
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  RSpec.describe HTTP::Response::Body do
4
4
  let(:connection) { double(:sequence_id => 0) }
5
- let(:chunks) { [String.new("Hello, "), String.new("World!")] }
5
+ let(:chunks) { ["Hello, ", "World!"] }
6
6
 
7
7
  before do
8
8
  allow(connection).to receive(:readpartial) { chunks.shift }
@@ -16,7 +16,7 @@ RSpec.describe HTTP::Response::Body do
16
16
  end
17
17
 
18
18
  context "when body empty" do
19
- let(:chunks) { [String.new("")] }
19
+ let(:chunks) { [""] }
20
20
 
21
21
  it "returns responds to empty? with true" do
22
22
  expect(subject).to be_empty
@@ -45,12 +45,12 @@ RSpec.describe HTTP::Response::Body do
45
45
  it "returns content in specified encoding" do
46
46
  body = described_class.new(connection)
47
47
  expect(connection).to receive(:readpartial).
48
- and_return(String.new("content").force_encoding(Encoding::UTF_8))
48
+ and_return(String.new("content", :encoding => Encoding::UTF_8))
49
49
  expect(body.readpartial.encoding).to eq Encoding::BINARY
50
50
 
51
51
  body = described_class.new(connection, :encoding => Encoding::UTF_8)
52
52
  expect(connection).to receive(:readpartial).
53
- and_return(String.new("content").force_encoding(Encoding::BINARY))
53
+ and_return(String.new("content", :encoding => Encoding::BINARY))
54
54
  expect(body.readpartial.encoding).to eq Encoding::UTF_8
55
55
  end
56
56
  end
@@ -59,7 +59,7 @@ RSpec.describe HTTP::Response::Body do
59
59
  let(:chunks) do
60
60
  body = Zlib::Deflate.deflate("Hi, HTTP here ☺")
61
61
  len = body.length
62
- [String.new(body[0, len / 2]), String.new(body[(len / 2)..-1])]
62
+ [body[0, len / 2], body[(len / 2)..]]
63
63
  end
64
64
  subject(:body) do
65
65
  inflater = HTTP::Response::Inflater.new(connection)
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe HTTP::Response::Parser do
4
+ subject(:parser) { described_class.new }
5
+ let(:raw_response) do
6
+ "HTTP/1.1 200 OK\r\nContent-Length: 2\r\nContent-Type: application/json\r\nMyHeader: val\r\nEmptyHeader: \r\n\r\n{}"
7
+ end
8
+ let(:expected_headers) do
9
+ {
10
+ "Content-Length" => "2",
11
+ "Content-Type" => "application/json",
12
+ "MyHeader" => "val",
13
+ "EmptyHeader" => ""
14
+ }
15
+ end
16
+ let(:expected_body) { "{}" }
17
+
18
+ before do
19
+ parts.each { |part| subject.add(part) }
20
+ end
21
+
22
+ context "whole response in one part" do
23
+ let(:parts) { [raw_response] }
24
+
25
+ it "parses headers" do
26
+ expect(subject.headers.to_h).to eq(expected_headers)
27
+ end
28
+
29
+ it "parses body" do
30
+ expect(subject.read(expected_body.size)).to eq(expected_body)
31
+ end
32
+ end
33
+
34
+ context "response in many parts" do
35
+ let(:parts) { raw_response.chars }
36
+
37
+ it "parses headers" do
38
+ expect(subject.headers.to_h).to eq(expected_headers)
39
+ end
40
+
41
+ it "parses body" do
42
+ expect(subject.read(expected_body.size)).to eq(expected_body)
43
+ end
44
+ end
45
+
46
+ context "when got 100 Continue response" do
47
+ let :raw_response do
48
+ "HTTP/1.1 100 Continue\r\n\r\n" \
49
+ "HTTP/1.1 200 OK\r\n" \
50
+ "Content-Length: 12\r\n\r\n" \
51
+ "Hello World!"
52
+ end
53
+
54
+ context "when response is feeded in one part" do
55
+ let(:parts) { [raw_response] }
56
+
57
+ it "skips to next non-info response" do
58
+ expect(subject.status_code).to eq(200)
59
+ expect(subject.headers).to eq("Content-Length" => "12")
60
+ expect(subject.read(12)).to eq("Hello World!")
61
+ end
62
+ end
63
+
64
+ context "when response is feeded in many parts" do
65
+ let(:parts) { raw_response.chars }
66
+
67
+ it "skips to next non-info response" do
68
+ expect(subject.status_code).to eq(200)
69
+ expect(subject.headers).to eq("Content-Length" => "12")
70
+ expect(subject.read(12)).to eq("Hello World!")
71
+ end
72
+ end
73
+ end
74
+ end
@@ -26,7 +26,7 @@ RSpec.describe HTTP::Response::Status do
26
26
  end
27
27
 
28
28
  described_class::REASONS.each do |code, reason|
29
- class_eval <<-RUBY
29
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
30
30
  context 'with well-known code: #{code}' do
31
31
  let(:code) { #{code} }
32
32
  it { is_expected.to eq #{reason.inspect} }
@@ -165,7 +165,7 @@ RSpec.describe HTTP::Response::Status do
165
165
  end
166
166
 
167
167
  described_class::SYMBOLS.each do |code, symbol|
168
- class_eval <<-RUBY
168
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
169
169
  context 'with well-known code: #{code}' do
170
170
  let(:code) { #{code} }
171
171
  it { is_expected.to be #{symbol.inspect} }
@@ -193,7 +193,7 @@ RSpec.describe HTTP::Response::Status do
193
193
  end
194
194
 
195
195
  described_class::SYMBOLS.each do |code, symbol|
196
- class_eval <<-RUBY
196
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
197
197
  describe '##{symbol}?' do
198
198
  subject { status.#{symbol}? }
199
199
 
@@ -4,6 +4,7 @@ RSpec.describe HTTP::Response do
4
4
  let(:body) { "Hello world!" }
5
5
  let(:uri) { "http://example.com/" }
6
6
  let(:headers) { {} }
7
+ let(:request) { HTTP::Request.new(:verb => :get, :uri => uri) }
7
8
 
8
9
  subject(:response) do
9
10
  HTTP::Response.new(
@@ -11,7 +12,7 @@ RSpec.describe HTTP::Response do
11
12
  :version => "1.1",
12
13
  :headers => headers,
13
14
  :body => body,
14
- :uri => uri
15
+ :request => request
15
16
  )
16
17
  end
17
18
 
@@ -109,7 +110,7 @@ RSpec.describe HTTP::Response do
109
110
  expect(response.parse("application/json")).to eq "foo" => "bar"
110
111
  end
111
112
 
112
- it "supports MIME type aliases" do
113
+ it "supports mime type aliases" do
113
114
  expect(response.parse(:json)).to eq "foo" => "bar"
114
115
  end
115
116
  end
@@ -129,13 +130,12 @@ RSpec.describe HTTP::Response do
129
130
  end
130
131
 
131
132
  describe "#inspect" do
133
+ subject { response.inspect }
134
+
132
135
  let(:headers) { {:content_type => "text/plain"} }
133
136
  let(:body) { double :to_s => "foobar" }
134
137
 
135
- it "returns human-friendly response representation" do
136
- expect(response.inspect).
137
- to eq '#<HTTP::Response/1.1 200 OK {"Content-Type"=>"text/plain"}>'
138
- end
138
+ it { is_expected.to eq '#<HTTP::Response/1.1 200 OK {"Content-Type"=>"text/plain"}>' }
139
139
  end
140
140
 
141
141
  describe "#cookies" do
@@ -166,7 +166,8 @@ RSpec.describe HTTP::Response do
166
166
  HTTP::Response.new(
167
167
  :version => "1.1",
168
168
  :status => 200,
169
- :connection => connection
169
+ :connection => connection,
170
+ :request => request
170
171
  )
171
172
  end
172
173
 
@@ -183,4 +184,79 @@ RSpec.describe HTTP::Response do
183
184
  end
184
185
  it { is_expected.not_to be_chunked }
185
186
  end
187
+
188
+ describe "backwards compatibilty with :uri" do
189
+ context "with no :verb" do
190
+ subject(:response) do
191
+ HTTP::Response.new(
192
+ :status => 200,
193
+ :version => "1.1",
194
+ :headers => headers,
195
+ :body => body,
196
+ :uri => uri
197
+ )
198
+ end
199
+
200
+ it "defaults the uri to :uri" do
201
+ expect(response.request.uri.to_s).to eq uri
202
+ end
203
+
204
+ it "defaults to the verb to :get" do
205
+ expect(response.request.verb).to eq :get
206
+ end
207
+ end
208
+
209
+ context "with both a :request and :uri" do
210
+ subject(:response) do
211
+ HTTP::Response.new(
212
+ :status => 200,
213
+ :version => "1.1",
214
+ :headers => headers,
215
+ :body => body,
216
+ :uri => uri,
217
+ :request => request
218
+ )
219
+ end
220
+
221
+ it "raises ArgumentError" do
222
+ expect { response }.to raise_error(ArgumentError)
223
+ end
224
+ end
225
+ end
226
+
227
+ describe "#body" do
228
+ let(:connection) { double(:sequence_id => 0) }
229
+ let(:chunks) { ["Hello, ", "World!"] }
230
+
231
+ subject(:response) do
232
+ HTTP::Response.new(
233
+ :status => 200,
234
+ :version => "1.1",
235
+ :headers => headers,
236
+ :request => request,
237
+ :connection => connection
238
+ )
239
+ end
240
+
241
+ before do
242
+ allow(connection).to receive(:readpartial) { chunks.shift }
243
+ allow(connection).to receive(:body_completed?) { chunks.empty? }
244
+ end
245
+
246
+ context "with no Content-Type" do
247
+ let(:headers) { {} }
248
+
249
+ it "returns a body with default binary encoding" do
250
+ expect(response.body.to_s.encoding).to eq Encoding::BINARY
251
+ end
252
+ end
253
+
254
+ context "with Content-Type: application/json" do
255
+ let(:headers) { {"Content-Type" => "application/json"} }
256
+
257
+ it "returns a body with a default UTF_8 encoding" do
258
+ expect(response.body.to_s.encoding).to eq Encoding::UTF_8
259
+ end
260
+ end
261
+ end
186
262
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe HTTP::Retriable::DelayCalculator do
4
+ let(:response) do
5
+ HTTP::Response.new(
6
+ status: 200,
7
+ version: "1.1",
8
+ headers: {},
9
+ body: "Hello world!",
10
+ request: HTTP::Request.new(verb: :get, uri: "http://example.com")
11
+ )
12
+ end
13
+
14
+ def call_delay(iterations, **options)
15
+ described_class.new(options).call(iterations, response)
16
+ end
17
+
18
+ def call_retry_header(value, **options)
19
+ response.headers["Retry-After"] = value
20
+ described_class.new(options).call(rand(1...100), response)
21
+ end
22
+
23
+ it "prevents negative sleep time" do
24
+ expect(call_delay(20, delay: -20)).to eq 0
25
+ end
26
+
27
+ it "backs off exponentially" do
28
+ expect(call_delay(1)).to be_between 0, 1
29
+ expect(call_delay(2)).to be_between 1, 2
30
+ expect(call_delay(3)).to be_between 3, 4
31
+ expect(call_delay(4)).to be_between 7, 8
32
+ expect(call_delay(5)).to be_between 15, 16
33
+ end
34
+
35
+ it "can have a maximum wait time" do
36
+ expect(call_delay(1, max_delay: 5)).to be_between 0, 1
37
+ expect(call_delay(5, max_delay: 5)).to eq 5
38
+ end
39
+
40
+ it "respects Retry-After headers as integer" do
41
+ delay_time = rand(6...2500)
42
+ header_value = delay_time.to_s
43
+ expect(call_retry_header(header_value)).to eq delay_time
44
+ expect(call_retry_header(header_value, max_delay: 5)).to eq 5
45
+ end
46
+
47
+ it "respects Retry-After headers as rfc2822 timestamp" do
48
+ delay_time = rand(6...2500)
49
+ header_value = (Time.now.gmtime + delay_time).to_datetime.rfc2822.sub("+0000", "GMT")
50
+ expect(call_retry_header(header_value)).to be_within(1).of(delay_time)
51
+ expect(call_retry_header(header_value, max_delay: 5)).to eq 5
52
+ end
53
+
54
+ it "respects Retry-After headers as rfc2822 timestamp in the past" do
55
+ delay_time = rand(6...2500)
56
+ header_value = (Time.now.gmtime - delay_time).to_datetime.rfc2822.sub("+0000", "GMT")
57
+ expect(call_retry_header(header_value)).to eq 0
58
+ end
59
+
60
+ it "does not error on invalid Retry-After header" do
61
+ [ # invalid strings
62
+ "This is a string with a number 5 in it",
63
+ "8 Eight is the first digit in this string",
64
+ "This is a string with a #{Time.now.gmtime.to_datetime.rfc2822} timestamp in it"
65
+ ].each do |header_value|
66
+ expect(call_retry_header(header_value)).to eq 0
67
+ end
68
+ end
69
+ end
@@ -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