http 6.0.0-java

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 (142) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +267 -0
  3. data/CONTRIBUTING.md +26 -0
  4. data/LICENSE.txt +20 -0
  5. data/README.md +263 -0
  6. data/SECURITY.md +17 -0
  7. data/UPGRADING.md +491 -0
  8. data/http.gemspec +48 -0
  9. data/lib/http/base64.rb +22 -0
  10. data/lib/http/chainable/helpers.rb +62 -0
  11. data/lib/http/chainable/verbs.rb +136 -0
  12. data/lib/http/chainable.rb +377 -0
  13. data/lib/http/client.rb +230 -0
  14. data/lib/http/connection/internals.rb +141 -0
  15. data/lib/http/connection.rb +265 -0
  16. data/lib/http/content_type.rb +89 -0
  17. data/lib/http/errors.rb +67 -0
  18. data/lib/http/feature.rb +86 -0
  19. data/lib/http/features/auto_deflate.rb +230 -0
  20. data/lib/http/features/auto_inflate.rb +64 -0
  21. data/lib/http/features/caching/entry.rb +178 -0
  22. data/lib/http/features/caching/in_memory_store.rb +63 -0
  23. data/lib/http/features/caching.rb +216 -0
  24. data/lib/http/features/digest_auth.rb +234 -0
  25. data/lib/http/features/instrumentation.rb +149 -0
  26. data/lib/http/features/logging.rb +231 -0
  27. data/lib/http/features/normalize_uri.rb +34 -0
  28. data/lib/http/features/raise_error.rb +37 -0
  29. data/lib/http/form_data/composite_io.rb +106 -0
  30. data/lib/http/form_data/file.rb +95 -0
  31. data/lib/http/form_data/multipart/param.rb +62 -0
  32. data/lib/http/form_data/multipart.rb +106 -0
  33. data/lib/http/form_data/part.rb +52 -0
  34. data/lib/http/form_data/readable.rb +58 -0
  35. data/lib/http/form_data/urlencoded.rb +175 -0
  36. data/lib/http/form_data/version.rb +8 -0
  37. data/lib/http/form_data.rb +102 -0
  38. data/lib/http/headers/known.rb +90 -0
  39. data/lib/http/headers/normalizer.rb +50 -0
  40. data/lib/http/headers.rb +343 -0
  41. data/lib/http/mime_type/adapter.rb +43 -0
  42. data/lib/http/mime_type/json.rb +41 -0
  43. data/lib/http/mime_type.rb +96 -0
  44. data/lib/http/options/definitions.rb +189 -0
  45. data/lib/http/options.rb +241 -0
  46. data/lib/http/redirector.rb +157 -0
  47. data/lib/http/request/body.rb +181 -0
  48. data/lib/http/request/builder.rb +184 -0
  49. data/lib/http/request/proxy.rb +83 -0
  50. data/lib/http/request/writer.rb +186 -0
  51. data/lib/http/request.rb +375 -0
  52. data/lib/http/response/body.rb +172 -0
  53. data/lib/http/response/inflater.rb +60 -0
  54. data/lib/http/response/parser.rb +223 -0
  55. data/lib/http/response/status/reasons.rb +79 -0
  56. data/lib/http/response/status.rb +263 -0
  57. data/lib/http/response.rb +350 -0
  58. data/lib/http/retriable/delay_calculator.rb +91 -0
  59. data/lib/http/retriable/errors.rb +35 -0
  60. data/lib/http/retriable/performer.rb +197 -0
  61. data/lib/http/session.rb +280 -0
  62. data/lib/http/timeout/global.rb +229 -0
  63. data/lib/http/timeout/null.rb +225 -0
  64. data/lib/http/timeout/per_operation.rb +197 -0
  65. data/lib/http/uri/normalizer.rb +82 -0
  66. data/lib/http/uri/parsing.rb +182 -0
  67. data/lib/http/uri.rb +376 -0
  68. data/lib/http/version.rb +6 -0
  69. data/lib/http.rb +36 -0
  70. data/sig/deps.rbs +122 -0
  71. data/sig/http.rbs +1619 -0
  72. data/test/http/base64_test.rb +28 -0
  73. data/test/http/client_test.rb +739 -0
  74. data/test/http/connection_test.rb +1533 -0
  75. data/test/http/content_type_test.rb +190 -0
  76. data/test/http/errors_test.rb +28 -0
  77. data/test/http/feature_test.rb +49 -0
  78. data/test/http/features/auto_deflate_test.rb +317 -0
  79. data/test/http/features/auto_inflate_test.rb +213 -0
  80. data/test/http/features/caching_test.rb +942 -0
  81. data/test/http/features/digest_auth_test.rb +996 -0
  82. data/test/http/features/instrumentation_test.rb +246 -0
  83. data/test/http/features/logging_test.rb +654 -0
  84. data/test/http/features/normalize_uri_test.rb +41 -0
  85. data/test/http/features/raise_error_test.rb +77 -0
  86. data/test/http/form_data/composite_io_test.rb +215 -0
  87. data/test/http/form_data/file_test.rb +255 -0
  88. data/test/http/form_data/fixtures/the-http-gem.info +1 -0
  89. data/test/http/form_data/multipart_test.rb +303 -0
  90. data/test/http/form_data/part_test.rb +90 -0
  91. data/test/http/form_data/urlencoded_test.rb +164 -0
  92. data/test/http/form_data_test.rb +232 -0
  93. data/test/http/headers/normalizer_test.rb +93 -0
  94. data/test/http/headers_test.rb +888 -0
  95. data/test/http/mime_type/json_test.rb +39 -0
  96. data/test/http/mime_type_test.rb +150 -0
  97. data/test/http/options/base_uri_test.rb +148 -0
  98. data/test/http/options/body_test.rb +21 -0
  99. data/test/http/options/features_test.rb +38 -0
  100. data/test/http/options/form_test.rb +21 -0
  101. data/test/http/options/headers_test.rb +32 -0
  102. data/test/http/options/json_test.rb +21 -0
  103. data/test/http/options/merge_test.rb +78 -0
  104. data/test/http/options/new_test.rb +37 -0
  105. data/test/http/options/proxy_test.rb +32 -0
  106. data/test/http/options_test.rb +575 -0
  107. data/test/http/redirector_test.rb +639 -0
  108. data/test/http/request/body_test.rb +318 -0
  109. data/test/http/request/builder_test.rb +623 -0
  110. data/test/http/request/writer_test.rb +391 -0
  111. data/test/http/request_test.rb +1733 -0
  112. data/test/http/response/body_test.rb +292 -0
  113. data/test/http/response/parser_test.rb +105 -0
  114. data/test/http/response/status_test.rb +322 -0
  115. data/test/http/response_test.rb +502 -0
  116. data/test/http/retriable/delay_calculator_test.rb +194 -0
  117. data/test/http/retriable/errors_test.rb +71 -0
  118. data/test/http/retriable/performer_test.rb +551 -0
  119. data/test/http/session_test.rb +424 -0
  120. data/test/http/timeout/global_test.rb +239 -0
  121. data/test/http/timeout/null_test.rb +218 -0
  122. data/test/http/timeout/per_operation_test.rb +220 -0
  123. data/test/http/uri/normalizer_test.rb +89 -0
  124. data/test/http/uri_test.rb +1140 -0
  125. data/test/http/version_test.rb +15 -0
  126. data/test/http_test.rb +818 -0
  127. data/test/regression_tests.rb +27 -0
  128. data/test/support/capture_warning.rb +10 -0
  129. data/test/support/dummy_server/encoding_routes.rb +47 -0
  130. data/test/support/dummy_server/routes.rb +201 -0
  131. data/test/support/dummy_server/servlet.rb +81 -0
  132. data/test/support/dummy_server.rb +200 -0
  133. data/test/support/fakeio.rb +21 -0
  134. data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
  135. data/test/support/http_handling_shared/timeout_tests.rb +134 -0
  136. data/test/support/http_handling_shared.rb +11 -0
  137. data/test/support/proxy_server.rb +207 -0
  138. data/test/support/servers/runner.rb +67 -0
  139. data/test/support/simplecov.rb +28 -0
  140. data/test/support/ssl_helper.rb +108 -0
  141. data/test/test_helper.rb +38 -0
  142. metadata +218 -0
