http 5.3.1 → 6.0.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.
Files changed (201) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +241 -41
  3. data/LICENSE.txt +1 -1
  4. data/README.md +110 -13
  5. data/UPGRADING.md +491 -0
  6. data/http.gemspec +32 -29
  7. data/lib/http/base64.rb +11 -1
  8. data/lib/http/chainable/helpers.rb +62 -0
  9. data/lib/http/chainable/verbs.rb +136 -0
  10. data/lib/http/chainable.rb +232 -136
  11. data/lib/http/client.rb +158 -127
  12. data/lib/http/connection/internals.rb +141 -0
  13. data/lib/http/connection.rb +126 -97
  14. data/lib/http/content_type.rb +61 -6
  15. data/lib/http/errors.rb +25 -1
  16. data/lib/http/feature.rb +65 -5
  17. data/lib/http/features/auto_deflate.rb +124 -17
  18. data/lib/http/features/auto_inflate.rb +38 -15
  19. data/lib/http/features/caching/entry.rb +178 -0
  20. data/lib/http/features/caching/in_memory_store.rb +63 -0
  21. data/lib/http/features/caching.rb +216 -0
  22. data/lib/http/features/digest_auth.rb +234 -0
  23. data/lib/http/features/instrumentation.rb +97 -17
  24. data/lib/http/features/logging.rb +183 -5
  25. data/lib/http/features/normalize_uri.rb +17 -0
  26. data/lib/http/features/raise_error.rb +18 -3
  27. data/lib/http/form_data/composite_io.rb +106 -0
  28. data/lib/http/form_data/file.rb +95 -0
  29. data/lib/http/form_data/multipart/param.rb +62 -0
  30. data/lib/http/form_data/multipart.rb +106 -0
  31. data/lib/http/form_data/part.rb +52 -0
  32. data/lib/http/form_data/readable.rb +58 -0
  33. data/lib/http/form_data/urlencoded.rb +175 -0
  34. data/lib/http/form_data/version.rb +8 -0
  35. data/lib/http/form_data.rb +102 -0
  36. data/lib/http/headers/known.rb +3 -0
  37. data/lib/http/headers/normalizer.rb +17 -36
  38. data/lib/http/headers.rb +172 -65
  39. data/lib/http/mime_type/adapter.rb +24 -9
  40. data/lib/http/mime_type/json.rb +19 -4
  41. data/lib/http/mime_type.rb +21 -3
  42. data/lib/http/options/definitions.rb +189 -0
  43. data/lib/http/options.rb +172 -125
  44. data/lib/http/redirector.rb +80 -75
  45. data/lib/http/request/body.rb +87 -6
  46. data/lib/http/request/builder.rb +184 -0
  47. data/lib/http/request/proxy.rb +83 -0
  48. data/lib/http/request/writer.rb +76 -16
  49. data/lib/http/request.rb +214 -98
  50. data/lib/http/response/body.rb +103 -18
  51. data/lib/http/response/inflater.rb +35 -7
  52. data/lib/http/response/parser.rb +98 -4
  53. data/lib/http/response/status/reasons.rb +2 -4
  54. data/lib/http/response/status.rb +141 -31
  55. data/lib/http/response.rb +219 -61
  56. data/lib/http/retriable/delay_calculator.rb +38 -11
  57. data/lib/http/retriable/errors.rb +21 -0
  58. data/lib/http/retriable/performer.rb +82 -38
  59. data/lib/http/session.rb +280 -0
  60. data/lib/http/timeout/global.rb +147 -34
  61. data/lib/http/timeout/null.rb +155 -9
  62. data/lib/http/timeout/per_operation.rb +139 -18
  63. data/lib/http/uri/normalizer.rb +82 -0
  64. data/lib/http/uri/parsing.rb +182 -0
  65. data/lib/http/uri.rb +289 -124
  66. data/lib/http/version.rb +2 -1
  67. data/lib/http.rb +11 -2
  68. data/sig/deps.rbs +122 -0
  69. data/sig/http.rbs +1619 -0
  70. data/test/http/base64_test.rb +28 -0
  71. data/test/http/client_test.rb +739 -0
  72. data/test/http/connection_test.rb +1533 -0
  73. data/test/http/content_type_test.rb +190 -0
  74. data/test/http/errors_test.rb +28 -0
  75. data/test/http/feature_test.rb +49 -0
  76. data/test/http/features/auto_deflate_test.rb +317 -0
  77. data/test/http/features/auto_inflate_test.rb +213 -0
  78. data/test/http/features/caching_test.rb +942 -0
  79. data/test/http/features/digest_auth_test.rb +996 -0
  80. data/test/http/features/instrumentation_test.rb +246 -0
  81. data/test/http/features/logging_test.rb +654 -0
  82. data/test/http/features/normalize_uri_test.rb +41 -0
  83. data/test/http/features/raise_error_test.rb +77 -0
  84. data/test/http/form_data/composite_io_test.rb +215 -0
  85. data/test/http/form_data/file_test.rb +255 -0
  86. data/test/http/form_data/fixtures/the-http-gem.info +1 -0
  87. data/test/http/form_data/multipart_test.rb +303 -0
  88. data/test/http/form_data/part_test.rb +90 -0
  89. data/test/http/form_data/urlencoded_test.rb +164 -0
  90. data/test/http/form_data_test.rb +232 -0
  91. data/test/http/headers/normalizer_test.rb +93 -0
  92. data/test/http/headers_test.rb +888 -0
  93. data/test/http/mime_type/json_test.rb +39 -0
  94. data/test/http/mime_type_test.rb +150 -0
  95. data/test/http/options/base_uri_test.rb +148 -0
  96. data/test/http/options/body_test.rb +21 -0
  97. data/test/http/options/features_test.rb +38 -0
  98. data/test/http/options/form_test.rb +21 -0
  99. data/test/http/options/headers_test.rb +32 -0
  100. data/test/http/options/json_test.rb +21 -0
  101. data/test/http/options/merge_test.rb +78 -0
  102. data/test/http/options/new_test.rb +37 -0
  103. data/test/http/options/proxy_test.rb +32 -0
  104. data/test/http/options_test.rb +575 -0
  105. data/test/http/redirector_test.rb +639 -0
  106. data/test/http/request/body_test.rb +318 -0
  107. data/test/http/request/builder_test.rb +623 -0
  108. data/test/http/request/writer_test.rb +391 -0
  109. data/test/http/request_test.rb +1733 -0
  110. data/test/http/response/body_test.rb +292 -0
  111. data/test/http/response/parser_test.rb +105 -0
  112. data/test/http/response/status_test.rb +322 -0
  113. data/test/http/response_test.rb +502 -0
  114. data/test/http/retriable/delay_calculator_test.rb +194 -0
  115. data/test/http/retriable/errors_test.rb +71 -0
  116. data/test/http/retriable/performer_test.rb +551 -0
  117. data/test/http/session_test.rb +424 -0
  118. data/test/http/timeout/global_test.rb +239 -0
  119. data/test/http/timeout/null_test.rb +218 -0
  120. data/test/http/timeout/per_operation_test.rb +220 -0
  121. data/test/http/uri/normalizer_test.rb +89 -0
  122. data/test/http/uri_test.rb +1140 -0
  123. data/test/http/version_test.rb +15 -0
  124. data/test/http_test.rb +818 -0
  125. data/test/regression_tests.rb +27 -0
  126. data/test/support/dummy_server/encoding_routes.rb +47 -0
  127. data/test/support/dummy_server/routes.rb +201 -0
  128. data/test/support/dummy_server/servlet.rb +81 -0
  129. data/test/support/dummy_server.rb +200 -0
  130. data/{spec → test}/support/fakeio.rb +2 -2
  131. data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
  132. data/test/support/http_handling_shared/timeout_tests.rb +134 -0
  133. data/test/support/http_handling_shared.rb +11 -0
  134. data/test/support/proxy_server.rb +207 -0
  135. data/test/support/servers/runner.rb +67 -0
  136. data/{spec → test}/support/simplecov.rb +11 -2
  137. data/test/support/ssl_helper.rb +108 -0
  138. data/test/test_helper.rb +38 -0
  139. metadata +108 -168
  140. data/.github/workflows/ci.yml +0 -67
  141. data/.gitignore +0 -15
  142. data/.rspec +0 -1
  143. data/.rubocop/layout.yml +0 -8
  144. data/.rubocop/metrics.yml +0 -4
  145. data/.rubocop/rspec.yml +0 -9
  146. data/.rubocop/style.yml +0 -32
  147. data/.rubocop.yml +0 -11
  148. data/.rubocop_todo.yml +0 -219
  149. data/.yardopts +0 -2
  150. data/CHANGES_OLD.md +0 -1002
  151. data/Gemfile +0 -51
  152. data/Guardfile +0 -18
  153. data/Rakefile +0 -64
  154. data/lib/http/headers/mixin.rb +0 -34
  155. data/lib/http/retriable/client.rb +0 -37
  156. data/logo.png +0 -0
  157. data/spec/lib/http/client_spec.rb +0 -556
  158. data/spec/lib/http/connection_spec.rb +0 -88
  159. data/spec/lib/http/content_type_spec.rb +0 -47
  160. data/spec/lib/http/features/auto_deflate_spec.rb +0 -77
  161. data/spec/lib/http/features/auto_inflate_spec.rb +0 -86
  162. data/spec/lib/http/features/instrumentation_spec.rb +0 -81
  163. data/spec/lib/http/features/logging_spec.rb +0 -65
  164. data/spec/lib/http/features/raise_error_spec.rb +0 -62
  165. data/spec/lib/http/headers/mixin_spec.rb +0 -36
  166. data/spec/lib/http/headers/normalizer_spec.rb +0 -52
  167. data/spec/lib/http/headers_spec.rb +0 -527
  168. data/spec/lib/http/options/body_spec.rb +0 -15
  169. data/spec/lib/http/options/features_spec.rb +0 -33
  170. data/spec/lib/http/options/form_spec.rb +0 -15
  171. data/spec/lib/http/options/headers_spec.rb +0 -24
  172. data/spec/lib/http/options/json_spec.rb +0 -15
  173. data/spec/lib/http/options/merge_spec.rb +0 -68
  174. data/spec/lib/http/options/new_spec.rb +0 -30
  175. data/spec/lib/http/options/proxy_spec.rb +0 -20
  176. data/spec/lib/http/options_spec.rb +0 -13
  177. data/spec/lib/http/redirector_spec.rb +0 -530
  178. data/spec/lib/http/request/body_spec.rb +0 -211
  179. data/spec/lib/http/request/writer_spec.rb +0 -121
  180. data/spec/lib/http/request_spec.rb +0 -234
  181. data/spec/lib/http/response/body_spec.rb +0 -85
  182. data/spec/lib/http/response/parser_spec.rb +0 -74
  183. data/spec/lib/http/response/status_spec.rb +0 -253
  184. data/spec/lib/http/response_spec.rb +0 -262
  185. data/spec/lib/http/retriable/delay_calculator_spec.rb +0 -69
  186. data/spec/lib/http/retriable/performer_spec.rb +0 -302
  187. data/spec/lib/http/uri/normalizer_spec.rb +0 -95
  188. data/spec/lib/http/uri_spec.rb +0 -71
  189. data/spec/lib/http_spec.rb +0 -535
  190. data/spec/regression_specs.rb +0 -24
  191. data/spec/spec_helper.rb +0 -89
  192. data/spec/support/black_hole.rb +0 -13
  193. data/spec/support/dummy_server/servlet.rb +0 -203
  194. data/spec/support/dummy_server.rb +0 -44
  195. data/spec/support/fuubar.rb +0 -21
  196. data/spec/support/http_handling_shared.rb +0 -190
  197. data/spec/support/proxy_server.rb +0 -39
  198. data/spec/support/servers/config.rb +0 -11
  199. data/spec/support/servers/runner.rb +0 -19
  200. data/spec/support/ssl_helper.rb +0 -104
  201. /data/{spec → test}/support/capture_warning.rb +0 -0
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class HTTPRetriableErrorsTest < Minitest::Test
6
+ cover "HTTP::OutOfRetriesError*"
7
+
8
+ def error
9
+ @error ||= HTTP::OutOfRetriesError.new("out of retries")
10
+ end
11
+
12
+ # -- #response --
13
+
14
+ def test_response_defaults_to_nil
15
+ assert_nil error.response
16
+ end
17
+
18
+ def test_response_can_be_set_and_read
19
+ sentinel = Object.new
20
+ error.response = sentinel
21
+
22
+ assert_same sentinel, error.response
23
+ end
24
+
25
+ # -- #cause --
26
+
27
+ def test_cause_returns_nil_when_no_cause_is_set
28
+ assert_nil error.cause
29
+ end
30
+
31
+ def test_cause_returns_the_explicitly_set_cause
32
+ original = RuntimeError.new("boom")
33
+ error.cause = original
34
+
35
+ assert_same original, error.cause
36
+ end
37
+
38
+ def test_cause_returns_the_implicit_cause_when_no_explicit_cause_is_set
39
+ implicit = RuntimeError.new("implicit")
40
+
41
+ err = begin
42
+ raise implicit
43
+ rescue RuntimeError
44
+ begin
45
+ raise HTTP::OutOfRetriesError, "out of retries"
46
+ rescue HTTP::OutOfRetriesError => e
47
+ e
48
+ end
49
+ end
50
+
51
+ assert_same implicit, err.cause
52
+ end
53
+
54
+ def test_cause_prefers_the_explicit_cause_over_the_implicit_cause
55
+ explicit = RuntimeError.new("explicit")
56
+ implicit = RuntimeError.new("implicit")
57
+
58
+ err = begin
59
+ raise implicit
60
+ rescue RuntimeError
61
+ begin
62
+ raise HTTP::OutOfRetriesError, "out of retries"
63
+ rescue HTTP::OutOfRetriesError => e
64
+ e
65
+ end
66
+ end
67
+ err.cause = explicit
68
+
69
+ assert_same explicit, err.cause
70
+ end
71
+ end
@@ -0,0 +1,551 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ # Custom exception used across performer tests
6
+ unless defined?(CustomException)
7
+ class CustomException < StandardError
8
+ end
9
+ end
10
+
11
+ # Subclass for testing is_a? vs instance_of? in retry_exception?
12
+ unless defined?(CustomSubException)
13
+ class CustomSubException < HTTP::TimeoutError
14
+ end
15
+ end
16
+
17
+ class HTTPRetriablePerformerTest < Minitest::Test
18
+ cover "HTTP::Retriable::Performer*"
19
+
20
+ def client
21
+ @client ||= HTTP::Client.new
22
+ end
23
+
24
+ def response
25
+ @response ||= HTTP::Response.new(
26
+ status: 200,
27
+ version: "1.1",
28
+ headers: {},
29
+ body: "Hello world!",
30
+ request: request
31
+ )
32
+ end
33
+
34
+ def request
35
+ @request ||= HTTP::Request.new(
36
+ verb: :get,
37
+ uri: "http://example.com"
38
+ )
39
+ end
40
+
41
+ def setup
42
+ super
43
+ @perform_spy = { counter: 0 }
44
+ end
45
+
46
+ def counter_spy
47
+ @perform_spy[:counter]
48
+ end
49
+
50
+ def perform(client_arg = client, request_arg = request, **options, &block)
51
+ # by explicitly overwriting the default delay, we make a much faster test suite
52
+ options = { delay: 0 }.merge(options)
53
+
54
+ HTTP::Retriable::Performer
55
+ .new(**options)
56
+ .perform(client_arg, request_arg) do
57
+ @perform_spy[:counter] += 1
58
+ block ? yield : response
59
+ end
60
+ end
61
+
62
+ def measure_wait
63
+ t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
64
+ result = yield
65
+ t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
66
+ [t2 - t1, result]
67
+ end
68
+
69
+ # -- #initialize --
70
+
71
+ def test_initialize_coerces_tries_to_integer
72
+ performer = HTTP::Retriable::Performer.new(tries: 3.7)
73
+
74
+ assert_equal 3, performer.instance_variable_get(:@tries)
75
+ end
76
+
77
+ def test_initialize_coerces_string_tries_via_to_i
78
+ performer = HTTP::Retriable::Performer.new(tries: "3")
79
+
80
+ assert_equal 3, performer.instance_variable_get(:@tries)
81
+ end
82
+
83
+ def test_initialize_truncates_float_like_string_tries_via_to_i
84
+ performer = HTTP::Retriable::Performer.new(tries: "3.7")
85
+
86
+ assert_equal 3, performer.instance_variable_get(:@tries)
87
+ end
88
+
89
+ def test_initialize_uses_default_delay_when_none_provided
90
+ performer = HTTP::Retriable::Performer.new
91
+ delay = performer.calculate_delay(1, nil)
92
+
93
+ assert_operator delay, :>=, 0
94
+ end
95
+
96
+ # -- #perform: expected exception --
97
+
98
+ def test_perform_expected_exception_retries_the_request
99
+ assert_raises HTTP::OutOfRetriesError do
100
+ perform(exceptions: [CustomException], tries: 2) do
101
+ raise CustomException
102
+ end
103
+ end
104
+ assert_equal 2, counter_spy
105
+ end
106
+
107
+ def test_perform_expected_exception_retries_subclasses_of_listed_exceptions
108
+ assert_raises HTTP::OutOfRetriesError do
109
+ perform(exceptions: [HTTP::TimeoutError], tries: 2) do
110
+ raise CustomSubException
111
+ end
112
+ end
113
+ assert_equal 2, counter_spy
114
+ end
115
+
116
+ # -- #perform: unexpected exception --
117
+
118
+ def test_perform_unexpected_exception_does_not_retry
119
+ assert_raises CustomException do
120
+ perform(exceptions: [], tries: 2) do
121
+ raise CustomException
122
+ end
123
+ end
124
+ assert_equal 1, counter_spy
125
+ end
126
+
127
+ # -- #perform: expected status codes --
128
+
129
+ def make_response(**)
130
+ HTTP::Response.new(
131
+ status: 200,
132
+ version: "1.1",
133
+ headers: {},
134
+ body: "Hello world!",
135
+ request: request, **
136
+ )
137
+ end
138
+
139
+ def test_perform_expected_status_retries_the_request
140
+ assert_raises HTTP::OutOfRetriesError do
141
+ perform(retry_statuses: [200], tries: 2)
142
+ end
143
+ assert_equal 2, counter_spy
144
+ end
145
+
146
+ def test_perform_does_not_retry_when_range_does_not_cover_status
147
+ result = perform(retry_statuses: [400...500], tries: 2) do
148
+ make_response(status: 200)
149
+ end
150
+
151
+ assert_equal 200, result.status.to_i
152
+ end
153
+
154
+ def test_perform_does_not_retry_when_numeric_does_not_match_status
155
+ result = perform(retry_statuses: [500], tries: 2) do
156
+ make_response(status: 200)
157
+ end
158
+
159
+ assert_equal 200, result.status.to_i
160
+ end
161
+
162
+ def test_perform_does_not_retry_when_proc_returns_false
163
+ result = perform(retry_statuses: [->(s) { s >= 500 }], tries: 2) do
164
+ make_response(status: 200)
165
+ end
166
+
167
+ assert_equal 200, result.status.to_i
168
+ end
169
+
170
+ # -- status codes expressed in many ways --
171
+
172
+ [
173
+ 301,
174
+ 301.0,
175
+ [200, 301, 485],
176
+ 250...400,
177
+ [250...Float::INFINITY],
178
+ ->(status_code) { status_code == 301 },
179
+ [->(status_code) { status_code == 301 }]
180
+ ].each do |retry_statuses|
181
+ define_method(:"test_perform_status_codes_#{retry_statuses}") do
182
+ assert_raises HTTP::OutOfRetriesError do
183
+ perform(retry_statuses: retry_statuses, tries: 2) do
184
+ make_response(status: 301)
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ # -- unexpected status code --
191
+
192
+ def test_perform_unexpected_status_does_not_retry
193
+ result = perform(retry_statuses: [], tries: 2)
194
+
195
+ assert_equal response, result
196
+ assert_equal 1, counter_spy
197
+ end
198
+
199
+ # -- on_retry callback --
200
+
201
+ def test_on_retry_callback_with_exception
202
+ callback_call_spy = 0
203
+
204
+ callback_spy = proc do |callback_request, error, callback_response|
205
+ assert_equal request, callback_request
206
+ assert_kind_of HTTP::TimeoutError, error
207
+ assert_nil callback_response
208
+ callback_call_spy += 1
209
+ end
210
+
211
+ assert_raises HTTP::OutOfRetriesError do
212
+ perform(tries: 3, on_retry: callback_spy) do
213
+ raise HTTP::TimeoutError
214
+ end
215
+ end
216
+
217
+ assert_equal 2, callback_call_spy
218
+ end
219
+
220
+ def test_on_retry_callback_with_response
221
+ callback_call_spy = 0
222
+
223
+ callback_spy = proc do |callback_request, error, callback_response|
224
+ assert_equal request, callback_request
225
+ assert_nil error
226
+ assert_equal response, callback_response
227
+ callback_call_spy += 1
228
+ end
229
+
230
+ assert_raises HTTP::OutOfRetriesError do
231
+ perform(retry_statuses: [200], tries: 3, on_retry: callback_spy)
232
+ end
233
+
234
+ assert_equal 2, callback_call_spy
235
+ end
236
+
237
+ # -- delay option --
238
+
239
+ def test_delay_sleeps_for_the_calculated_delay
240
+ slept_values = []
241
+ performer = HTTP::Retriable::Performer.new(delay: 0.123, tries: 2, should_retry: ->(*) { true })
242
+ performer.define_singleton_method(:sleep) { |d| slept_values << d }
243
+
244
+ assert_raises(HTTP::OutOfRetriesError) do
245
+ performer.perform(client, request) { response }
246
+ end
247
+
248
+ assert_equal [0.123], slept_values
249
+ end
250
+
251
+ def test_delay_can_be_a_positive_number
252
+ timing_slack = 0.5
253
+ time, = measure_wait do
254
+ assert_raises(HTTP::OutOfRetriesError) do
255
+ perform(delay: 0.02, tries: 3, should_retry: ->(*) { true })
256
+ end
257
+ end
258
+
259
+ assert_in_delta 0.04, time, timing_slack
260
+ end
261
+
262
+ def test_delay_can_be_a_proc_number
263
+ timing_slack = 0.5
264
+ time, = measure_wait do
265
+ assert_raises(HTTP::OutOfRetriesError) do
266
+ perform(delay: ->(attempt) { attempt / 50.0 }, tries: 3, should_retry: ->(*) { true })
267
+ end
268
+ end
269
+
270
+ assert_in_delta 0.06, time, timing_slack
271
+ end
272
+
273
+ def test_delay_receives_correct_retry_number_when_a_proc
274
+ retry_count = 0
275
+ retry_proc = proc do |attempt|
276
+ assert_equal retry_count, attempt
277
+ assert_operator attempt, :>, 0
278
+ 0
279
+ end
280
+ assert_raises(HTTP::OutOfRetriesError) do
281
+ perform(delay: retry_proc, should_retry: ->(*) { true }) do
282
+ retry_count += 1
283
+ response
284
+ end
285
+ end
286
+ end
287
+
288
+ def test_delay_respects_max_delay_option
289
+ timing_slack = 0.5
290
+ time, = measure_wait do
291
+ assert_raises(HTTP::OutOfRetriesError) do
292
+ perform(delay: 100, max_delay: 0.02, tries: 3, should_retry: ->(*) { true })
293
+ end
294
+ end
295
+
296
+ assert_in_delta 0.04, time, timing_slack
297
+ end
298
+
299
+ # -- should_retry option --
300
+
301
+ def test_should_retry_decides_if_request_should_be_retried
302
+ retry_proc = proc do |req, err, res, attempt|
303
+ assert_equal request, req
304
+ if res
305
+ assert_nil err
306
+ assert_equal response, res
307
+ else
308
+ assert_kind_of CustomException, err
309
+ assert_nil res
310
+ end
311
+ attempt < 5
312
+ end
313
+
314
+ begin
315
+ perform(should_retry: retry_proc) do
316
+ rand < 0.5 ? response : raise(CustomException)
317
+ end
318
+ rescue CustomException
319
+ nil
320
+ end
321
+
322
+ assert_equal 5, counter_spy
323
+ end
324
+
325
+ def test_should_retry_passes_the_exception_to_proc
326
+ received_err = nil
327
+ retry_proc = proc do |_req, err, _res, _attempt|
328
+ received_err = err
329
+ false
330
+ end
331
+
332
+ assert_raises CustomException do
333
+ perform(should_retry: retry_proc) do
334
+ raise CustomException
335
+ end
336
+ end
337
+
338
+ assert_kind_of CustomException, received_err
339
+ end
340
+
341
+ def test_should_retry_raises_original_error_if_not_retryable
342
+ retry_proc = ->(*) { false }
343
+
344
+ assert_raises CustomException do
345
+ perform(should_retry: retry_proc) do
346
+ raise CustomException
347
+ end
348
+ end
349
+
350
+ assert_equal 1, counter_spy
351
+ end
352
+
353
+ def test_should_retry_raises_out_of_retries_error_if_retryable
354
+ retry_proc = ->(*) { true }
355
+
356
+ assert_raises HTTP::OutOfRetriesError do
357
+ perform(should_retry: retry_proc) do
358
+ raise CustomException
359
+ end
360
+ end
361
+
362
+ assert_equal 5, counter_spy
363
+ end
364
+
365
+ # -- #calculate_delay --
366
+
367
+ def test_calculate_delay_passes_response_to_delay_calculator
368
+ responses_seen = []
369
+
370
+ performer = HTTP::Retriable::Performer.new(delay: 0, retry_statuses: [200], tries: 2)
371
+ calculator = performer.instance_variable_get(:@delay_calculator)
372
+ original_call = calculator.method(:call)
373
+ calculator.define_singleton_method(:call) do |iteration, resp|
374
+ responses_seen << resp
375
+ original_call.call(iteration, resp)
376
+ end
377
+
378
+ begin
379
+ performer.perform(client, request) { response }
380
+ rescue HTTP::OutOfRetriesError
381
+ nil
382
+ end
383
+
384
+ assert_equal response, responses_seen.first
385
+ end
386
+
387
+ # -- when block returns nil --
388
+
389
+ def test_when_block_returns_nil_continues_iterating
390
+ call_count = 0
391
+ perform(tries: 3) do
392
+ call_count += 1
393
+ call_count < 2 ? nil : response
394
+ end
395
+
396
+ assert_equal 2, call_count
397
+ end
398
+
399
+ # -- connection closing --
400
+
401
+ def test_connection_closing_does_not_close_on_proper_response
402
+ close_called = false
403
+ mock_client = fake(close: ->(*) { close_called = true })
404
+ perform(mock_client)
405
+
406
+ refute close_called
407
+ end
408
+
409
+ def test_connection_closing_closes_after_each_raised_attempt
410
+ close_count = 0
411
+ mock_client = fake(close: ->(*) { close_count += 1 })
412
+
413
+ assert_raises(HTTP::OutOfRetriesError) do
414
+ perform(mock_client, should_retry: ->(*) { true }, tries: 3)
415
+ end
416
+
417
+ assert_equal 3, close_count
418
+ end
419
+
420
+ def test_connection_closing_closes_on_unexpected_exception
421
+ close_count = 0
422
+ mock_client = fake(close: ->(*) { close_count += 1 })
423
+
424
+ assert_raises(CustomException) do
425
+ perform(mock_client) do
426
+ raise CustomException
427
+ end
428
+ end
429
+
430
+ assert_equal 1, close_count
431
+ end
432
+
433
+ # -- response flushing on exhausted retries --
434
+
435
+ def test_response_flushing_flushes_when_retries_exhausted
436
+ flushed = false
437
+ flush_response = HTTP::Response.new(
438
+ status: 503,
439
+ version: "1.1",
440
+ headers: {},
441
+ body: "Service Unavailable",
442
+ request: request
443
+ )
444
+ flush_response.define_singleton_method(:flush) do
445
+ flushed = true
446
+ self
447
+ end
448
+
449
+ begin
450
+ HTTP::Retriable::Performer
451
+ .new(delay: 0, retry_statuses: [503], tries: 2)
452
+ .perform(client, request) { flush_response }
453
+ rescue HTTP::OutOfRetriesError
454
+ nil
455
+ end
456
+
457
+ assert flushed, "expected response to be flushed on final attempt"
458
+ end
459
+
460
+ # -- HTTP::OutOfRetriesError --
461
+
462
+ def test_out_of_retries_error_has_original_exception_as_cause
463
+ err = nil
464
+ begin
465
+ perform(exceptions: [CustomException]) do
466
+ raise CustomException
467
+ end
468
+ rescue HTTP::OutOfRetriesError => e
469
+ err = e
470
+ end
471
+
472
+ assert_kind_of CustomException, err.cause
473
+ end
474
+
475
+ def test_out_of_retries_error_has_last_response_as_attribute
476
+ err = nil
477
+ begin
478
+ perform(should_retry: ->(*) { true })
479
+ rescue HTTP::OutOfRetriesError => e
480
+ err = e
481
+ end
482
+
483
+ assert_equal response, err.response
484
+ end
485
+
486
+ def test_out_of_retries_error_has_message_containing_verb_and_uri
487
+ err = nil
488
+ begin
489
+ perform(exceptions: [CustomException]) do
490
+ raise CustomException
491
+ end
492
+ rescue HTTP::OutOfRetriesError => e
493
+ err = e
494
+ end
495
+
496
+ assert_includes err.message, "GET"
497
+ assert_includes err.message, "http://example.com"
498
+ assert_includes err.message, "failed"
499
+ end
500
+
501
+ def test_out_of_retries_error_includes_status_when_response_present
502
+ err = nil
503
+ begin
504
+ perform(retry_statuses: [200], tries: 2)
505
+ rescue HTTP::OutOfRetriesError => e
506
+ err = e
507
+ end
508
+
509
+ assert_includes err.message, "200"
510
+ assert_includes err.message, "GET"
511
+ assert_includes err.message, "http://example.com"
512
+ end
513
+
514
+ def test_out_of_retries_error_includes_exception_in_message
515
+ err = nil
516
+ begin
517
+ perform(exceptions: [CustomException]) do
518
+ raise CustomException, "something went wrong"
519
+ end
520
+ rescue HTTP::OutOfRetriesError => e
521
+ err = e
522
+ end
523
+
524
+ assert_includes err.message, "something went wrong"
525
+ end
526
+
527
+ def test_out_of_retries_error_does_not_include_status_when_no_response
528
+ err = nil
529
+ begin
530
+ perform(exceptions: [CustomException]) do
531
+ raise CustomException
532
+ end
533
+ rescue HTTP::OutOfRetriesError => e
534
+ err = e
535
+ end
536
+
537
+ refute_includes err.message, " with "
538
+ end
539
+
540
+ def test_out_of_retries_error_does_not_include_exception_when_no_exception
541
+ err = nil
542
+ begin
543
+ perform(retry_statuses: [200], tries: 2)
544
+ rescue HTTP::OutOfRetriesError => e
545
+ err = e
546
+ end
547
+
548
+ assert_match(/failed with [\w ]+\z/, err.message)
549
+ assert_includes err.message, " with "
550
+ end
551
+ end