faraday 0.12.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +350 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +22 -325
  5. data/Rakefile +7 -0
  6. data/examples/client_spec.rb +65 -0
  7. data/examples/client_test.rb +79 -0
  8. data/lib/faraday.rb +120 -188
  9. data/lib/faraday/adapter.rb +84 -22
  10. data/lib/faraday/adapter/em_http.rb +150 -104
  11. data/lib/faraday/adapter/em_http_ssl_patch.rb +24 -18
  12. data/lib/faraday/adapter/em_synchrony.rb +110 -63
  13. data/lib/faraday/adapter/em_synchrony/parallel_manager.rb +18 -15
  14. data/lib/faraday/adapter/excon.rb +98 -54
  15. data/lib/faraday/adapter/httpclient.rb +83 -59
  16. data/lib/faraday/adapter/net_http_persistent.rb +68 -27
  17. data/lib/faraday/adapter/patron.rb +86 -37
  18. data/lib/faraday/adapter/rack.rb +30 -13
  19. data/lib/faraday/adapter/test.rb +103 -62
  20. data/lib/faraday/adapter/typhoeus.rb +7 -115
  21. data/lib/faraday/adapter_registry.rb +30 -0
  22. data/lib/faraday/autoload.rb +46 -36
  23. data/lib/faraday/connection.rb +336 -177
  24. data/lib/faraday/dependency_loader.rb +37 -0
  25. data/lib/faraday/encoders/flat_params_encoder.rb +105 -0
  26. data/lib/faraday/encoders/nested_params_encoder.rb +176 -0
  27. data/lib/faraday/error.rb +126 -37
  28. data/lib/faraday/file_part.rb +128 -0
  29. data/lib/faraday/logging/formatter.rb +105 -0
  30. data/lib/faraday/methods.rb +6 -0
  31. data/lib/faraday/middleware.rb +19 -25
  32. data/lib/faraday/middleware_registry.rb +129 -0
  33. data/lib/faraday/options.rb +39 -193
  34. data/lib/faraday/options/connection_options.rb +22 -0
  35. data/lib/faraday/options/env.rb +181 -0
  36. data/lib/faraday/options/proxy_options.rb +28 -0
  37. data/lib/faraday/options/request_options.rb +22 -0
  38. data/lib/faraday/options/ssl_options.rb +59 -0
  39. data/lib/faraday/param_part.rb +53 -0
  40. data/lib/faraday/parameters.rb +4 -196
  41. data/lib/faraday/rack_builder.rb +77 -63
  42. data/lib/faraday/request.rb +94 -32
  43. data/lib/faraday/request/authorization.rb +44 -30
  44. data/lib/faraday/request/basic_authentication.rb +14 -7
  45. data/lib/faraday/request/instrumentation.rb +45 -27
  46. data/lib/faraday/request/multipart.rb +86 -48
  47. data/lib/faraday/request/retry.rb +209 -134
  48. data/lib/faraday/request/token_authentication.rb +15 -10
  49. data/lib/faraday/request/url_encoded.rb +43 -23
  50. data/lib/faraday/response.rb +27 -23
  51. data/lib/faraday/response/logger.rb +22 -69
  52. data/lib/faraday/response/raise_error.rb +49 -14
  53. data/lib/faraday/utils.rb +38 -247
  54. data/lib/faraday/utils/headers.rb +139 -0
  55. data/lib/faraday/utils/params_hash.rb +61 -0
  56. data/lib/faraday/version.rb +5 -0
  57. data/spec/external_adapters/faraday_specs_setup.rb +14 -0
  58. data/spec/faraday/adapter/em_http_spec.rb +47 -0
  59. data/spec/faraday/adapter/em_synchrony_spec.rb +16 -0
  60. data/spec/faraday/adapter/excon_spec.rb +49 -0
  61. data/spec/faraday/adapter/httpclient_spec.rb +73 -0
  62. data/spec/faraday/adapter/net_http_persistent_spec.rb +57 -0
  63. data/spec/faraday/adapter/net_http_spec.rb +64 -0
  64. data/spec/faraday/adapter/patron_spec.rb +18 -0
  65. data/spec/faraday/adapter/rack_spec.rb +8 -0
  66. data/spec/faraday/adapter/test_spec.rb +260 -0
  67. data/spec/faraday/adapter/typhoeus_spec.rb +7 -0
  68. data/spec/faraday/adapter_registry_spec.rb +28 -0
  69. data/spec/faraday/adapter_spec.rb +55 -0
  70. data/spec/faraday/composite_read_io_spec.rb +80 -0
  71. data/spec/faraday/connection_spec.rb +691 -0
  72. data/spec/faraday/error_spec.rb +60 -0
  73. data/spec/faraday/middleware_spec.rb +52 -0
  74. data/spec/faraday/options/env_spec.rb +70 -0
  75. data/spec/faraday/options/options_spec.rb +297 -0
  76. data/spec/faraday/options/proxy_options_spec.rb +37 -0
  77. data/spec/faraday/options/request_options_spec.rb +19 -0
  78. data/spec/faraday/params_encoders/flat_spec.rb +42 -0
  79. data/spec/faraday/params_encoders/nested_spec.rb +142 -0
  80. data/spec/faraday/rack_builder_spec.rb +345 -0
  81. data/spec/faraday/request/authorization_spec.rb +88 -0
  82. data/spec/faraday/request/instrumentation_spec.rb +76 -0
  83. data/spec/faraday/request/multipart_spec.rb +302 -0
  84. data/spec/faraday/request/retry_spec.rb +242 -0
  85. data/spec/faraday/request/url_encoded_spec.rb +83 -0
  86. data/spec/faraday/request_spec.rb +120 -0
  87. data/spec/faraday/response/logger_spec.rb +220 -0
  88. data/spec/faraday/response/middleware_spec.rb +68 -0
  89. data/spec/faraday/response/raise_error_spec.rb +169 -0
  90. data/spec/faraday/response_spec.rb +75 -0
  91. data/spec/faraday/utils/headers_spec.rb +82 -0
  92. data/spec/faraday/utils_spec.rb +56 -0
  93. data/spec/faraday_spec.rb +37 -0
  94. data/spec/spec_helper.rb +132 -0
  95. data/spec/support/disabling_stub.rb +14 -0
  96. data/spec/support/fake_safe_buffer.rb +15 -0
  97. data/spec/support/helper_methods.rb +133 -0
  98. data/spec/support/shared_examples/adapter.rb +105 -0
  99. data/spec/support/shared_examples/params_encoder.rb +18 -0
  100. data/spec/support/shared_examples/request_method.rb +262 -0
  101. data/spec/support/streaming_response_checker.rb +35 -0
  102. data/spec/support/webmock_rack_app.rb +68 -0
  103. metadata +109 -10
  104. data/lib/faraday/adapter/net_http.rb +0 -135
  105. data/lib/faraday/upload_io.rb +0 -67