@@ -0,0 +1,350 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ require "http/errors"
6
+ require "http/headers"
7
+ require "http/content_type"
8
+ require "http/mime_type"
9
+ require "http/response/status"
10
+ require "http/response/inflater"
11
+ require "http/cookie"
12
+ require "time"
13
+
14
+ module HTTP
15
+ # Represents an HTTP response with status, headers, and body
16
+ class Response
17
+ extend Forwardable
18
+
19
+ # The response status
20
+ #
21
+ # @example
22
+ # response.status # => #<HTTP::Response::Status 200>
23
+ #
24
+ # @return [Status] the response status
25
+ # @api public
26
+ attr_reader :status
27
+
28
+ # The HTTP version
29
+ #
30
+ # @example
31
+ # response.version # => "1.1"
32
+ #
33
+ # @return [String] the HTTP version
34
+ # @api public
35
+ attr_reader :version
36
+
37
+ # The response body
38
+ #
39
+ # @example
40
+ # response.body
41
+ #
42
+ # @return [Body] the response body
43
+ # @api public
44
+ attr_reader :body
45
+
46
+ # The original request
47
+ #
48
+ # @example
49
+ # response.request
50
+ #
51
+ # @return [Request] the original request
52
+ # @api public
53
+ attr_reader :request
54
+
55
+ # The HTTP headers collection
56
+ #
57
+ # @example
58
+ # response.headers
59
+ #
60
+ # @return [HTTP::Headers] the response headers
61
+ # @api public
62
+ attr_reader :headers
63
+
64
+ # The proxy headers
65
+ #
66
+ # @example
67
+ # response.proxy_headers
68
+ #
69
+ # @return [Hash] the proxy headers
70
+ # @api public
71
+ attr_reader :proxy_headers
72
+
73
+ # Create a new Response instance
74
+ #
75
+ # @example
76
+ # Response.new(status: 200, version: "1.1", request: req)
77
+ #
78
+ # @param [Integer] status Status code
79
+ # @param [String] version HTTP version
80
+ # @param [Hash] headers
81
+ # @param [Hash] proxy_headers
82
+ # @param [HTTP::Connection, nil] connection
83
+ # @param [String, nil] encoding Encoding to use when reading body
84
+ # @param [String, nil] body
85
+ # @param [HTTP::Request, nil] request The request this is in response to
86
+ # @param [String, nil] uri (DEPRECATED) used to populate a missing request
87
+ # @return [Response]
88
+ # @api public
89
+ def initialize(status:, version:, headers: {}, proxy_headers: {}, connection: nil,
90
+ encoding: nil, body: nil, request: nil, uri: nil)
91
+ @version = version
92
+ @request = init_request(request, uri)
93
+ @status = HTTP::Response::Status.new(status)
94
+ @headers = HTTP::Headers.coerce(headers)
95
+ @proxy_headers = HTTP::Headers.coerce(proxy_headers)
96
+ @body = init_body(body, connection, encoding)
97
+ end
98
+
99
+ # @!method reason
100
+ # Return the reason phrase for the response status
101
+ # @example
102
+ # response.reason # => "OK"
103
+ # @return [String, nil]
104
+ # @api public
105
+ def_delegator :@status, :reason
106
+
107
+ # @!method code
108
+ # Return the numeric status code
109
+ # @example
110
+ # response.code # => 200
111
+ # @return [Integer]
112
+ # @api public
113
+ def_delegator :@status, :code
114
+
115
+ # @!method to_s
116
+ # Consume the response body as a string
117
+ # @example
118
+ # response.to_s # => "<html>...</html>"
119
+ # @return [String]
120
+ # @api public
121
+ def_delegator :@body, :to_s
122
+ alias to_str to_s
123
+
124
+ # @!method readpartial
125
+ # Read a chunk of the response body
126
+ # @example
127
+ # response.readpartial # => "chunk"
128
+ # @return [String]
129
+ # @raise [EOFError] when no more data left
130
+ # @api public
131
+ def_delegator :@body, :readpartial
132
+
133
+ # @!method connection
134
+ # Return the underlying connection object
135
+ # @example
136
+ # response.connection
137
+ # @return [HTTP::Connection]
138
+ # @api public
139
+ def_delegator :@body, :connection
140
+
141
+ # @!method uri
142
+ # Return the URI of the original request
143
+ # @example
144
+ # response.uri # => #<HTTP::URI ...>
145
+ # @return (see HTTP::Request#uri)
146
+ # @api public
147
+ def_delegator :@request, :uri
148
+
149
+ # Returns an Array ala Rack: `[status, headers, body]`
150
+ #
151
+ # @example
152
+ # response.to_a # => [200, {"Content-Type" => "text/html"}, "body"]
153
+ #
154
+ # @return [Array(Fixnum, Hash, String)]
155
+ # @api public
156
+ def to_a
157
+ [status.to_i, headers.to_h, body.to_s]
158
+ end
159
+
160
+ # @!method deconstruct
161
+ # Array pattern matching interface
162
+ #
163
+ # @example
164
+ # response.deconstruct
165
+ #
166
+ # @see #to_a
167
+ # @return [Array(Integer, Hash, String)]
168
+ # @api public
169
+ alias deconstruct to_a
170
+
171
+ # Pattern matching interface for matching against response attributes
172
+ #
173
+ # @example
174
+ # case response
175
+ # in { status: 200..299, body: /success/ }
176
+ # "ok"
177
+ # in { status: 400.. }
178
+ # "error"
179
+ # end
180
+ #
181
+ # @param keys [Array<Symbol>, nil] keys to extract, or nil for all
182
+ # @return [Hash{Symbol => Object}]
183
+ # @api public
184
+ def deconstruct_keys(keys)
185
+ hash = {
186
+ status: @status,
187
+ version: @version,
188
+ headers: @headers,
189
+ body: @body,
190
+ request: @request,
191
+ proxy_headers: @proxy_headers
192
+ }
193
+ keys ? hash.slice(*keys) : hash
194
+ end
195
+
196
+ # Flushes body and returns self-reference
197
+ #
198
+ # @example
199
+ # response.flush # => #<HTTP::Response ...>
200
+ #
201
+ # @return [Response]
202
+ # @api public
203
+ def flush
204
+ body.to_s
205
+ self
206
+ end
207
+
208
+ # Value of the Content-Length header
209
+ #
210
+ # @example
211
+ # response.content_length # => 438
212
+ #
213
+ # @return [nil] if Content-Length was not given, or it's value was invalid
214
+ # (not an integer, e.g. empty string or string with non-digits).
215
+ # @return [Integer] otherwise
216
+ # @api public
217
+ def content_length
218
+ # http://greenbytes.de/tech/webdav/rfc7230.html#rfc.section.3.3.3
219
+ # Clause 3: "If a message is received with both a Transfer-Encoding
220
+ # and a Content-Length header field, the Transfer-Encoding overrides the Content-Length.
221
+ return nil if @headers.include?(Headers::TRANSFER_ENCODING)
222
+
223
+ # RFC 7230 Section 3.3.2: If multiple Content-Length values are present,
224
+ # they must all be identical; otherwise treat as invalid.
225
+ values = @headers.get(Headers::CONTENT_LENGTH).uniq
226
+ return nil unless values.one?
227
+
228
+ Integer(values.first, exception: false)
229
+ end
230
+
231
+ # Parsed Content-Type header
232
+ #
233
+ # @example
234
+ # response.content_type # => #<HTTP::ContentType ...>
235
+ #
236
+ # @return [HTTP::ContentType]
237
+ # @api public
238
+ def content_type
239
+ @content_type ||= ContentType.parse headers[Headers::CONTENT_TYPE]
240
+ end
241
+
242
+ # @!method mime_type
243
+ # MIME type of response (if any)
244
+ # @example
245
+ # response.mime_type # => "text/html"
246
+ # @return [String, nil]
247
+ # @api public
248
+ def_delegator :content_type, :mime_type
249
+
250
+ # @!method charset
251
+ # Charset of response (if any)
252
+ # @example
253
+ # response.charset # => "utf-8"
254
+ # @return [String, nil]
255
+ # @api public
256
+ def_delegator :content_type, :charset
257
+
258
+ # Cookies from Set-Cookie headers
259
+ #
260
+ # @example
261
+ # response.cookies # => [#<HTTP::Cookie ...>, ...]
262
+ #
263
+ # @return [Array<HTTP::Cookie>]
264
+ # @api public
265
+ def cookies
266
+ @cookies ||= headers.get(Headers::SET_COOKIE).flat_map { |v| HTTP::Cookie.parse(v, uri) }
267
+ end
268
+
269
+ # Check if the response uses chunked transfer encoding
270
+ #
271
+ # @example
272
+ # response.chunked? # => true
273
+ #
274
+ # @return [Boolean]
275
+ # @api public
276
+ def chunked?
277
+ return false unless @headers.include?(Headers::TRANSFER_ENCODING)
278
+
279
+ encoding = @headers.get(Headers::TRANSFER_ENCODING)
280
+
281
+ encoding.last == Headers::CHUNKED
282
+ end
283
+
284
+ # Parse response body with corresponding MIME type adapter
285
+ #
286
+ # @example
287
+ # response.parse("application/json") # => {"key" => "value"}
288
+ #
289
+ # @param type [#to_s] Parse as given MIME type.
290
+ # @raise (see MimeType.[])
291
+ # @return [Object]
292
+ # @api public
293
+ def parse(type = nil)
294
+ MimeType[type || mime_type].decode to_s
295
+ rescue => e
296
+ raise ParseError, e.message
297
+ end
298
+
299
+ # Inspect a response
300
+ #
301
+ # @example
302
+ # response.inspect # => "#<HTTP::Response/1.1 200 OK text/html>"
303
+ #
304
+ # @return [String]
305
+ # @api public
306
+ def inspect
307
+ "#<#{self.class}/#{@version} #{code} #{reason} #{mime_type}>"
308
+ end
309
+
310
+ private
311
+
312
+ # Determine the default encoding for the body
313
+ # @return [Encoding]
314
+ # @api private
315
+ def default_encoding
316
+ return Encoding::UTF_8 if mime_type == "application/json"
317
+
318
+ Encoding::BINARY
319
+ end
320
+
321
+ # Initialize the response body
322
+ #
323
+ # @return [Body]
324
+ # @api private
325
+ def init_body(body, connection, encoding)
326
+ if body
327
+ body
328
+ else
329
+ encoding ||= charset || default_encoding
330
+
331
+ Response::Body.new(connection, encoding: encoding)
332
+ end
333
+ end
334
+
335
+ # Initialize an HTTP::Request
336
+ #
337
+ # @return [HTTP::Request]
338
+ # @api private
339
+ def init_request(request, uri)
340
+ raise ArgumentError, ":uri is for backwards compatibilty and conflicts with :request" if request && uri
341
+
342
+ # For backwards compatibilty
343
+ if uri
344
+ HTTP::Request.new(uri: uri, verb: :get)
345
+ else
346
+ request
347
+ end
348
+ end
349
+ end
350
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ module Retriable
5
+ # Calculates retry delays with support for Retry-After headers
6
+ # @api private
7
+ class DelayCalculator
8
+ # Initializes the delay calculator
9
+ #
10
+ # @param [#call, Numeric, nil] delay delay value or proc
11
+ # @param [#to_f] max_delay maximum delay cap
12
+ # @api private
13
+ # @return [HTTP::Retriable::DelayCalculator]
14
+ def initialize(delay: nil, max_delay: Float::MAX)
15
+ @max_delay = Float(max_delay)
16
+ if delay.respond_to?(:call)
17
+ @delay_proc = delay
18
+ else
19
+ @delay = delay
20
+ end
21
+ end
22
+
23
+ # Calculates delay for the given iteration
24
+ #
25
+ # @param [Integer] iteration
26
+ # @param [HTTP::Response, nil] response
27
+ # @api private
28
+ # @return [Numeric]
29
+ def call(iteration, response)
30
+ delay = if response && (retry_header = response.headers["Retry-After"])
31
+ delay_from_retry_header(retry_header)
32
+ else
33
+ calculate_delay_from_iteration(iteration)
34
+ end
35
+
36
+ ensure_delay_in_bounds(delay)
37
+ end
38
+
39
+ # Pattern matching RFC 2822 formatted dates in Retry-After headers
40
+ RFC2822_DATE_REGEX = /^
41
+ (?:Sun|Mon|Tue|Wed|Thu|Fri|Sat),\s+
42
+ (?:0[1-9]|[1-2]?[0-9]|3[01])\s+
43
+ (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+
44
+ (?:19[0-9]{2}|[2-9][0-9]{3})\s+
45
+ (?:2[0-3]|[0-1][0-9]):(?:[0-5][0-9]):(?:60|[0-5][0-9])\s+
46
+ GMT
47
+ $/x
48
+
49
+ # Parses delay from Retry-After header value
50
+ #
51
+ # @param [String] value
52
+ # @api private
53
+ # @return [Numeric]
54
+ def delay_from_retry_header(value)
55
+ value = String(value).strip
56
+
57
+ case value
58
+ when RFC2822_DATE_REGEX then DateTime.rfc2822(value).to_time - Time.now.utc
59
+ when /\A\d+$/ then value.to_i
60
+ else 0
61
+ end
62
+ end
63
+
64
+ # Calculates delay based on iteration number
65
+ #
66
+ # @param [Integer] iteration
67
+ # @api private
68
+ # @return [Numeric]
69
+ def calculate_delay_from_iteration(iteration)
70
+ if @delay_proc
71
+ @delay_proc.call(iteration)
72
+ elsif @delay
73
+ @delay
74
+ else
75
+ delay = (2**(iteration - 1)) - 1
76
+ delay_noise = rand
77
+ delay + delay_noise
78
+ end
79
+ end
80
+
81
+ # Clamps delay to configured bounds
82
+ #
83
+ # @param [Numeric] delay
84
+ # @api private
85
+ # @return [Numeric]
86
+ def ensure_delay_in_bounds(delay)
87
+ Float(delay.clamp(0, @max_delay))
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ # Retriable performance ran out of attempts
5
+ class OutOfRetriesError < Error
6
+ # The last response received before failure
7
+ #
8
+ # @example
9
+ # error.response
10
+ #
11
+ # @return [HTTP::Response, nil] the last response received
12
+ # @api public
13
+ attr_accessor :response
14
+
15
+ # Set the underlying exception
16
+ #
17
+ # @example
18
+ # error.cause = original_error
19
+ #
20
+ # @return [Exception, nil]
21
+ # @api public
22
+ attr_writer :cause
23
+
24
+ # Returns the cause of the error
25
+ #
26
+ # @example
27
+ # error.cause
28
+ #
29
+ # @api public
30
+ # @return [Exception, nil]
31
+ def cause
32
+ @cause || super
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "http/retriable/errors"
5
+ require "http/retriable/delay_calculator"
6
+ require "openssl"
7
+
8
+ module HTTP
9
+ # Retry logic for failed HTTP requests
10
+ module Retriable
11
+ # Request performing watchdog.
12
+ # @api private
13
+ class Performer
14
+ # Exceptions we should retry
15
+ RETRIABLE_ERRORS = [
16
+ HTTP::TimeoutError,
17
+ HTTP::ConnectionError,
18
+ IO::EAGAINWaitReadable,
19
+ Errno::ECONNRESET,
20
+ Errno::ECONNREFUSED,
21
+ Errno::EHOSTUNREACH,
22
+ OpenSSL::SSL::SSLError,
23
+ EOFError,
24
+ IOError
25
+ ].freeze
26
+
27
+ # Create a new retry performer
28
+ #
29
+ # @param [#to_i] tries maximum number of attempts
30
+ # @param [#call, #to_i, nil] delay delay between retries
31
+ # @param [Array<Exception>] exceptions exception classes to retry
32
+ # @param [Array<#to_i>, nil] retry_statuses status codes to retry
33
+ # @param [#call] on_retry callback invoked on each retry
34
+ # @param [#to_f] max_delay maximum delay between retries
35
+ # @param [#call, nil] should_retry custom retry predicate
36
+ # @api private
37
+ # @return [HTTP::Retriable::Performer]
38
+ def initialize(tries: 5, delay: nil, exceptions: RETRIABLE_ERRORS, retry_statuses: nil,
39
+ on_retry: ->(*_args) {}, max_delay: Float::MAX, should_retry: nil)
40
+ @exception_classes = exceptions
41
+ @retry_statuses = retry_statuses
42
+ @tries = tries.to_i
43
+ @on_retry = on_retry
44
+ @should_retry_proc = should_retry
45
+ @delay_calculator = DelayCalculator.new(delay: delay, max_delay: max_delay)
46
+ end
47
+
48
+ # Execute request with retry logic
49
+ #
50
+ # @see #initialize
51
+ # @return [HTTP::Response]
52
+ # @api private
53
+ def perform(client, req, &block)
54
+ 1.upto(Float::INFINITY) do |attempt| # infinite loop with index
55
+ err, res = try_request(&block)
56
+
57
+ if retry_request?(req, err, res, attempt)
58
+ retry_attempt(client, req, err, res, attempt)
59
+ elsif err
60
+ finish_attempt(client, err)
61
+ elsif res
62
+ return res
63
+ end
64
+ end
65
+ end
66
+
67
+ # Calculates delay between retries
68
+ #
69
+ # @param [Integer] iteration
70
+ # @param [HTTP::Response, nil] response
71
+ # @api private
72
+ # @return [Numeric]
73
+ def calculate_delay(iteration, response)
74
+ @delay_calculator.call(iteration, response)
75
+ end
76
+
77
+ private
78
+
79
+ # Executes a single retry attempt
80
+ #
81
+ # @api private
82
+ # @return [void]
83
+ def retry_attempt(client, req, err, res, attempt)
84
+ # Some servers support Keep-Alive on any response. Thus we should
85
+ # flush response before retry, to avoid state error (when socket
86
+ # has pending response data and we try to write new request).
87
+ # Alternatively, as we don't need response body here at all, we
88
+ # are going to close client, effectively closing underlying socket
89
+ # and resetting client's state.
90
+ wait_for_retry_or_raise(req, err, res, attempt)
91
+ ensure
92
+ client.close
93
+ end
94
+
95
+ # Closes client and raises the error
96
+ #
97
+ # @api private
98
+ # @return [void]
99
+ def finish_attempt(client, err)
100
+ client.close
101
+ raise err
102
+ end
103
+
104
+ # Attempts to execute the request block
105
+ #
106
+ # @api private
107
+ # @return [Array]
108
+ # rubocop:disable Lint/RescueException
109
+ def try_request
110
+ err, res = nil
111
+
112
+ begin
113
+ res = yield
114
+ rescue Exception => e
115
+ err = e
116
+ end
117
+
118
+ [err, res]
119
+ end
120
+ # rubocop:enable Lint/RescueException
121
+
122
+ # Checks whether the request should be retried
123
+ #
124
+ # @api private
125
+ # @return [Boolean]
126
+ def retry_request?(req, err, res, attempt)
127
+ if @should_retry_proc
128
+ @should_retry_proc.call(req, err, res, attempt)
129
+ elsif err
130
+ retry_exception?(err)
131
+ else
132
+ retry_response?(res)
133
+ end
134
+ end
135
+
136
+ # Checks whether the exception is retriable
137
+ #
138
+ # @api private
139
+ # @return [Boolean]
140
+ def retry_exception?(err)
141
+ @exception_classes.any? { |e| err.is_a?(e) }
142
+ end
143
+
144
+ # Checks whether the response status warrants retry
145
+ #
146
+ # @api private
147
+ # @return [Boolean]
148
+ def retry_response?(res)
149
+ return false unless @retry_statuses
150
+
151
+ response_status = Integer(res.status)
152
+ retry_matchers = [@retry_statuses].flatten
153
+
154
+ retry_matchers.any? do |matcher|
155
+ case matcher
156
+ when Range then matcher.cover?(response_status)
157
+ when Numeric then matcher == response_status
158
+ else matcher.call(response_status)
159
+ end
160
+ end
161
+ end
162
+
163
+ # Waits for retry delay or raises if out of attempts
164
+ #
165
+ # @api private
166
+ # @return [void]
167
+ def wait_for_retry_or_raise(req, err, res, attempt)
168
+ if attempt < @tries
169
+ @on_retry.call(req, err, res)
170
+ sleep(calculate_delay(attempt, res))
171
+ else
172
+ res&.flush
173
+ raise out_of_retries_error(req, res, err)
174
+ end
175
+ end
176
+
177
+ # Builds OutOfRetriesError
178
+ #
179
+ # @param request [HTTP::Request]
180
+ # @param response [HTTP::Response, nil]
181
+ # @param exception [Exception, nil]
182
+ # @api private
183
+ # @return [HTTP::OutOfRetriesError]
184
+ def out_of_retries_error(request, response, exception)
185
+ message = format("%s <%s> failed", String(request.verb).upcase, request.uri)
186
+
187
+ message += " with #{response.status}" if response
188
+ message += ":#{exception}" if exception
189
+
190
+ OutOfRetriesError.new(message).tap do |ex|
191
+ ex.cause = exception
192
+ ex.response = response
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end