faraday 0.9.1 → 0.17.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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +196 -0
- data/LICENSE.md +1 -1
- data/README.md +192 -28
- data/Rakefile +6 -64
- data/lib/faraday/adapter/em_http.rb +17 -11
- data/lib/faraday/adapter/em_http_ssl_patch.rb +1 -1
- data/lib/faraday/adapter/em_synchrony.rb +19 -5
- data/lib/faraday/adapter/excon.rb +13 -11
- data/lib/faraday/adapter/httpclient.rb +31 -9
- data/lib/faraday/adapter/net_http.rb +36 -14
- data/lib/faraday/adapter/net_http_persistent.rb +37 -17
- data/lib/faraday/adapter/patron.rb +44 -21
- data/lib/faraday/adapter/rack.rb +1 -1
- data/lib/faraday/adapter/test.rb +79 -28
- data/lib/faraday/adapter/typhoeus.rb +4 -115
- data/lib/faraday/adapter.rb +10 -1
- data/lib/faraday/autoload.rb +1 -1
- data/lib/faraday/connection.rb +72 -20
- data/lib/faraday/deprecate.rb +101 -0
- data/lib/faraday/error.rb +90 -24
- data/lib/faraday/options.rb +43 -20
- data/lib/faraday/parameters.rb +56 -39
- data/lib/faraday/rack_builder.rb +27 -2
- data/lib/faraday/request/authorization.rb +1 -2
- data/lib/faraday/request/multipart.rb +7 -2
- data/lib/faraday/request/retry.rb +84 -19
- data/lib/faraday/request.rb +22 -0
- data/lib/faraday/response/logger.rb +29 -8
- data/lib/faraday/response/raise_error.rb +7 -3
- data/lib/faraday/response.rb +9 -5
- data/lib/faraday/utils.rb +32 -3
- data/lib/faraday.rb +14 -34
- data/spec/faraday/deprecate_spec.rb +69 -0
- data/spec/faraday/error_spec.rb +102 -0
- data/spec/faraday/response/raise_error_spec.rb +95 -0
- data/spec/spec_helper.rb +104 -0
- data/test/adapters/em_http_test.rb +10 -0
- data/test/adapters/em_synchrony_test.rb +22 -10
- data/test/adapters/excon_test.rb +10 -0
- data/test/adapters/httpclient_test.rb +14 -1
- data/test/adapters/integration.rb +17 -8
- data/test/adapters/logger_test.rb +65 -11
- data/test/adapters/net_http_persistent_test.rb +96 -2
- data/test/adapters/net_http_test.rb +67 -2
- data/test/adapters/patron_test.rb +28 -8
- data/test/adapters/rack_test.rb +8 -1
- data/test/adapters/test_middleware_test.rb +46 -3
- data/test/adapters/typhoeus_test.rb +19 -9
- data/test/composite_read_io_test.rb +16 -18
- data/test/connection_test.rb +294 -78
- data/test/env_test.rb +55 -5
- data/test/helper.rb +11 -17
- data/test/middleware/retry_test.rb +115 -10
- data/test/middleware_stack_test.rb +97 -10
- data/test/options_test.rb +97 -16
- data/test/parameters_test.rb +94 -1
- data/test/request_middleware_test.rb +24 -40
- data/test/response_middleware_test.rb +4 -4
- data/test/utils_test.rb +40 -0
- metadata +21 -66
- data/.document +0 -6
- data/CONTRIBUTING.md +0 -36
- data/Gemfile +0 -25
- data/faraday.gemspec +0 -34
- data/script/cached-bundle +0 -46
- data/script/console +0 -7
- data/script/generate_certs +0 -42
- data/script/package +0 -7
- data/script/proxy-server +0 -42
- data/script/release +0 -17
- data/script/s3-put +0 -71
- data/script/server +0 -36
- data/script/test +0 -172
data/lib/faraday/error.rb
CHANGED
@@ -1,22 +1,25 @@
|
|
1
|
-
|
2
|
-
class Error < StandardError; end
|
3
|
-
class MissingDependency < Error; end
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
5
|
-
|
6
|
-
|
3
|
+
require 'faraday/deprecate'
|
4
|
+
|
5
|
+
# Faraday namespace.
|
6
|
+
module Faraday
|
7
|
+
# Faraday error base class.
|
8
|
+
class Error < StandardError
|
9
|
+
attr_reader :response, :wrapped_exception
|
7
10
|
|
8
|
-
def initialize(
|
11
|
+
def initialize(exc, response = nil)
|
9
12
|
@wrapped_exception = nil
|
10
13
|
@response = response
|
11
14
|
|
12
|
-
if
|
13
|
-
super(
|
14
|
-
@wrapped_exception =
|
15
|
-
elsif
|
16
|
-
super("the server responded with status #{
|
17
|
-
@response =
|
15
|
+
if exc.respond_to?(:backtrace)
|
16
|
+
super(exc.message)
|
17
|
+
@wrapped_exception = exc
|
18
|
+
elsif exc.respond_to?(:each_key)
|
19
|
+
super("the server responded with status #{exc[:status]}")
|
20
|
+
@response = exc
|
18
21
|
else
|
19
|
-
super(
|
22
|
+
super(exc.to_s)
|
20
23
|
end
|
21
24
|
end
|
22
25
|
|
@@ -29,25 +32,88 @@ module Faraday
|
|
29
32
|
end
|
30
33
|
|
31
34
|
def inspect
|
32
|
-
|
35
|
+
inner = +''
|
36
|
+
inner << " wrapped=#{@wrapped_exception.inspect}" if @wrapped_exception
|
37
|
+
inner << " response=#{@response.inspect}" if @response
|
38
|
+
inner << " #{super}" if inner.empty?
|
39
|
+
%(#<#{self.class}#{inner}>)
|
33
40
|
end
|
34
41
|
end
|
35
42
|
|
36
|
-
class
|
37
|
-
class
|
38
|
-
|
43
|
+
# Faraday client error class. Represents 4xx status responses.
|
44
|
+
class ClientError < Error
|
45
|
+
end
|
46
|
+
|
47
|
+
# Raised by Faraday::Response::RaiseError in case of a 400 response.
|
48
|
+
class BadRequestError < ClientError
|
49
|
+
end
|
50
|
+
|
51
|
+
# Raised by Faraday::Response::RaiseError in case of a 401 response.
|
52
|
+
class UnauthorizedError < ClientError
|
53
|
+
end
|
54
|
+
|
55
|
+
# Raised by Faraday::Response::RaiseError in case of a 403 response.
|
56
|
+
class ForbiddenError < ClientError
|
57
|
+
end
|
58
|
+
|
59
|
+
# Raised by Faraday::Response::RaiseError in case of a 404 response.
|
60
|
+
class ResourceNotFound < ClientError
|
61
|
+
end
|
62
|
+
|
63
|
+
# Raised by Faraday::Response::RaiseError in case of a 407 response.
|
64
|
+
class ProxyAuthError < ClientError
|
65
|
+
end
|
39
66
|
|
40
|
-
|
41
|
-
|
42
|
-
|
67
|
+
# Raised by Faraday::Response::RaiseError in case of a 409 response.
|
68
|
+
class ConflictError < ClientError
|
69
|
+
end
|
70
|
+
|
71
|
+
# Raised by Faraday::Response::RaiseError in case of a 422 response.
|
72
|
+
class UnprocessableEntityError < ClientError
|
73
|
+
end
|
74
|
+
|
75
|
+
# Faraday server error class. Represents 5xx status responses.
|
76
|
+
class ServerError < Error
|
77
|
+
end
|
78
|
+
|
79
|
+
# A unified client error for timeouts.
|
80
|
+
class TimeoutError < ServerError
|
81
|
+
def initialize(exc = 'timeout', response = nil)
|
82
|
+
super(exc, response)
|
43
83
|
end
|
44
84
|
end
|
45
85
|
|
46
|
-
|
86
|
+
# Raised by Faraday::Response::RaiseError in case of a nil status in response.
|
87
|
+
class NilStatusError < ServerError
|
88
|
+
def initialize(_exc, response: nil)
|
89
|
+
message = 'http status could not be derived from the server response'
|
90
|
+
super(message, response)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# A unified error for failed connections.
|
95
|
+
class ConnectionFailed < Error
|
96
|
+
end
|
97
|
+
|
98
|
+
# A unified client error for SSL errors.
|
99
|
+
class SSLError < Error
|
100
|
+
end
|
101
|
+
|
102
|
+
# Raised by FaradayMiddleware::ResponseMiddleware
|
103
|
+
class ParsingError < Error
|
104
|
+
end
|
105
|
+
|
106
|
+
# Exception used to control the Retry middleware.
|
107
|
+
#
|
108
|
+
# @see Faraday::Request::Retry
|
109
|
+
class RetriableResponse < Error
|
47
110
|
end
|
48
111
|
|
49
|
-
[
|
50
|
-
|
51
|
-
Error.const_set(
|
112
|
+
%i[ClientError ConnectionFailed ResourceNotFound
|
113
|
+
ParsingError TimeoutError SSLError RetriableResponse].each do |const|
|
114
|
+
Error.const_set(
|
115
|
+
const,
|
116
|
+
DeprecatedClass.proxy_class(Faraday.const_get(const))
|
117
|
+
)
|
52
118
|
end
|
53
119
|
end
|
data/lib/faraday/options.rb
CHANGED
@@ -18,23 +18,20 @@ module Faraday
|
|
18
18
|
# Public
|
19
19
|
def update(obj)
|
20
20
|
obj.each do |key, value|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
value.
|
26
|
-
|
27
|
-
|
28
|
-
value = hash
|
21
|
+
sub_options = self.class.options_for(key)
|
22
|
+
if sub_options
|
23
|
+
new_value = sub_options.from(value) if value
|
24
|
+
elsif value.is_a?(Hash)
|
25
|
+
new_value = value.dup
|
26
|
+
else
|
27
|
+
new_value = value
|
29
28
|
end
|
30
29
|
|
31
|
-
self.send("#{key}=",
|
30
|
+
self.send("#{key}=", new_value) unless new_value.nil?
|
32
31
|
end
|
33
32
|
self
|
34
33
|
end
|
35
34
|
|
36
|
-
alias merge! update
|
37
|
-
|
38
35
|
# Public
|
39
36
|
def delete(key)
|
40
37
|
value = send(key)
|
@@ -48,8 +45,24 @@ module Faraday
|
|
48
45
|
end
|
49
46
|
|
50
47
|
# Public
|
51
|
-
def merge(
|
52
|
-
|
48
|
+
def merge!(other)
|
49
|
+
other.each do |key, other_value|
|
50
|
+
self_value = self.send(key)
|
51
|
+
sub_options = self.class.options_for(key)
|
52
|
+
new_value = (self_value && sub_options && other_value) ? self_value.merge(other_value) : other_value
|
53
|
+
self.send("#{key}=", new_value) unless new_value.nil?
|
54
|
+
end
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
# Public
|
59
|
+
def merge(other)
|
60
|
+
dup.merge!(other)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Public
|
64
|
+
def deep_dup
|
65
|
+
self.class.from(self)
|
53
66
|
end
|
54
67
|
|
55
68
|
# Public
|
@@ -59,7 +72,7 @@ module Faraday
|
|
59
72
|
if args.size > 0
|
60
73
|
send(key_setter, args.first)
|
61
74
|
elsif block_given?
|
62
|
-
send(key_setter,
|
75
|
+
send(key_setter, yield(key))
|
63
76
|
else
|
64
77
|
raise self.class.fetch_error_class, "key not found: #{key.inspect}"
|
65
78
|
end
|
@@ -189,8 +202,7 @@ module Faraday
|
|
189
202
|
end
|
190
203
|
|
191
204
|
class RequestOptions < Options.new(:params_encoder, :proxy, :bind,
|
192
|
-
:timeout, :open_timeout, :boundary,
|
193
|
-
:oauth)
|
205
|
+
:timeout, :open_timeout, :write_timeout, :boundary, :oauth, :context)
|
194
206
|
|
195
207
|
def []=(key, value)
|
196
208
|
if key && key.to_sym == :proxy
|
@@ -202,7 +214,8 @@ module Faraday
|
|
202
214
|
end
|
203
215
|
|
204
216
|
class SSLOptions < Options.new(:verify, :ca_file, :ca_path, :verify_mode,
|
205
|
-
:cert_store, :client_cert, :client_key, :certificate, :private_key, :verify_depth,
|
217
|
+
:cert_store, :client_cert, :client_key, :certificate, :private_key, :verify_depth,
|
218
|
+
:version, :min_version, :max_version)
|
206
219
|
|
207
220
|
def verify?
|
208
221
|
verify != false
|
@@ -231,8 +244,8 @@ module Faraday
|
|
231
244
|
super(value)
|
232
245
|
end
|
233
246
|
|
234
|
-
memoized(:user) { uri.user && Utils.unescape(uri.user) }
|
235
|
-
memoized(:password) { uri.password && Utils.unescape(uri.password) }
|
247
|
+
memoized(:user) { uri && uri.user && Utils.unescape(uri.user) }
|
248
|
+
memoized(:password) { uri && uri.password && Utils.unescape(uri.password) }
|
236
249
|
end
|
237
250
|
|
238
251
|
class ConnectionOptions < Options.new(:request, :proxy, :ssl, :builder, :url,
|
@@ -252,7 +265,8 @@ module Faraday
|
|
252
265
|
end
|
253
266
|
|
254
267
|
class Env < Options.new(:method, :body, :url, :request, :request_headers,
|
255
|
-
:ssl, :parallel_manager, :params, :response, :response_headers, :status
|
268
|
+
:ssl, :parallel_manager, :params, :response, :response_headers, :status,
|
269
|
+
:reason_phrase)
|
256
270
|
|
257
271
|
ContentLength = 'Content-Length'.freeze
|
258
272
|
StatusesWithoutBody = Set.new [204, 304]
|
@@ -269,6 +283,15 @@ module Faraday
|
|
269
283
|
|
270
284
|
def_delegators :request, :params_encoder
|
271
285
|
|
286
|
+
# Public
|
287
|
+
def self.from(value)
|
288
|
+
env = super(value)
|
289
|
+
if value.respond_to?(:custom_members)
|
290
|
+
env.custom_members.update(value.custom_members)
|
291
|
+
end
|
292
|
+
env
|
293
|
+
end
|
294
|
+
|
272
295
|
# Public
|
273
296
|
def [](key)
|
274
297
|
if in_member_set?(key)
|
data/lib/faraday/parameters.rb
CHANGED
@@ -40,12 +40,15 @@ module Faraday
|
|
40
40
|
end
|
41
41
|
return buffer.chop
|
42
42
|
elsif value.is_a?(Array)
|
43
|
+
new_parent = "#{parent}%5B%5D"
|
44
|
+
return new_parent if value.empty?
|
43
45
|
buffer = ""
|
44
46
|
value.each_with_index do |val, i|
|
45
|
-
new_parent = "#{parent}%5B%5D"
|
46
47
|
buffer << "#{to_query.call(new_parent, val)}&"
|
47
48
|
end
|
48
49
|
return buffer.chop
|
50
|
+
elsif value.nil?
|
51
|
+
return parent
|
49
52
|
else
|
50
53
|
encoded_value = escape(value)
|
51
54
|
return "#{parent}=#{encoded_value}"
|
@@ -63,50 +66,64 @@ module Faraday
|
|
63
66
|
|
64
67
|
def self.decode(query)
|
65
68
|
return nil if query == nil
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
69
|
+
|
70
|
+
params = {}
|
71
|
+
query.split("&").each do |pair|
|
72
|
+
next if pair.empty?
|
73
|
+
key, value = pair.split("=", 2)
|
74
|
+
key = unescape(key)
|
75
|
+
value = unescape(value.gsub(/\+/, ' ')) if value
|
76
|
+
|
77
|
+
subkeys = key.scan(/[^\[\]]+(?:\]?\[\])?/)
|
78
|
+
context = params
|
79
|
+
subkeys.each_with_index do |subkey, i|
|
80
|
+
is_array = subkey =~ /[\[\]]+\Z/
|
81
|
+
subkey = $` if is_array
|
82
|
+
last_subkey = i == subkeys.length - 1
|
83
|
+
|
84
|
+
if !last_subkey || is_array
|
85
|
+
value_type = is_array ? Array : Hash
|
86
|
+
if context[subkey] && !context[subkey].is_a?(value_type)
|
87
|
+
raise TypeError, "expected %s (got %s) for param `%s'" % [
|
88
|
+
value_type.name,
|
89
|
+
context[subkey].class.name,
|
90
|
+
subkey
|
91
|
+
]
|
92
|
+
end
|
93
|
+
context = (context[subkey] ||= value_type.new)
|
71
94
|
end
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
95
|
+
|
96
|
+
if context.is_a?(Array) && !is_array
|
97
|
+
if !context.last.is_a?(Hash) || context.last.has_key?(subkey)
|
98
|
+
context << {}
|
99
|
+
end
|
100
|
+
context = context.last
|
101
|
+
end
|
102
|
+
|
103
|
+
if last_subkey
|
104
|
+
if is_array
|
105
|
+
context << value
|
106
|
+
else
|
107
|
+
context[subkey] = value
|
108
|
+
end
|
77
109
|
end
|
78
|
-
else
|
79
|
-
hash
|
80
110
|
end
|
81
111
|
end
|
82
112
|
|
83
|
-
|
84
|
-
|
85
|
-
pair.split('=', 2) if pair && !pair.empty?
|
86
|
-
end).compact.inject(empty_accumulator.dup) do |accu, (key, value)|
|
87
|
-
key = unescape(key)
|
88
|
-
if value.kind_of?(String)
|
89
|
-
value = unescape(value.gsub(/\+/, ' '))
|
90
|
-
end
|
113
|
+
dehash(params, 0)
|
114
|
+
end
|
91
115
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
else
|
104
|
-
current_hash[subkeys.last] = value
|
105
|
-
end
|
106
|
-
accu
|
107
|
-
end).inject(empty_accumulator.dup) do |accu, (key, value)|
|
108
|
-
accu[key] = value.kind_of?(Hash) ? dehash.call(value) : value
|
109
|
-
accu
|
116
|
+
# Internal: convert a nested hash with purely numeric keys into an array.
|
117
|
+
# FIXME: this is not compatible with Rack::Utils.parse_nested_query
|
118
|
+
def self.dehash(hash, depth)
|
119
|
+
hash.each do |key, value|
|
120
|
+
hash[key] = dehash(value, depth + 1) if value.kind_of?(Hash)
|
121
|
+
end
|
122
|
+
|
123
|
+
if depth > 0 && !hash.empty? && hash.keys.all? { |k| k =~ /^\d+$/ }
|
124
|
+
hash.keys.sort.inject([]) { |all, key| all << hash[key] }
|
125
|
+
else
|
126
|
+
hash
|
110
127
|
end
|
111
128
|
end
|
112
129
|
end
|
data/lib/faraday/rack_builder.rb
CHANGED
@@ -84,6 +84,7 @@ module Faraday
|
|
84
84
|
use_symbol(Faraday::Middleware, klass, *args, &block)
|
85
85
|
else
|
86
86
|
raise_if_locked
|
87
|
+
warn_middleware_after_adapter if adapter_set?
|
87
88
|
@handlers << self.class::Handler.new(klass, *args, &block)
|
88
89
|
end
|
89
90
|
end
|
@@ -105,6 +106,7 @@ module Faraday
|
|
105
106
|
def insert(index, *args, &block)
|
106
107
|
raise_if_locked
|
107
108
|
index = assert_index(index)
|
109
|
+
warn_middleware_after_adapter if inserting_after_adapter?(index)
|
108
110
|
handler = self.class::Handler.new(*args, &block)
|
109
111
|
@handlers.insert(index, handler)
|
110
112
|
end
|
@@ -136,6 +138,8 @@ module Faraday
|
|
136
138
|
#
|
137
139
|
# Returns a Faraday::Response.
|
138
140
|
def build_response(connection, request)
|
141
|
+
warn 'WARNING: No adapter was configured for this request' unless adapter_set?
|
142
|
+
|
139
143
|
app.call(build_env(connection, request))
|
140
144
|
end
|
141
145
|
|
@@ -151,8 +155,9 @@ module Faraday
|
|
151
155
|
lock!
|
152
156
|
to_app(lambda { |env|
|
153
157
|
response = Response.new
|
154
|
-
response.finish(env) unless env.parallel?
|
155
158
|
env.response = response
|
159
|
+
response.finish(env) unless env.parallel?
|
160
|
+
response
|
156
161
|
})
|
157
162
|
end
|
158
163
|
end
|
@@ -188,7 +193,7 @@ module Faraday
|
|
188
193
|
# :ssl - Hash of options for configuring SSL requests.
|
189
194
|
def build_env(connection, request)
|
190
195
|
Env.new(request.method, request.body,
|
191
|
-
connection.build_exclusive_url(request.path, request.params),
|
196
|
+
connection.build_exclusive_url(request.path, request.params, request.options.params_encoder),
|
192
197
|
request.options, request.headers, connection.ssl,
|
193
198
|
connection.parallel_manager)
|
194
199
|
end
|
@@ -199,6 +204,26 @@ module Faraday
|
|
199
204
|
raise StackLocked, "can't modify middleware stack after making a request" if locked?
|
200
205
|
end
|
201
206
|
|
207
|
+
def warn_middleware_after_adapter
|
208
|
+
warn "WARNING: Unexpected middleware set after the adapter. " \
|
209
|
+
"This won't be supported from Faraday 1.0."
|
210
|
+
end
|
211
|
+
|
212
|
+
def adapter_set?
|
213
|
+
@handlers.any? { |handler| is_adapter?(handler) }
|
214
|
+
end
|
215
|
+
|
216
|
+
def inserting_after_adapter?(index)
|
217
|
+
adapter_index = @handlers.find_index { |handler| is_adapter?(handler) }
|
218
|
+
return false if adapter_index.nil?
|
219
|
+
|
220
|
+
index > adapter_index
|
221
|
+
end
|
222
|
+
|
223
|
+
def is_adapter?(handler)
|
224
|
+
handler.klass.ancestors.include? Faraday::Adapter
|
225
|
+
end
|
226
|
+
|
202
227
|
def use_symbol(mod, key, *args, &block)
|
203
228
|
use(mod.lookup_middleware(key), *args, &block)
|
204
229
|
end
|
@@ -1,13 +1,14 @@
|
|
1
1
|
require File.expand_path("../url_encoded", __FILE__)
|
2
|
+
require 'securerandom'
|
2
3
|
|
3
4
|
module Faraday
|
4
5
|
class Request::Multipart < Request::UrlEncoded
|
5
6
|
self.mime_type = 'multipart/form-data'.freeze
|
6
|
-
|
7
|
+
DEFAULT_BOUNDARY_PREFIX = "-----------RubyMultipartPost".freeze unless defined? DEFAULT_BOUNDARY_PREFIX
|
7
8
|
|
8
9
|
def call(env)
|
9
10
|
match_content_type(env) do |params|
|
10
|
-
env.request.boundary ||=
|
11
|
+
env.request.boundary ||= unique_boundary
|
11
12
|
env.request_headers[CONTENT_TYPE] += "; boundary=#{env.request.boundary}"
|
12
13
|
env.body = create_multipart(env, params)
|
13
14
|
end
|
@@ -44,6 +45,10 @@ module Faraday
|
|
44
45
|
return body
|
45
46
|
end
|
46
47
|
|
48
|
+
def unique_boundary
|
49
|
+
"#{DEFAULT_BOUNDARY_PREFIX}-#{SecureRandom.hex}"
|
50
|
+
end
|
51
|
+
|
47
52
|
def process_params(params, prefix = nil, pieces = nil, &block)
|
48
53
|
params.inject(pieces || []) do |all, (key, value)|
|
49
54
|
key = "#{prefix}[#{key}]" if prefix
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
1
3
|
module Faraday
|
2
4
|
# Catches exceptions and retries each request a limited number of times.
|
3
5
|
#
|
@@ -10,7 +12,7 @@ module Faraday
|
|
10
12
|
#
|
11
13
|
# Faraday.new do |conn|
|
12
14
|
# conn.request :retry, max: 2, interval: 0.05,
|
13
|
-
# interval_randomness: 0.5, backoff_factor: 2
|
15
|
+
# interval_randomness: 0.5, backoff_factor: 2,
|
14
16
|
# exceptions: [CustomException, 'Timeout::Error']
|
15
17
|
# conn.adapter ...
|
16
18
|
# end
|
@@ -19,15 +21,19 @@ module Faraday
|
|
19
21
|
# interval that is random between 0.1 and 0.15
|
20
22
|
#
|
21
23
|
class Request::Retry < Faraday::Middleware
|
22
|
-
|
24
|
+
DEFAULT_EXCEPTIONS = [Errno::ETIMEDOUT, 'Timeout::Error',
|
25
|
+
Faraday::TimeoutError, Faraday::RetriableResponse
|
26
|
+
].freeze
|
23
27
|
IDEMPOTENT_METHODS = [:delete, :get, :head, :options, :put]
|
24
28
|
|
25
|
-
class Options < Faraday::Options.new(:max, :interval, :
|
26
|
-
:exceptions, :methods, :retry_if
|
29
|
+
class Options < Faraday::Options.new(:max, :interval, :max_interval, :interval_randomness,
|
30
|
+
:backoff_factor, :exceptions, :methods, :retry_if, :retry_block,
|
31
|
+
:retry_statuses)
|
32
|
+
|
27
33
|
DEFAULT_CHECK = lambda { |env,exception| false }
|
28
34
|
|
29
35
|
def self.from(value)
|
30
|
-
if
|
36
|
+
if Integer === value
|
31
37
|
new(value)
|
32
38
|
else
|
33
39
|
super(value)
|
@@ -42,8 +48,12 @@ module Faraday
|
|
42
48
|
(self[:interval] ||= 0).to_f
|
43
49
|
end
|
44
50
|
|
51
|
+
def max_interval
|
52
|
+
(self[:max_interval] ||= Float::MAX).to_f
|
53
|
+
end
|
54
|
+
|
45
55
|
def interval_randomness
|
46
|
-
(self[:interval_randomness] ||= 0).
|
56
|
+
(self[:interval_randomness] ||= 0).to_f
|
47
57
|
end
|
48
58
|
|
49
59
|
def backoff_factor
|
@@ -51,8 +61,7 @@ module Faraday
|
|
51
61
|
end
|
52
62
|
|
53
63
|
def exceptions
|
54
|
-
Array(self[:exceptions] ||=
|
55
|
-
Error::TimeoutError])
|
64
|
+
Array(self[:exceptions] ||= DEFAULT_EXCEPTIONS)
|
56
65
|
end
|
57
66
|
|
58
67
|
def methods
|
@@ -63,6 +72,13 @@ module Faraday
|
|
63
72
|
self[:retry_if] ||= DEFAULT_CHECK
|
64
73
|
end
|
65
74
|
|
75
|
+
def retry_block
|
76
|
+
self[:retry_block] ||= Proc.new {}
|
77
|
+
end
|
78
|
+
|
79
|
+
def retry_statuses
|
80
|
+
Array(self[:retry_statuses] ||= [])
|
81
|
+
end
|
66
82
|
end
|
67
83
|
|
68
84
|
# Public: Initialize middleware
|
@@ -73,13 +89,14 @@ module Faraday
|
|
73
89
|
# interval_randomness - The maximum random interval amount expressed
|
74
90
|
# as a float between 0 and 1 to use in addition to the
|
75
91
|
# interval. (default: 0)
|
92
|
+
# max_interval - An upper limit for the interval (default: Float::MAX)
|
76
93
|
# backoff_factor - The amount to multiple each successive retry's
|
77
94
|
# interval amount by in order to provide backoff
|
78
95
|
# (default: 1)
|
79
96
|
# exceptions - The list of exceptions to handle. Exceptions can be
|
80
97
|
# given as Class, Module, or String. (default:
|
81
|
-
# [Errno::ETIMEDOUT, Timeout::Error,
|
82
|
-
#
|
98
|
+
# [Errno::ETIMEDOUT, 'Timeout::Error',
|
99
|
+
# Faraday::TimeoutError, Faraday::RetriableResponse])
|
83
100
|
# methods - A list of HTTP methods to retry without calling retry_if. Pass
|
84
101
|
# an empty Array to call retry_if for all exceptions.
|
85
102
|
# (defaults to the idempotent HTTP methods in IDEMPOTENT_METHODS)
|
@@ -89,17 +106,21 @@ module Faraday
|
|
89
106
|
# if the exception produced is non-recoverable or if the
|
90
107
|
# the HTTP method called is not idempotent.
|
91
108
|
# (defaults to return false)
|
109
|
+
# retry_block - block that is executed after every retry. Request environment, middleware options,
|
110
|
+
# current number of retries and the exception is passed to the block as parameters.
|
92
111
|
def initialize(app, options = nil)
|
93
112
|
super(app)
|
94
113
|
@options = Options.from(options)
|
95
114
|
@errmatch = build_exception_matcher(@options.exceptions)
|
96
115
|
end
|
97
116
|
|
98
|
-
def
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
117
|
+
def calculate_sleep_amount(retries, env)
|
118
|
+
retry_after = calculate_retry_after(env)
|
119
|
+
retry_interval = calculate_retry_interval(retries)
|
120
|
+
|
121
|
+
return if retry_after && retry_after > @options.max_interval
|
122
|
+
|
123
|
+
retry_after && retry_after >= retry_interval ? retry_after : retry_interval
|
103
124
|
end
|
104
125
|
|
105
126
|
def call(env)
|
@@ -107,14 +128,25 @@ module Faraday
|
|
107
128
|
request_body = env[:body]
|
108
129
|
begin
|
109
130
|
env[:body] = request_body # after failure env[:body] is set to the response body
|
110
|
-
@app.call(env)
|
131
|
+
@app.call(env).tap do |resp|
|
132
|
+
raise Faraday::RetriableResponse.new(nil, resp) if @options.retry_statuses.include?(resp.status)
|
133
|
+
end
|
111
134
|
rescue @errmatch => exception
|
112
135
|
if retries > 0 && retry_request?(env, exception)
|
113
136
|
retries -= 1
|
114
|
-
|
115
|
-
|
137
|
+
rewind_files(request_body)
|
138
|
+
@options.retry_block.call(env, @options, retries, exception)
|
139
|
+
if (sleep_amount = calculate_sleep_amount(retries + 1, env))
|
140
|
+
sleep sleep_amount
|
141
|
+
retry
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
if exception.is_a?(Faraday::RetriableResponse)
|
146
|
+
exception.response
|
147
|
+
else
|
148
|
+
raise
|
116
149
|
end
|
117
|
-
raise
|
118
150
|
end
|
119
151
|
end
|
120
152
|
|
@@ -144,5 +176,38 @@ module Faraday
|
|
144
176
|
@options.methods.include?(env[:method]) || @options.retry_if.call(env, exception)
|
145
177
|
end
|
146
178
|
|
179
|
+
def rewind_files(body)
|
180
|
+
return unless body.is_a?(Hash)
|
181
|
+
body.each do |_, value|
|
182
|
+
if value.is_a? UploadIO
|
183
|
+
value.rewind
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# MDN spec for Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
189
|
+
def calculate_retry_after(env)
|
190
|
+
response_headers = env[:response_headers]
|
191
|
+
return unless response_headers
|
192
|
+
|
193
|
+
retry_after_value = env[:response_headers]["Retry-After"]
|
194
|
+
|
195
|
+
# Try to parse date from the header value
|
196
|
+
begin
|
197
|
+
datetime = DateTime.rfc2822(retry_after_value)
|
198
|
+
datetime.to_time - Time.now.utc
|
199
|
+
rescue ArgumentError
|
200
|
+
retry_after_value.to_f
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def calculate_retry_interval(retries)
|
205
|
+
retry_index = @options.max - retries
|
206
|
+
current_interval = @options.interval * (@options.backoff_factor ** retry_index)
|
207
|
+
current_interval = [current_interval, @options.max_interval].min
|
208
|
+
random_interval = rand * @options.interval_randomness.to_f * @options.interval
|
209
|
+
|
210
|
+
current_interval + random_interval
|
211
|
+
end
|
147
212
|
end
|
148
213
|
end
|