@@ -1,41 +1,55 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Faraday
2
- class Request::Authorization < Faraday::Middleware
3
- KEY = "Authorization".freeze unless defined? KEY
4
+ class Request
5
+ # Request middleware for the Authorization HTTP header
6
+ class Authorization < Faraday::Middleware
7
+ unless defined?(::Faraday::Request::Authorization::KEY)
8
+ KEY = 'Authorization'
9
+ end
4
10
 
5
- # Public
6
- def self.header(type, token)
7
- case token
8
- when String, Symbol
9
- "#{type} #{token}"
10
- when Hash
11
- build_hash(type.to_s, token)
12
- else
13
- raise ArgumentError, "Can't build an Authorization #{type} header from #{token.inspect}"
11
+ # @param type [String, Symbol]
12
+ # @param token [String, Symbol, Hash]
13
+ # @return [String] a header value
14
+ def self.header(type, token)
15
+ case token
16
+ when String, Symbol
17
+ "#{type} #{token}"
18
+ when Hash
19
+ build_hash(type.to_s, token)
20
+ else
21
+ raise ArgumentError,
22
+ "Can't build an Authorization #{type}" \
23
+ "header from #{token.inspect}"
24
+ end
14
25
  end
15
- end
16
26
 
17
- # Internal
18
- def self.build_hash(type, hash)
19
- comma = ", "
20
- values = []
21
- hash.each do |key, value|
22
- values << "#{key}=#{value.to_s.inspect}"
27
+ # @param type [String]
28
+ # @param hash [Hash]
29
+ # @return [String] type followed by comma-separated key=value pairs
30
+ # @api private
31
+ def self.build_hash(type, hash)
32
+ comma = ', '
33
+ values = []
34
+ hash.each do |key, value|
35
+ values << "#{key}=#{value.to_s.inspect}"
36
+ end
37
+ "#{type} #{values * comma}"
23
38
  end
