faraday 1.10.6 → 2.0.0.alpha.pre.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 +4 -4
- data/CHANGELOG.md +111 -1
- data/README.md +16 -9
- data/examples/client_test.rb +1 -1
- data/lib/faraday/adapter/test.rb +2 -0
- data/lib/faraday/adapter.rb +0 -5
- data/lib/faraday/connection.rb +3 -86
- data/lib/faraday/encoders/nested_params_encoder.rb +3 -12
- data/lib/faraday/error.rb +7 -0
- data/lib/faraday/file_part.rb +122 -0
- data/lib/faraday/logging/formatter.rb +1 -0
- data/lib/faraday/middleware.rb +0 -1
- data/lib/faraday/middleware_registry.rb +15 -79
- data/lib/faraday/options.rb +3 -3
- data/lib/faraday/param_part.rb +53 -0
- data/lib/faraday/rack_builder.rb +1 -1
- data/lib/faraday/request/authorization.rb +26 -40
- data/lib/faraday/request/instrumentation.rb +2 -0
- data/lib/faraday/request/multipart.rb +108 -0
- data/lib/faraday/request/retry.rb +241 -0
- data/lib/faraday/request/url_encoded.rb +2 -0
- data/lib/faraday/request.rb +8 -24
- data/lib/faraday/response/json.rb +4 -4
- data/lib/faraday/response/logger.rb +2 -0
- data/lib/faraday/response/raise_error.rb +9 -1
- data/lib/faraday/response.rb +7 -20
- data/lib/faraday/utils/headers.rb +1 -1
- data/lib/faraday/utils.rb +9 -4
- data/lib/faraday/version.rb +1 -1
- data/lib/faraday.rb +6 -45
- data/spec/faraday/connection_spec.rb +78 -97
- data/spec/faraday/options/env_spec.rb +2 -2
- data/spec/faraday/params_encoders/nested_spec.rb +0 -30
- data/spec/faraday/rack_builder_spec.rb +5 -43
- data/spec/faraday/request/authorization_spec.rb +14 -36
- data/spec/faraday/request/instrumentation_spec.rb +5 -7
- data/spec/faraday/request/multipart_spec.rb +302 -0
- data/spec/faraday/request/retry_spec.rb +254 -0
- data/spec/faraday/request_spec.rb +0 -11
- data/spec/faraday/response/json_spec.rb +4 -6
- data/spec/faraday/response/raise_error_spec.rb +7 -4
- data/spec/faraday/utils_spec.rb +1 -1
- data/spec/faraday_spec.rb +4 -8
- data/spec/spec_helper.rb +0 -2
- data/spec/support/fake_safe_buffer.rb +1 -1
- data/spec/support/shared_examples/request_method.rb +5 -5
- metadata +25 -156
- data/lib/faraday/adapter/typhoeus.rb +0 -15
- data/lib/faraday/autoload.rb +0 -89
- data/lib/faraday/dependency_loader.rb +0 -39
- data/lib/faraday/deprecate.rb +0 -110
- data/lib/faraday/request/basic_authentication.rb +0 -20
- data/lib/faraday/request/token_authentication.rb +0 -20
- data/spec/faraday/adapter/em_http_spec.rb +0 -49
- data/spec/faraday/adapter/em_synchrony_spec.rb +0 -18
- data/spec/faraday/adapter/excon_spec.rb +0 -49
- data/spec/faraday/adapter/httpclient_spec.rb +0 -73
- data/spec/faraday/adapter/net_http_spec.rb +0 -64
- data/spec/faraday/adapter/patron_spec.rb +0 -18
- data/spec/faraday/adapter/rack_spec.rb +0 -8
- data/spec/faraday/adapter/typhoeus_spec.rb +0 -7
- data/spec/faraday/deprecate_spec.rb +0 -147
- data/spec/faraday/response/middleware_spec.rb +0 -68
- data/spec/support/webmock_rack_app.rb +0 -68
|
@@ -6,59 +6,26 @@ module Faraday
|
|
|
6
6
|
# Adds the ability for other modules to register and lookup
|
|
7
7
|
# middleware classes.
|
|
8
8
|
module MiddlewareRegistry
|
|
9
|
+
def registered_middleware
|
|
10
|
+
@registered_middleware ||= {}
|
|
11
|
+
end
|
|
12
|
+
|
|
9
13
|
# Register middleware class(es) on the current module.
|
|
10
14
|
#
|
|
11
|
-
# @param
|
|
12
|
-
# @param mapping [Hash{
|
|
13
|
-
# Symbol => Module,
|
|
14
|
-
# Symbol => Array<Module, Symbol, String>,
|
|
15
|
-
# }] Middleware mapping from a lookup symbol to a reference to the
|
|
16
|
-
# middleware.
|
|
17
|
-
# Classes can be expressed as:
|
|
18
|
-
# - a fully qualified constant
|
|
19
|
-
# - a Symbol
|
|
20
|
-
# - a Proc that will be lazily called to return the former
|
|
21
|
-
# - an array is given, its first element is the constant or symbol,
|
|
22
|
-
# and its second is a file to `require`.
|
|
15
|
+
# @param mappings [Hash] Middleware mappings from a lookup symbol to a middleware class.
|
|
23
16
|
# @return [void]
|
|
24
17
|
#
|
|
25
18
|
# @example Lookup by a constant
|
|
26
19
|
#
|
|
27
20
|
# module Faraday
|
|
28
|
-
# class Whatever
|
|
21
|
+
# class Whatever < Middleware
|
|
29
22
|
# # Middleware looked up by :foo returns Faraday::Whatever::Foo.
|
|
30
|
-
# register_middleware
|
|
31
|
-
# end
|
|
32
|
-
# end
|
|
33
|
-
#
|
|
34
|
-
# @example Lookup by a symbol
|
|
35
|
-
#
|
|
36
|
-
# module Faraday
|
|
37
|
-
# class Whatever
|
|
38
|
-
# # Middleware looked up by :bar returns
|
|
39
|
-
# # Faraday::Whatever.const_get(:Bar)
|
|
40
|
-
# register_middleware bar: :Bar
|
|
41
|
-
# end
|
|
42
|
-
# end
|
|
43
|
-
#
|
|
44
|
-
# @example Lookup by a symbol and string in an array
|
|
45
|
-
#
|
|
46
|
-
# module Faraday
|
|
47
|
-
# class Whatever
|
|
48
|
-
# # Middleware looked up by :baz requires 'baz' and returns
|
|
49
|
-
# # Faraday::Whatever.const_get(:Baz)
|
|
50
|
-
# register_middleware baz: [:Baz, 'baz']
|
|
23
|
+
# register_middleware(foo: Whatever)
|
|
51
24
|
# end
|
|
52
25
|
# end
|
|
53
|
-
|
|
54
|
-
def register_middleware(autoload_path = nil, mapping = nil)
|
|
55
|
-
if mapping.nil?
|
|
56
|
-
mapping = autoload_path
|
|
57
|
-
autoload_path = nil
|
|
58
|
-
end
|
|
26
|
+
def register_middleware(**mappings)
|
|
59
27
|
middleware_mutex do
|
|
60
|
-
|
|
61
|
-
(@registered_middleware ||= {}).update(mapping)
|
|
28
|
+
registered_middleware.update(mappings)
|
|
62
29
|
end
|
|
63
30
|
end
|
|
64
31
|
|
|
@@ -66,7 +33,7 @@ module Faraday
|
|
|
66
33
|
#
|
|
67
34
|
# @param key [Symbol] key for the registered middleware.
|
|
68
35
|
def unregister_middleware(key)
|
|
69
|
-
|
|
36
|
+
registered_middleware.delete(key)
|
|
70
37
|
end
|
|
71
38
|
|
|
72
39
|
# Lookup middleware class with a registered Symbol shortcut.
|
|
@@ -78,16 +45,15 @@ module Faraday
|
|
|
78
45
|
# @example
|
|
79
46
|
#
|
|
80
47
|
# module Faraday
|
|
81
|
-
# class Whatever
|
|
82
|
-
# register_middleware
|
|
48
|
+
# class Whatever < Middleware
|
|
49
|
+
# register_middleware(foo: Whatever)
|
|
83
50
|
# end
|
|
84
51
|
# end
|
|
85
52
|
#
|
|
86
|
-
# Faraday::
|
|
87
|
-
# # => Faraday::Whatever
|
|
88
|
-
#
|
|
53
|
+
# Faraday::Middleware.lookup_middleware(:foo)
|
|
54
|
+
# # => Faraday::Whatever
|
|
89
55
|
def lookup_middleware(key)
|
|
90
|
-
|
|
56
|
+
registered_middleware[key] ||
|
|
91
57
|
raise(Faraday::Error, "#{key.inspect} is not registered on #{self}")
|
|
92
58
|
end
|
|
93
59
|
|
|
@@ -95,35 +61,5 @@ module Faraday
|
|
|
95
61
|
@middleware_mutex ||= Monitor.new
|
|
96
62
|
@middleware_mutex.synchronize(&block)
|
|
97
63
|
end
|
|
98
|
-
|
|
99
|
-
def fetch_middleware(key)
|
|
100
|
-
defined?(@registered_middleware) && @registered_middleware[key]
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def load_middleware(key)
|
|
104
|
-
value = fetch_middleware(key)
|
|
105
|
-
case value
|
|
106
|
-
when Module
|
|
107
|
-
value
|
|
108
|
-
when Symbol, String
|
|
109
|
-
middleware_mutex do
|
|
110
|
-
@registered_middleware[key] = const_get(value)
|
|
111
|
-
end
|
|
112
|
-
when Proc
|
|
113
|
-
middleware_mutex do
|
|
114
|
-
@registered_middleware[key] = value.call
|
|
115
|
-
end
|
|
116
|
-
when Array
|
|
117
|
-
middleware_mutex do
|
|
118
|
-
const, path = value
|
|
119
|
-
if (root = @middleware_autoload_path)
|
|
120
|
-
path = "#{root}/#{path}"
|
|
121
|
-
end
|
|
122
|
-
require(path)
|
|
123
|
-
@registered_middleware[key] = const
|
|
124
|
-
end
|
|
125
|
-
load_middleware(key)
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
64
|
end
|
|
129
65
|
end
|
data/lib/faraday/options.rb
CHANGED
|
@@ -104,7 +104,7 @@ module Faraday
|
|
|
104
104
|
|
|
105
105
|
# Public
|
|
106
106
|
def each_key(&block)
|
|
107
|
-
return to_enum(:each_key) unless
|
|
107
|
+
return to_enum(:each_key) unless block
|
|
108
108
|
|
|
109
109
|
keys.each(&block)
|
|
110
110
|
end
|
|
@@ -118,7 +118,7 @@ module Faraday
|
|
|
118
118
|
|
|
119
119
|
# Public
|
|
120
120
|
def each_value(&block)
|
|
121
|
-
return to_enum(:each_value) unless
|
|
121
|
+
return to_enum(:each_value) unless block
|
|
122
122
|
|
|
123
123
|
values.each(&block)
|
|
124
124
|
end
|
|
@@ -168,7 +168,7 @@ module Faraday
|
|
|
168
168
|
end
|
|
169
169
|
|
|
170
170
|
def self.memoized(key, &block)
|
|
171
|
-
unless
|
|
171
|
+
unless block
|
|
172
172
|
raise ArgumentError, '#memoized must be called with a block'
|
|
173
173
|
end
|
|
174
174
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Faraday
|
|
4
|
+
# Multipart value used to POST data with a content type.
|
|
5
|
+
class ParamPart
|
|
6
|
+
# @param value [String] Uploaded content as a String.
|
|
7
|
+
# @param content_type [String] String content type of the value.
|
|
8
|
+
# @param content_id [String] Optional String of this value's Content-ID.
|
|
9
|
+
#
|
|
10
|
+
# @return [Faraday::ParamPart]
|
|
11
|
+
def initialize(value, content_type, content_id = nil)
|
|
12
|
+
@value = value
|
|
13
|
+
@content_type = content_type
|
|
14
|
+
@content_id = content_id
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Converts this value to a form part.
|
|
18
|
+
#
|
|
19
|
+
# @param boundary [String] String multipart boundary that must not exist in
|
|
20
|
+
# the content exactly.
|
|
21
|
+
# @param key [String] String key name for this value.
|
|
22
|
+
#
|
|
23
|
+
# @return [Faraday::Parts::Part]
|
|
24
|
+
def to_part(boundary, key)
|
|
25
|
+
Faraday::Parts::Part.new(boundary, key, value, headers)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns a Hash of String key/value pairs.
|
|
29
|
+
#
|
|
30
|
+
# @return [Hash]
|
|
31
|
+
def headers
|
|
32
|
+
{
|
|
33
|
+
'Content-Type' => content_type,
|
|
34
|
+
'Content-ID' => content_id
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The content to upload.
|
|
39
|
+
#
|
|
40
|
+
# @return [String]
|
|
41
|
+
attr_reader :value
|
|
42
|
+
|
|
43
|
+
# The value's content type.
|
|
44
|
+
#
|
|
45
|
+
# @return [String]
|
|
46
|
+
attr_reader :content_type
|
|
47
|
+
|
|
48
|
+
# The value's content ID, if given.
|
|
49
|
+
#
|
|
50
|
+
# @return [String, nil]
|
|
51
|
+
attr_reader :content_id
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/faraday/rack_builder.rb
CHANGED
|
@@ -1,53 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'base64'
|
|
4
|
-
|
|
5
3
|
module Faraday
|
|
6
4
|
class Request
|
|
7
5
|
# Request middleware for the Authorization HTTP header
|
|
8
6
|
class Authorization < Faraday::Middleware
|
|
9
|
-
|
|
10
|
-
KEY = 'Authorization'
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# @param type [String, Symbol]
|
|
14
|
-
# @param token [String, Symbol, Hash]
|
|
15
|
-
# @return [String] a header value
|
|
16
|
-
def self.header(type, token)
|
|
17
|
-
case token
|
|
18
|
-
when String, Symbol, Proc
|
|
19
|
-
token = token.call if token.is_a?(Proc)
|
|
20
|
-
"#{type} #{token}"
|
|
21
|
-
when Hash
|
|
22
|
-
build_hash(type.to_s, token)
|
|
23
|
-
else
|
|
24
|
-
raise ArgumentError,
|
|
25
|
-
"Can't build an Authorization #{type}" \
|
|
26
|
-
"header from #{token.inspect}"
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# @param type [String]
|
|
31
|
-
# @param hash [Hash]
|
|
32
|
-
# @return [String] type followed by comma-separated key=value pairs
|
|
33
|
-
# @api private
|
|
34
|
-
def self.build_hash(type, hash)
|
|
35
|
-
comma = ', '
|
|
36
|
-
values = []
|
|
37
|
-
hash.each do |key, value|
|
|
38
|
-
value = value.call if value.is_a?(Proc)
|
|
39
|
-
values << "#{key}=#{value.to_s.inspect}"
|
|
40
|
-
end
|
|
41
|
-
"#{type} #{values * comma}"
|
|
42
|
-
end
|
|
7
|
+
KEY = 'Authorization'
|
|
43
8
|
|
|
44
9
|
# @param app [#call]
|
|
45
10
|
# @param type [String, Symbol] Type of Authorization
|
|
46
|
-
# @param
|
|
11
|
+
# @param params [Array<String, Proc>] parameters to build the Authorization header.
|
|
12
|
+
# If the type is `:basic`, then these can be a login and password pair.
|
|
13
|
+
# Otherwise, a single value is expected that will be appended after the type.
|
|
47
14
|
# This value can be a proc, in which case it will be invoked on each request.
|
|
48
|
-
def initialize(app, type,
|
|
15
|
+
def initialize(app, type, *params)
|
|
49
16
|
@type = type
|
|
50
|
-
@
|
|
17
|
+
@params = params
|
|
51
18
|
super(app)
|
|
52
19
|
end
|
|
53
20
|
|
|
@@ -55,8 +22,27 @@ module Faraday
|
|
|
55
22
|
def on_request(env)
|
|
56
23
|
return if env.request_headers[KEY]
|
|
57
24
|
|
|
58
|
-
env.request_headers[KEY] =
|
|
25
|
+
env.request_headers[KEY] = header_from(@type, *@params)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# @param type [String, Symbol]
|
|
31
|
+
# @param params [Array]
|
|
32
|
+
# @return [String] a header value
|
|
33
|
+
def header_from(type, *params)
|
|
34
|
+
if type.to_s.casecmp('basic').zero? && params.size == 2
|
|
35
|
+
Utils.basic_header_from(*params)
|
|
36
|
+
elsif params.size != 1
|
|
37
|
+
raise ArgumentError, "Unexpected params received (got #{params.size} instead of 1)"
|
|
38
|
+
else
|
|
39
|
+
value = params.first
|
|
40
|
+
value = value.call if value.is_a?(Proc)
|
|
41
|
+
"#{type} #{value}"
|
|
42
|
+
end
|
|
59
43
|
end
|
|
60
44
|
end
|
|
61
45
|
end
|
|
62
46
|
end
|
|
47
|
+
|
|
48
|
+
Faraday::Request.register_middleware(authorization: Faraday::Request::Authorization)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require File.expand_path('url_encoded', __dir__)
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
|
|
6
|
+
module Faraday
|
|
7
|
+
class Request
|
|
8
|
+
# Middleware for supporting multi-part requests.
|
|
9
|
+
class Multipart < UrlEncoded
|
|
10
|
+
self.mime_type = 'multipart/form-data'
|
|
11
|
+
unless defined?(::Faraday::Request::Multipart::DEFAULT_BOUNDARY_PREFIX)
|
|
12
|
+
DEFAULT_BOUNDARY_PREFIX = '-----------RubyMultipartPost'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(app = nil, options = {})
|
|
16
|
+
super(app)
|
|
17
|
+
@options = options
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Checks for files in the payload, otherwise leaves everything untouched.
|
|
21
|
+
#
|
|
22
|
+
# @param env [Faraday::Env]
|
|
23
|
+
def call(env)
|
|
24
|
+
match_content_type(env) do |params|
|
|
25
|
+
env.request.boundary ||= unique_boundary
|
|
26
|
+
env.request_headers[CONTENT_TYPE] +=
|
|
27
|
+
"; boundary=#{env.request.boundary}"
|
|
28
|
+
env.body = create_multipart(env, params)
|
|
29
|
+
end
|
|
30
|
+
@app.call env
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param env [Faraday::Env]
|
|
34
|
+
def process_request?(env)
|
|
35
|
+
type = request_type(env)
|
|
36
|
+
env.body.respond_to?(:each_key) && !env.body.empty? && (
|
|
37
|
+
(type.empty? && has_multipart?(env.body)) ||
|
|
38
|
+
(type == self.class.mime_type)
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns true if obj is an enumerable with values that are multipart.
|
|
43
|
+
#
|
|
44
|
+
# @param obj [Object]
|
|
45
|
+
# @return [Boolean]
|
|
46
|
+
def has_multipart?(obj) # rubocop:disable Naming/PredicateName
|
|
47
|
+
if obj.respond_to?(:each)
|
|
48
|
+
(obj.respond_to?(:values) ? obj.values : obj).each do |val|
|
|
49
|
+
return true if val.respond_to?(:content_type) || has_multipart?(val)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @param env [Faraday::Env]
|
|
56
|
+
# @param params [Hash]
|
|
57
|
+
def create_multipart(env, params)
|
|
58
|
+
boundary = env.request.boundary
|
|
59
|
+
parts = process_params(params) do |key, value|
|
|
60
|
+
part(boundary, key, value)
|
|
61
|
+
end
|
|
62
|
+
parts << Faraday::Parts::EpiloguePart.new(boundary)
|
|
63
|
+
|
|
64
|
+
body = Faraday::CompositeReadIO.new(parts)
|
|
65
|
+
env.request_headers[Faraday::Env::ContentLength] = body.length.to_s
|
|
66
|
+
body
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def part(boundary, key, value)
|
|
70
|
+
if value.respond_to?(:to_part)
|
|
71
|
+
value.to_part(boundary, key)
|
|
72
|
+
else
|
|
73
|
+
Faraday::Parts::Part.new(boundary, key, value)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @return [String]
|
|
78
|
+
def unique_boundary
|
|
79
|
+
"#{DEFAULT_BOUNDARY_PREFIX}-#{SecureRandom.hex}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @param params [Hash]
|
|
83
|
+
# @param prefix [String]
|
|
84
|
+
# @param pieces [Array]
|
|
85
|
+
def process_params(params, prefix = nil, pieces = nil, &block)
|
|
86
|
+
params.inject(pieces || []) do |all, (key, value)|
|
|
87
|
+
if prefix
|
|
88
|
+
key = @options[:flat_encode] ? prefix.to_s : "#{prefix}[#{key}]"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
case value
|
|
92
|
+
when Array
|
|
93
|
+
values = value.inject([]) { |a, v| a << [nil, v] }
|
|
94
|
+
process_params(values, key, all, &block)
|
|
95
|
+
when Hash
|
|
96
|
+
process_params(value, key, all, &block)
|
|
97
|
+
else
|
|
98
|
+
# rubocop:disable Performance/RedundantBlockCall
|
|
99
|
+
all << block.call(key, value)
|
|
100
|
+
# rubocop:enable Performance/RedundantBlockCall
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
Faraday::Request.register_middleware(multipart: Faraday::Request::Multipart)
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Faraday
|
|
4
|
+
class Request
|
|
5
|
+
# Catches exceptions and retries each request a limited number of times.
|
|
6
|
+
#
|
|
7
|
+
# By default, it retries 2 times and handles only timeout exceptions. It can
|
|
8
|
+
# be configured with an arbitrary number of retries, a list of exceptions to
|
|
9
|
+
# handle, a retry interval, a percentage of randomness to add to the retry
|
|
10
|
+
# interval, and a backoff factor.
|
|
11
|
+
#
|
|
12
|
+
# @example Configure Retry middleware using intervals
|
|
13
|
+
# Faraday.new do |conn|
|
|
14
|
+
# conn.request(:retry, max: 2,
|
|
15
|
+
# interval: 0.05,
|
|
16
|
+
# interval_randomness: 0.5,
|
|
17
|
+
# backoff_factor: 2,
|
|
18
|
+
# exceptions: [CustomException, 'Timeout::Error'])
|
|
19
|
+
#
|
|
20
|
+
# conn.adapter(:net_http) # NB: Last middleware must be the adapter
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# This example will result in a first interval that is random between 0.05
|
|
24
|
+
# and 0.075 and a second interval that is random between 0.1 and 0.125.
|
|
25
|
+
class Retry < Faraday::Middleware
|
|
26
|
+
DEFAULT_EXCEPTIONS = [
|
|
27
|
+
Errno::ETIMEDOUT, 'Timeout::Error',
|
|
28
|
+
Faraday::TimeoutError, Faraday::RetriableResponse
|
|
29
|
+
].freeze
|
|
30
|
+
IDEMPOTENT_METHODS = %i[delete get head options put].freeze
|
|
31
|
+
|
|
32
|
+
# Options contains the configurable parameters for the Retry middleware.
|
|
33
|
+
class Options < Faraday::Options.new(:max, :interval, :max_interval,
|
|
34
|
+
:interval_randomness,
|
|
35
|
+
:backoff_factor, :exceptions,
|
|
36
|
+
:methods, :retry_if, :retry_block,
|
|
37
|
+
:retry_statuses)
|
|
38
|
+
|
|
39
|
+
DEFAULT_CHECK = ->(_env, _exception) { false }
|
|
40
|
+
|
|
41
|
+
def self.from(value)
|
|
42
|
+
if value.is_a?(Integer)
|
|
43
|
+
new(value)
|
|
44
|
+
else
|
|
45
|
+
super(value)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def max
|
|
50
|
+
(self[:max] ||= 2).to_i
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def interval
|
|
54
|
+
(self[:interval] ||= 0).to_f
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def max_interval
|
|
58
|
+
(self[:max_interval] ||= Float::MAX).to_f
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def interval_randomness
|
|
62
|
+
(self[:interval_randomness] ||= 0).to_f
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def backoff_factor
|
|
66
|
+
(self[:backoff_factor] ||= 1).to_f
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def exceptions
|
|
70
|
+
Array(self[:exceptions] ||= DEFAULT_EXCEPTIONS)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def methods
|
|
74
|
+
Array(self[:methods] ||= IDEMPOTENT_METHODS)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def retry_if
|
|
78
|
+
self[:retry_if] ||= DEFAULT_CHECK
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def retry_block
|
|
82
|
+
self[:retry_block] ||= proc {}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def retry_statuses
|
|
86
|
+
Array(self[:retry_statuses] ||= [])
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @param app [#call]
|
|
91
|
+
# @param options [Hash]
|
|
92
|
+
# @option options [Integer] :max (2) Maximum number of retries
|
|
93
|
+
# @option options [Integer] :interval (0) Pause in seconds between retries
|
|
94
|
+
# @option options [Integer] :interval_randomness (0) The maximum random
|
|
95
|
+
# interval amount expressed as a float between
|
|
96
|
+
# 0 and 1 to use in addition to the interval.
|
|
97
|
+
# @option options [Integer] :max_interval (Float::MAX) An upper limit
|
|
98
|
+
# for the interval
|
|
99
|
+
# @option options [Integer] :backoff_factor (1) The amount to multiply
|
|
100
|
+
# each successive retry's interval amount by in order to provide backoff
|
|
101
|
+
# @option options [Array] :exceptions ([ Errno::ETIMEDOUT,
|
|
102
|
+
# 'Timeout::Error', Faraday::TimeoutError, Faraday::RetriableResponse])
|
|
103
|
+
# The list of exceptions to handle. Exceptions can be given as
|
|
104
|
+
# Class, Module, or String.
|
|
105
|
+
# @option options [Array] :methods (the idempotent HTTP methods
|
|
106
|
+
# in IDEMPOTENT_METHODS) A list of HTTP methods to retry without
|
|
107
|
+
# calling retry_if. Pass an empty Array to call retry_if
|
|
108
|
+
# for all exceptions.
|
|
109
|
+
# @option options [Block] :retry_if (false) block that will receive
|
|
110
|
+
# the env object and the exception raised
|
|
111
|
+
# and should decide if the code should retry still the action or
|
|
112
|
+
# not independent of the retry count. This would be useful
|
|
113
|
+
# if the exception produced is non-recoverable or if the
|
|
114
|
+
# the HTTP method called is not idempotent.
|
|
115
|
+
# @option options [Block] :retry_block block that is executed before
|
|
116
|
+
# every retry. Request environment, middleware options, current number
|
|
117
|
+
# of retries and the exception is passed to the block as parameters.
|
|
118
|
+
# @option options [Array] :retry_statuses Array of Integer HTTP status
|
|
119
|
+
# codes or a single Integer value that determines whether to raise
|
|
120
|
+
# a Faraday::RetriableResponse exception based on the HTTP status code
|
|
121
|
+
# of an HTTP response.
|
|
122
|
+
def initialize(app, options = nil)
|
|
123
|
+
super(app)
|
|
124
|
+
@options = Options.from(options)
|
|
125
|
+
@errmatch = build_exception_matcher(@options.exceptions)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def calculate_sleep_amount(retries, env)
|
|
129
|
+
retry_after = calculate_retry_after(env)
|
|
130
|
+
retry_interval = calculate_retry_interval(retries)
|
|
131
|
+
|
|
132
|
+
return if retry_after && retry_after > @options.max_interval
|
|
133
|
+
|
|
134
|
+
if retry_after && retry_after >= retry_interval
|
|
135
|
+
retry_after
|
|
136
|
+
else
|
|
137
|
+
retry_interval
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @param env [Faraday::Env]
|
|
142
|
+
def call(env)
|
|
143
|
+
retries = @options.max
|
|
144
|
+
request_body = env[:body]
|
|
145
|
+
begin
|
|
146
|
+
# after failure env[:body] is set to the response body
|
|
147
|
+
env[:body] = request_body
|
|
148
|
+
@app.call(env).tap do |resp|
|
|
149
|
+
if @options.retry_statuses.include?(resp.status)
|
|
150
|
+
raise Faraday::RetriableResponse.new(nil, resp)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
rescue @errmatch => e
|
|
154
|
+
if retries.positive? && retry_request?(env, e)
|
|
155
|
+
retries -= 1
|
|
156
|
+
rewind_files(request_body)
|
|
157
|
+
@options.retry_block.call(env, @options, retries, e)
|
|
158
|
+
if (sleep_amount = calculate_sleep_amount(retries + 1, env))
|
|
159
|
+
sleep sleep_amount
|
|
160
|
+
retry
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
raise unless e.is_a?(Faraday::RetriableResponse)
|
|
165
|
+
|
|
166
|
+
e.response
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# An exception matcher for the rescue clause can usually be any object
|
|
171
|
+
# that responds to `===`, but for Ruby 1.8 it has to be a Class or Module.
|
|
172
|
+
#
|
|
173
|
+
# @param exceptions [Array]
|
|
174
|
+
# @api private
|
|
175
|
+
# @return [Module] an exception matcher
|
|
176
|
+
def build_exception_matcher(exceptions)
|
|
177
|
+
matcher = Module.new
|
|
178
|
+
(
|
|
179
|
+
class << matcher
|
|
180
|
+
self
|
|
181
|
+
end).class_eval do
|
|
182
|
+
define_method(:===) do |error|
|
|
183
|
+
exceptions.any? do |ex|
|
|
184
|
+
if ex.is_a? Module
|
|
185
|
+
error.is_a? ex
|
|
186
|
+
else
|
|
187
|
+
Object.const_defined?(ex.to_s) && error.is_a?(Object.const_get(ex.to_s))
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
matcher
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
private
|
|
196
|
+
|
|
197
|
+
def retry_request?(env, exception)
|
|
198
|
+
@options.methods.include?(env[:method]) ||
|
|
199
|
+
@options.retry_if.call(env, exception)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def rewind_files(body)
|
|
203
|
+
return unless body.is_a?(Hash)
|
|
204
|
+
|
|
205
|
+
body.each do |_, value|
|
|
206
|
+
value.rewind if value.is_a?(UploadIO)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# MDN spec for Retry-After header:
|
|
211
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
|
212
|
+
def calculate_retry_after(env)
|
|
213
|
+
response_headers = env[:response_headers]
|
|
214
|
+
return unless response_headers
|
|
215
|
+
|
|
216
|
+
retry_after_value = env[:response_headers]['Retry-After']
|
|
217
|
+
|
|
218
|
+
# Try to parse date from the header value
|
|
219
|
+
begin
|
|
220
|
+
datetime = DateTime.rfc2822(retry_after_value)
|
|
221
|
+
datetime.to_time - Time.now.utc
|
|
222
|
+
rescue ArgumentError
|
|
223
|
+
retry_after_value.to_f
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def calculate_retry_interval(retries)
|
|
228
|
+
retry_index = @options.max - retries
|
|
229
|
+
current_interval = @options.interval *
|
|
230
|
+
(@options.backoff_factor**retry_index)
|
|
231
|
+
current_interval = [current_interval, @options.max_interval].min
|
|
232
|
+
random_interval = rand * @options.interval_randomness.to_f *
|
|
233
|
+
@options.interval
|
|
234
|
+
|
|
235
|
+
current_interval + random_interval
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
Faraday::Request.register_middleware(retry: Faraday::Request::Retry)
|