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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +267 -0
- data/CONTRIBUTING.md +26 -0
- data/LICENSE.txt +20 -0
- data/README.md +263 -0
- data/SECURITY.md +17 -0
- data/UPGRADING.md +491 -0
- data/http.gemspec +48 -0
- data/lib/http/base64.rb +22 -0
- data/lib/http/chainable/helpers.rb +62 -0
- data/lib/http/chainable/verbs.rb +136 -0
- data/lib/http/chainable.rb +377 -0
- data/lib/http/client.rb +230 -0
- data/lib/http/connection/internals.rb +141 -0
- data/lib/http/connection.rb +265 -0
- data/lib/http/content_type.rb +89 -0
- data/lib/http/errors.rb +67 -0
- data/lib/http/feature.rb +86 -0
- data/lib/http/features/auto_deflate.rb +230 -0
- data/lib/http/features/auto_inflate.rb +64 -0
- data/lib/http/features/caching/entry.rb +178 -0
- data/lib/http/features/caching/in_memory_store.rb +63 -0
- data/lib/http/features/caching.rb +216 -0
- data/lib/http/features/digest_auth.rb +234 -0
- data/lib/http/features/instrumentation.rb +149 -0
- data/lib/http/features/logging.rb +231 -0
- data/lib/http/features/normalize_uri.rb +34 -0
- data/lib/http/features/raise_error.rb +37 -0
- data/lib/http/form_data/composite_io.rb +106 -0
- data/lib/http/form_data/file.rb +95 -0
- data/lib/http/form_data/multipart/param.rb +62 -0
- data/lib/http/form_data/multipart.rb +106 -0
- data/lib/http/form_data/part.rb +52 -0
- data/lib/http/form_data/readable.rb +58 -0
- data/lib/http/form_data/urlencoded.rb +175 -0
- data/lib/http/form_data/version.rb +8 -0
- data/lib/http/form_data.rb +102 -0
- data/lib/http/headers/known.rb +90 -0
- data/lib/http/headers/normalizer.rb +50 -0
- data/lib/http/headers.rb +343 -0
- data/lib/http/mime_type/adapter.rb +43 -0
- data/lib/http/mime_type/json.rb +41 -0
- data/lib/http/mime_type.rb +96 -0
- data/lib/http/options/definitions.rb +189 -0
- data/lib/http/options.rb +241 -0
- data/lib/http/redirector.rb +157 -0
- data/lib/http/request/body.rb +181 -0
- data/lib/http/request/builder.rb +184 -0
- data/lib/http/request/proxy.rb +83 -0
- data/lib/http/request/writer.rb +186 -0
- data/lib/http/request.rb +375 -0
- data/lib/http/response/body.rb +172 -0
- data/lib/http/response/inflater.rb +60 -0
- data/lib/http/response/parser.rb +223 -0
- data/lib/http/response/status/reasons.rb +79 -0
- data/lib/http/response/status.rb +263 -0
- data/lib/http/response.rb +350 -0
- data/lib/http/retriable/delay_calculator.rb +91 -0
- data/lib/http/retriable/errors.rb +35 -0
- data/lib/http/retriable/performer.rb +197 -0
- data/lib/http/session.rb +280 -0
- data/lib/http/timeout/global.rb +229 -0
- data/lib/http/timeout/null.rb +225 -0
- data/lib/http/timeout/per_operation.rb +197 -0
- data/lib/http/uri/normalizer.rb +82 -0
- data/lib/http/uri/parsing.rb +182 -0
- data/lib/http/uri.rb +376 -0
- data/lib/http/version.rb +6 -0
- data/lib/http.rb +36 -0
- data/sig/deps.rbs +122 -0
- data/sig/http.rbs +1619 -0
- data/test/http/base64_test.rb +28 -0
- data/test/http/client_test.rb +739 -0
- data/test/http/connection_test.rb +1533 -0
- data/test/http/content_type_test.rb +190 -0
- data/test/http/errors_test.rb +28 -0
- data/test/http/feature_test.rb +49 -0
- data/test/http/features/auto_deflate_test.rb +317 -0
- data/test/http/features/auto_inflate_test.rb +213 -0
- data/test/http/features/caching_test.rb +942 -0
- data/test/http/features/digest_auth_test.rb +996 -0
- data/test/http/features/instrumentation_test.rb +246 -0
- data/test/http/features/logging_test.rb +654 -0
- data/test/http/features/normalize_uri_test.rb +41 -0
- data/test/http/features/raise_error_test.rb +77 -0
- data/test/http/form_data/composite_io_test.rb +215 -0
- data/test/http/form_data/file_test.rb +255 -0
- data/test/http/form_data/fixtures/the-http-gem.info +1 -0
- data/test/http/form_data/multipart_test.rb +303 -0
- data/test/http/form_data/part_test.rb +90 -0
- data/test/http/form_data/urlencoded_test.rb +164 -0
- data/test/http/form_data_test.rb +232 -0
- data/test/http/headers/normalizer_test.rb +93 -0
- data/test/http/headers_test.rb +888 -0
- data/test/http/mime_type/json_test.rb +39 -0
- data/test/http/mime_type_test.rb +150 -0
- data/test/http/options/base_uri_test.rb +148 -0
- data/test/http/options/body_test.rb +21 -0
- data/test/http/options/features_test.rb +38 -0
- data/test/http/options/form_test.rb +21 -0
- data/test/http/options/headers_test.rb +32 -0
- data/test/http/options/json_test.rb +21 -0
- data/test/http/options/merge_test.rb +78 -0
- data/test/http/options/new_test.rb +37 -0
- data/test/http/options/proxy_test.rb +32 -0
- data/test/http/options_test.rb +575 -0
- data/test/http/redirector_test.rb +639 -0
- data/test/http/request/body_test.rb +318 -0
- data/test/http/request/builder_test.rb +623 -0
- data/test/http/request/writer_test.rb +391 -0
- data/test/http/request_test.rb +1733 -0
- data/test/http/response/body_test.rb +292 -0
- data/test/http/response/parser_test.rb +105 -0
- data/test/http/response/status_test.rb +322 -0
- data/test/http/response_test.rb +502 -0
- data/test/http/retriable/delay_calculator_test.rb +194 -0
- data/test/http/retriable/errors_test.rb +71 -0
- data/test/http/retriable/performer_test.rb +551 -0
- data/test/http/session_test.rb +424 -0
- data/test/http/timeout/global_test.rb +239 -0
- data/test/http/timeout/null_test.rb +218 -0
- data/test/http/timeout/per_operation_test.rb +220 -0
- data/test/http/uri/normalizer_test.rb +89 -0
- data/test/http/uri_test.rb +1140 -0
- data/test/http/version_test.rb +15 -0
- data/test/http_test.rb +818 -0
- data/test/regression_tests.rb +27 -0
- data/test/support/capture_warning.rb +10 -0
- data/test/support/dummy_server/encoding_routes.rb +47 -0
- data/test/support/dummy_server/routes.rb +201 -0
- data/test/support/dummy_server/servlet.rb +81 -0
- data/test/support/dummy_server.rb +200 -0
- data/test/support/fakeio.rb +21 -0
- data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
- data/test/support/http_handling_shared/timeout_tests.rb +134 -0
- data/test/support/http_handling_shared.rb +11 -0
- data/test/support/proxy_server.rb +207 -0
- data/test/support/servers/runner.rb +67 -0
- data/test/support/simplecov.rb +28 -0
- data/test/support/ssl_helper.rb +108 -0
- data/test/test_helper.rb +38 -0
- 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
|