24
- "#{type} #{values * comma}"
25
- end
26
39
 
27
- def initialize(app, type, token)
28
- @header_value = self.class.header(type, token)
29
- super(app)
30
- end
40
+ # @param app [#call]
41
+ # @param type [String, Symbol] Type of Authorization
42
+ # @param token [String, Symbol, Hash] Token value for the Authorization
43
+ def initialize(app, type, token)
44
+ @header_value = self.class.header(type, token)
45
+ super(app)
46
+ end
31
47
 
32
- # Public
33
- def call(env)
34
- unless env.request_headers[KEY]
35
- env.request_headers[KEY] = @header_value
48
+ # @param env [Faraday::Env]
49
+ def call(env)
50
+ env.request_headers[KEY] = @header_value unless env.request_headers[KEY]
51
+ @app.call(env)
36
52
  end
37
- @app.call(env)
38
53
  end
39
54
  end
40
55
  end
41
-
@@ -1,13 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'base64'
2
4
 
3
5
  module Faraday
4
- class Request::BasicAuthentication < Request.load_middleware(:authorization)
5
- # Public
6
- def self.header(login, pass)
7
- value = Base64.encode64([login, pass].join(':'))
8
- value.gsub!("\n", '')
9
- super(:Basic, value)
6
+ class Request
7
+ # Authorization middleware for Basic Authentication.
8
+ class BasicAuthentication < load_middleware(:authorization)
9
+ # @param login [String]
10
+ # @param pass [String]
11
+ #
12
+ # @return [String] a Basic Authentication header line
13
+ def self.header(login, pass)
14
+ value = Base64.encode64([login, pass].join(':'))
15
+ value.delete!("\n")
16
+ super(:Basic, value)
17
+ end
10
18
  end
11
19
  end
12
20
  end
13
-
@@ -1,35 +1,53 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Faraday
2
- class Request::Instrumentation < Faraday::Middleware
3
- class Options < Faraday::Options.new(:name, :instrumenter)
4
- def name
5
- self[:name] ||= 'request.faraday'
6
- end
4
+ class Request
5
+ # Middleware for instrumenting Requests.
6
+ class Instrumentation < Faraday::Middleware
7
+ # Options class used in Request::Instrumentation class.
8
+ class Options < Faraday::Options.new(:name, :instrumenter)
9
+ # @return [String]
10
+ def name
11
+ self[:name] ||= 'request.faraday'
12
+ end
7
13
 
8
- def instrumenter
9
- self[:instrumenter] ||= ActiveSupport::Notifications
14
+ # @return [Class]
15
+ def instrumenter
16
+ self[:instrumenter] ||= ActiveSupport::Notifications
17
+ end
10
18
  end
11
- end
12
19
 
13
- # Public: Instruments requests using Active Support.
14
- #
15
- # Measures time spent only for synchronous requests.
16
- #
17
- # Examples
18
- #
19
- # ActiveSupport::Notifications.subscribe('request.faraday') do |name, starts, ends, _, env|
20
- # url = env[:url]
21
- # http_method = env[:method].to_s.upcase
22
- # duration = ends - starts
23
- # $stderr.puts '[%s] %s %s (%.3f s)' % [url.host, http_method, url.request_uri, duration]
24
- # end
25
- def initialize(app, options = nil)
26
- super(app)
27
- @name, @instrumenter = Options.from(options).values_at(:name, :instrumenter)
28
- end
20
+ # Instruments requests using Active Support.
21
+ #
22
+ # Measures time spent only for synchronous requests.
23
+ #
24
+ # @example Using ActiveSupport::Notifications to measure time spent
25
+ # for Faraday requests.
26
+ # ActiveSupport::Notifications
27
+ # .subscribe('request.faraday') do |name, starts, ends, _, env|
28
+ # url = env[:url]
29
+ # http_method = env[:method].to_s.upcase
30
+ # duration = ends - starts
31
+ # $stderr.puts '[%s] %s %s (%.3f s)' %
32
+ # [url.host, http_method, url.request_uri, duration]
33
+ # end
34
+ # @param app [#call]
35
+ # @param options [nil, Hash] Options hash
36
+ # @option options [String] :name ('request.faraday')
37
+ # Name of the instrumenter
38
+ # @option options [Class] :instrumenter (ActiveSupport::Notifications)
39
+ # Active Support instrumenter class.
40
+ def initialize(app, options = nil)
41
+ super(app)
42
+ @name, @instrumenter = Options.from(options)
43
+ .values_at(:name, :instrumenter)
44
+ end
29
45
 
30
- def call(env)
31
- @instrumenter.instrument(@name, env) do
32
- @app.call(env)
46
+ # @param env [Faraday::Env]
47
+ def call(env)
48
+ @instrumenter.instrument(@name, env) do
49
+ @app.call(env)
50
+ end
33
51
  end
34
52
  end
35
53
  end
@@ -1,66 +1,104 @@
1
- require File.expand_path("../url_encoded", __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path('url_encoded', __dir__)
2
4
  require 'securerandom'
3
5
 
4
6
  module Faraday
5
- class Request::Multipart < Request::UrlEncoded
6
- self.mime_type = 'multipart/form-data'.freeze
7
- DEFAULT_BOUNDARY_PREFIX = "-----------RubyMultipartPost".freeze unless defined? DEFAULT_BOUNDARY_PREFIX
8
-
9
- def call(env)
10
- match_content_type(env) do |params|
11
- env.request.boundary ||= unique_boundary
12
- env.request_headers[CONTENT_TYPE] += "; boundary=#{env.request.boundary}"
13
- env.body = create_multipart(env, params)
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'
14
13
  end
15
- @app.call env
16
- end
17
14
 
18
- def process_request?(env)
19
- type = request_type(env)
20
- env.body.respond_to?(:each_key) and !env.body.empty? and (
21
- (type.empty? and has_multipart?(env.body)) or
22
- type == self.class.mime_type
23
- )
24
- end
15
+ def initialize(app = nil, options = {})
16
+ super(app)
17
+ @options = options
18
+ end
25
19
 
26
- def has_multipart?(obj)
27
- # string is an enum in 1.8, returning list of itself
28
- if obj.respond_to?(:each) && !obj.is_a?(String)
29
- (obj.respond_to?(:values) ? obj.values : obj).each do |val|
30
- return true if (val.respond_to?(:content_type) || has_multipart?(val))
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)
31
29
  end
30
+ @app.call env
32
31
  end
33
- false
34
- end
35
32
 
36
- def create_multipart(env, params)
37
- boundary = env.request.boundary
38
- parts = process_params(params) do |key, value|
39
- Faraday::Parts::Part.new(boundary, key, value)
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
40
  end
41
- parts << Faraday::Parts::EpiloguePart.new(boundary)
42
41
 
43
- body = Faraday::CompositeReadIO.new(parts)
44
- env.request_headers[Faraday::Env::ContentLength] = body.length.to_s
45
- return body
46
- end
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
47
54
 
48
- def unique_boundary
49
- "#{DEFAULT_BOUNDARY_PREFIX}-#{SecureRandom.hex}"
50
- end
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)
51
63
 
52
- def process_params(params, prefix = nil, pieces = nil, &block)
53
- params.inject(pieces || []) do |all, (key, value)|
54
- key = "#{prefix}[#{key}]" if prefix
64
+ body = Faraday::CompositeReadIO.new(parts)
65
+ env.request_headers[Faraday::Env::ContentLength] = body.length.to_s
66
+ body
67
+ end
55
68
 
56
- case value
57
- when Array
58
- values = value.inject([]) { |a,v| a << [nil, v] }
59
- process_params(values, key, all, &block)
60
- when Hash
61
- process_params(value, key, all, &block)
69
+ def part(boundary, key, value)
70
+ if value.respond_to?(:to_part)
71
+ value.to_part(boundary, key)
62
72
  else
63
- all << block.call(key, value)
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
64
102
  end
65
103
  end
66
104
  end
@@ -1,164 +1,239 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Faraday
2
- # Catches exceptions and retries each request a limited number of times.
3
- #
4
- # By default, it retries 2 times and handles only timeout exceptions. It can
5
- # be configured with an arbitrary number of retries, a list of exceptions to
6
- # handle, a retry interval, a percentage of randomness to add to the retry
7
- # interval, and a backoff factor.
8
- #
9
- # Examples
10
- #
11
- # Faraday.new do |conn|
12
- # conn.request :retry, max: 2, interval: 0.05,
13
- # interval_randomness: 0.5, backoff_factor: 2,
14
- # exceptions: [CustomException, 'Timeout::Error']
15
- # conn.adapter ...
16
- # end
17
- #
18
- # This example will result in a first interval that is random between 0.05 and 0.075 and a second
19
- # interval that is random between 0.1 and 0.15
20
- #
21
- class Request::Retry < Faraday::Middleware
22
-
23
- IDEMPOTENT_METHODS = [:delete, :get, :head, :options, :put]
24
-
25
- class Options < Faraday::Options.new(:max, :interval, :max_interval, :interval_randomness,
26
- :backoff_factor, :exceptions, :methods, :retry_if)
27
- DEFAULT_CHECK = lambda { |env,exception| false }
28
-
29
- def self.from(value)
30
- if Integer === value
31
- new(value)
32
- else
33
- super(value)
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
34
47
  end
35
- end
36
48
 
37
- def max
38
- (self[:max] ||= 2).to_i
39
- end
49
+ def max
50
+ (self[:max] ||= 2).to_i
51
+ end
40
52
 
41
- def interval
42
- (self[:interval] ||= 0).to_f
43
- end
53
+ def interval
54
+ (self[:interval] ||= 0).to_f
55
+ end
44
56
 
45
- def max_interval
46
- (self[:max_interval] ||= Float::MAX).to_f
47
- end
57
+ def max_interval
58
+ (self[:max_interval] ||= Float::MAX).to_f
59
+ end
48
60
 
49
- def interval_randomness
50
- (self[:interval_randomness] ||= 0).to_f
51
- end
61
+ def interval_randomness
62
+ (self[:interval_randomness] ||= 0).to_f
63
+ end
52
64
 
53
- def backoff_factor
54
- (self[:backoff_factor] ||= 1).to_f
55
- end
65
+ def backoff_factor
66
+ (self[:backoff_factor] ||= 1).to_f
67
+ end
56
68
 
57
- def exceptions
58
- Array(self[:exceptions] ||= [Errno::ETIMEDOUT, 'Timeout::Error',
59
- Error::TimeoutError])
60
- end
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
61
80
 
62
- def methods
63
- Array(self[:methods] ||= IDEMPOTENT_METHODS)
81
+ def retry_block
82
+ self[:retry_block] ||= proc {}
83
+ end
84
+
85
+ def retry_statuses
86
+ Array(self[:retry_statuses] ||= [])
87
+ end
64
88
  end
65
89
 
66
- def retry_if
67
- self[:retry_if] ||= DEFAULT_CHECK
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)
68
126
  end
69
127
 
70
- end
128
+ def calculate_sleep_amount(retries, env)
129
+ retry_after = calculate_retry_after(env)
130
+ retry_interval = calculate_retry_interval(retries)
71
131
 
72
- # Public: Initialize middleware
73
- #
74
- # Options:
75
- # max - Maximum number of retries (default: 2)
76
- # interval - Pause in seconds between retries (default: 0)
77
- # interval_randomness - The maximum random interval amount expressed
78
- # as a float between 0 and 1 to use in addition to the
79
- # interval. (default: 0)
80
- # max_interval - An upper limit for the interval (default: Float::MAX)
81
- # backoff_factor - The amount to multiple each successive retry's
82
- # interval amount by in order to provide backoff
83
- # (default: 1)
84
- # exceptions - The list of exceptions to handle. Exceptions can be
85
- # given as Class, Module, or String. (default:
86
- # [Errno::ETIMEDOUT, Timeout::Error,
87
- # Error::TimeoutError])
88
- # methods - A list of HTTP methods to retry without calling retry_if. Pass
89
- # an empty Array to call retry_if for all exceptions.
90
- # (defaults to the idempotent HTTP methods in IDEMPOTENT_METHODS)
91
- # retry_if - block that will receive the env object and the exception raised
92
- # and should decide if the code should retry still the action or
93
- # not independent of the retry count. This would be useful
94
- # if the exception produced is non-recoverable or if the
95
- # the HTTP method called is not idempotent.
96
- # (defaults to return false)
97
- def initialize(app, options = nil)
98
- super(app)
99
- @options = Options.from(options)
100
- @errmatch = build_exception_matcher(@options.exceptions)
101
- end
132
+ return if retry_after && retry_after > @options.max_interval
102
133
 
103
- def sleep_amount(retries)
104
- retry_index = @options.max - retries
105
- current_interval = @options.interval * (@options.backoff_factor ** retry_index)
106
- current_interval = [current_interval, @options.max_interval].min
107
- random_interval = rand * @options.interval_randomness.to_f * @options.interval
108
- current_interval + random_interval
109
- end
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
110
163
 
111
- def call(env)
112
- retries = @options.max
113
- request_body = env[:body]
114
- begin
115
- env[:body] = request_body # after failure env[:body] is set to the response body
116
- @app.call(env)
117
- rescue @errmatch => exception
118
- if retries > 0 && retry_request?(env, exception)
119
- retries -= 1
120
- rewind_files(request_body)
121
- sleep sleep_amount(retries + 1)
122
- retry
123
- end
124
- raise
164
+ raise unless e.is_a?(Faraday::RetriableResponse)
165
+
166
+ e.response
167
+ end
125
168
  end
126
- end
127
169
 
128
- # Private: construct an exception matcher object.
129
- #
130
- # An exception matcher for the rescue clause can usually be any object that
131
- # responds to `===`, but for Ruby 1.8 it has to be a Class or Module.
132
- def build_exception_matcher(exceptions)
133
- matcher = Module.new
134
- (class << matcher; self; end).class_eval do
135
- define_method(:===) do |error|
136
- exceptions.any? do |ex|
137
- if ex.is_a? Module
138
- error.is_a? ex
139
- else
140
- error.class.to_s == ex.to_s
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
+ error.class.to_s == ex.to_s
188
+ end
141
189
  end
142
190
  end
143
191
  end
192
+ matcher
144
193
  end
145
- matcher
146
- end
147
194
 
148
- private
195
+ private
149
196
 
150
- def retry_request?(env, exception)
151
- @options.methods.include?(env[:method]) || @options.retry_if.call(env, exception)
152
- end
197
+ def retry_request?(env, exception)
198
+ @options.methods.include?(env[:method]) ||
199
+ @options.retry_if.call(env, exception)
200
+ end
153
201
 
154
- def rewind_files(body)
155
- return unless body.is_a?(Hash)
156
- body.each do |_, value|
157
- if value.is_a? UploadIO
158
- value.rewind
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)
159
207
  end
160
208
  end
161
- end
162
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
163
238
  end
164
239
  end