faraday 0.11.0 → 0.17.4

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 (65) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +232 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +151 -13
  5. data/Rakefile +13 -0
  6. data/lib/faraday/adapter/em_http.rb +9 -9
  7. data/lib/faraday/adapter/em_synchrony.rb +5 -5
  8. data/lib/faraday/adapter/excon.rb +6 -4
  9. data/lib/faraday/adapter/httpclient.rb +5 -5
  10. data/lib/faraday/adapter/net_http.rb +27 -9
  11. data/lib/faraday/adapter/net_http_persistent.rb +34 -16
  12. data/lib/faraday/adapter/patron.rb +32 -13
  13. data/lib/faraday/adapter/rack.rb +1 -1
  14. data/lib/faraday/adapter/test.rb +21 -13
  15. data/lib/faraday/adapter/typhoeus.rb +4 -115
  16. data/lib/faraday/adapter.rb +2 -0
  17. data/lib/faraday/autoload.rb +1 -1
  18. data/lib/faraday/connection.rb +59 -12
  19. data/lib/faraday/deprecate.rb +109 -0
  20. data/lib/faraday/error.rb +130 -35
  21. data/lib/faraday/options.rb +31 -25
  22. data/lib/faraday/parameters.rb +2 -1
  23. data/lib/faraday/rack_builder.rb +26 -2
  24. data/lib/faraday/request/multipart.rb +7 -2
  25. data/lib/faraday/request/retry.rb +76 -17
  26. data/lib/faraday/request.rb +20 -0
  27. data/lib/faraday/response/logger.rb +3 -3
  28. data/lib/faraday/response/raise_error.rb +7 -3
  29. data/lib/faraday/response.rb +3 -3
  30. data/lib/faraday/utils.rb +18 -9
  31. data/lib/faraday.rb +9 -5
  32. data/spec/faraday/deprecate_spec.rb +147 -0
  33. data/spec/faraday/error_spec.rb +102 -0
  34. data/spec/faraday/response/raise_error_spec.rb +106 -0
  35. data/spec/spec_helper.rb +105 -0
  36. data/test/adapters/default_test.rb +14 -0
  37. data/test/adapters/em_http_test.rb +30 -0
  38. data/test/adapters/em_synchrony_test.rb +32 -0
  39. data/test/adapters/excon_test.rb +30 -0
  40. data/test/adapters/httpclient_test.rb +34 -0
  41. data/test/adapters/integration.rb +263 -0
  42. data/test/adapters/logger_test.rb +136 -0
  43. data/test/adapters/net_http_persistent_test.rb +114 -0
  44. data/test/adapters/net_http_test.rb +79 -0
  45. data/test/adapters/patron_test.rb +40 -0
  46. data/test/adapters/rack_test.rb +38 -0
  47. data/test/adapters/test_middleware_test.rb +157 -0
  48. data/test/adapters/typhoeus_test.rb +38 -0
  49. data/test/authentication_middleware_test.rb +65 -0
  50. data/test/composite_read_io_test.rb +109 -0
  51. data/test/connection_test.rb +738 -0
  52. data/test/env_test.rb +268 -0
  53. data/test/helper.rb +75 -0
  54. data/test/live_server.rb +67 -0
  55. data/test/middleware/instrumentation_test.rb +88 -0
  56. data/test/middleware/retry_test.rb +282 -0
  57. data/test/middleware_stack_test.rb +260 -0
  58. data/test/multibyte.txt +1 -0
  59. data/test/options_test.rb +333 -0
  60. data/test/parameters_test.rb +157 -0
  61. data/test/request_middleware_test.rb +126 -0
  62. data/test/response_middleware_test.rb +72 -0
  63. data/test/strawberry.rb +2 -0
  64. data/test/utils_test.rb +98 -0
  65. metadata +48 -7
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faraday
4
+ # @param new_klass [Class] new Klass to use
5
+ #
6
+ # @return [Class] A modified version of new_klass that warns on
7
+ # usage about deprecation.
8
+ # @see Faraday::Deprecate
9
+ module DeprecatedClass
10
+ def self.proxy_class(origclass, ver = '1.0')
11
+ proxy = Class.new(origclass) do
12
+ const_set("ORIG_CLASS", origclass)
13
+
14
+ class << self
15
+ extend Faraday::Deprecate
16
+
17
+ def ===(other)
18
+ (superclass == const_get("ORIG_CLASS") && other.is_a?(superclass)) || super
19
+ end
20
+ end
21
+ end
22
+ proxy.singleton_class.send(:deprecate, :new, "#{origclass}.new", ver)
23
+ proxy.singleton_class.send(:deprecate, :inherited, origclass.name, ver)
24
+ proxy
25
+ end
26
+ end
27
+
28
+ # Deprecation using semver instead of date, based on Gem::Deprecate
29
+ # Provides a single method +deprecate+ to be used to declare when
30
+ # something is going away.
31
+ #
32
+ # class Legacy
33
+ # def self.klass_method
34
+ # # ...
35
+ # end
36
+ #
37
+ # def instance_method
38
+ # # ...
39
+ # end
40
+ #
41
+ # extend Faraday::Deprecate
42
+ # deprecate :instance_method, "X.z", '1.0'
43
+ #
44
+ # class << self
45
+ # extend Faraday::Deprecate
46
+ # deprecate :klass_method, :none, '1.0'
47
+ # end
48
+ # end
49
+ module Deprecate
50
+ def self.skip # :nodoc:
51
+ @skip ||= begin
52
+ case ENV['FARADAY_DEPRECATE'].to_s.downcase
53
+ when '1', 'warn' then :warn
54
+ else :skip
55
+ end
56
+ end
57
+ @skip == :skip
58
+ end
59
+
60
+ def self.skip=(value) # :nodoc:
61
+ @skip = value ? :skip : :warn
62
+ end
63
+
64
+ # Temporarily turn off warnings. Intended for tests only.
65
+ def skip_during
66
+ original = Faraday::Deprecate.skip
67
+ Faraday::Deprecate.skip, = true
68
+ yield
69
+ ensure
70
+ Faraday::Deprecate.skip = original
71
+ end
72
+
73
+ # Simple deprecation method that deprecates +name+ by wrapping it up
74
+ # in a dummy method. It warns on each call to the dummy method
75
+ # telling the user of +repl+ (unless +repl+ is :none) and the
76
+ # semver that it is planned to go away.
77
+ # @param name [Symbol] the method symbol to deprecate
78
+ # @param repl [#to_s, :none] the replacement to use, when `:none` it will
79
+ # alert the user that no replacemtent is present.
80
+ # @param ver [String] the semver the method will be removed.
81
+ def deprecate(name, repl, ver)
82
+ class_eval do
83
+ gem_ver = Gem::Version.new(ver)
84
+ old = "_deprecated_#{name}"
85
+ alias_method old, name
86
+ define_method name do |*args, &block|
87
+ mod = is_a? Module
88
+ target = mod ? "#{self}." : "#{self.class}#"
89
+ target_message = if name == :inherited
90
+ "Inheriting #{self}"
91
+ else
92
+ "#{target}#{name}"
93
+ end
94
+
95
+ msg = [
96
+ "NOTE: #{target_message} is deprecated",
97
+ repl == :none ? ' with no replacement' : "; use #{repl} instead. ",
98
+ "It will be removed in or after version #{gem_ver}",
99
+ "\n#{target}#{name} called from #{Gem.location_of_caller.join(':')}"
100
+ ]
101
+ warn "#{msg.join}." unless Faraday::Deprecate.skip
102
+ send old, *args, &block
103
+ end
104
+ end
105
+ end
106
+
107
+ module_function :deprecate, :skip_during
108
+ end
109
+ end
data/lib/faraday/error.rb CHANGED
@@ -1,23 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday/deprecate'
4
+
5
+ # Faraday namespace.
1
6
  module Faraday
2
- class Error < StandardError; end
3
- class MissingDependency < Error; end
7
+ # Faraday error base class.
8
+ class Error < StandardError
9
+ attr_reader :response, :wrapped_exception
4
10
 
5
- class ClientError < Error
6
- attr_reader :response
7
-
8
- def initialize(ex, response = nil)
9
- @wrapped_exception = nil
10
- @response = response
11
-
12
- if ex.respond_to?(:backtrace)
13
- super(ex.message)
14
- @wrapped_exception = ex
15
- elsif ex.respond_to?(:each_key)
16
- super("the server responded with status #{ex[:status]}")
17
- @response = ex
18
- else
19
- super(ex.to_s)
20
- end
11
+ def initialize(exc, response = nil)
12
+ @wrapped_exception = nil unless defined?(@wrapped_exception)
13
+ @response = nil unless defined?(@response)
14
+ super(exc_msg_and_response!(exc, response))
21
15
  end
22
16
 
23
17
  def backtrace
@@ -30,34 +24,135 @@ module Faraday
30
24
 
31
25
  def inspect
32
26
  inner = ''
33
- if @wrapped_exception
34
- inner << " wrapped=#{@wrapped_exception.inspect}"
35
- end
36
- if @response
37
- inner << " response=#{@response.inspect}"
38
- end
39
- if inner.empty?
40
- inner << " #{super}"
41
- end
27
+ inner += " wrapped=#{@wrapped_exception.inspect}" if @wrapped_exception
28
+ inner += " response=#{@response.inspect}" if @response
29
+ inner += " #{super}" if inner.empty?
42
30
  %(#<#{self.class}#{inner}>)
43
31
  end
32
+
33
+ protected
34
+
35
+ # Pulls out potential parent exception and response hash, storing them in
36
+ # instance variables.
37
+ # exc - Either an Exception, a string message, or a response hash.
38
+ # response - Hash
39
+ # :status - Optional integer HTTP response status
40
+ # :headers - String key/value hash of HTTP response header
41
+ # values.
42
+ # :body - Optional string HTTP response body.
43
+ #
44
+ # If a subclass has to call this, then it should pass a string message
45
+ # to `super`. See NilStatusError.
46
+ def exc_msg_and_response!(exc, response = nil)
47
+ if @response.nil? && @wrapped_exception.nil?
48
+ @wrapped_exception, msg, @response = exc_msg_and_response(exc, response)
49
+ return msg
50
+ end
51
+
52
+ exc.to_s
53
+ end
54
+
55
+ # Pulls out potential parent exception and response hash.
56
+ def exc_msg_and_response(exc, response = nil)
57
+ return [exc, exc.message, response] if exc.respond_to?(:backtrace)
58
+
59
+ return [nil, "the server responded with status #{exc[:status]}", exc] \
60
+ if exc.respond_to?(:each_key)
61
+
62
+ [nil, exc.to_s, response]
63
+ end
64
+ end
65
+
66
+ # Faraday client error class. Represents 4xx status responses.
67
+ class ClientError < Error
68
+ end
69
+
70
+ # Raised by Faraday::Response::RaiseError in case of a 400 response.
71
+ class BadRequestError < ClientError
72
+ end
73
+
74
+ # Raised by Faraday::Response::RaiseError in case of a 401 response.
75
+ class UnauthorizedError < ClientError
76
+ end
77
+
78
+ # Raised by Faraday::Response::RaiseError in case of a 403 response.
79
+ class ForbiddenError < ClientError
80
+ end
81
+
82
+ # Raised by Faraday::Response::RaiseError in case of a 404 response.
83
+ class ResourceNotFound < ClientError
44
84
  end
45
85
 
46
- class ConnectionFailed < ClientError; end
47
- class ResourceNotFound < ClientError; end
48
- class ParsingError < ClientError; end
86
+ # Raised by Faraday::Response::RaiseError in case of a 407 response.
87
+ class ProxyAuthError < ClientError
88
+ end
49
89
 
90
+ # Raised by Faraday::Response::RaiseError in case of a 409 response.
91
+ class ConflictError < ClientError
92
+ end
93
+
94
+ # Raised by Faraday::Response::RaiseError in case of a 422 response.
95
+ class UnprocessableEntityError < ClientError
96
+ end
97
+
98
+ # Faraday server error class. Represents 5xx status responses.
99
+ class ServerError < Error
100
+ end
101
+
102
+ # A unified client error for timeouts.
50
103
  class TimeoutError < ClientError
51
- def initialize(ex = nil)
52
- super(ex || "timeout")
104
+ def initialize(exc = 'timeout', response = nil)
105
+ super(exc, response)
106
+ end
107
+ end
108
+
109
+ # Raised by Faraday::Response::RaiseError in case of a nil status in response.
110
+ class NilStatusError < ServerError
111
+ def initialize(exc, response = nil)
112
+ exc_msg_and_response!(exc, response)
113
+ @response = unwrap_resp!(@response)
114
+ super('http status could not be derived from the server response')
53
115
  end
116
+
117
+ private
118
+
119
+ extend Faraday::Deprecate
120
+
121
+ def unwrap_resp(resp)
122
+ if inner = (resp.keys.size == 1 && resp[:response])
123
+ return unwrap_resp(inner)
124
+ end
125
+
126
+ resp
127
+ end
128
+
129
+ alias_method :unwrap_resp!, :unwrap_resp
130
+ deprecate('unwrap_resp', nil, '1.0')
54
131
  end
55
132
 
133
+ # A unified error for failed connections.
134
+ class ConnectionFailed < ClientError
135
+ end
136
+
137
+ # A unified client error for SSL errors.
56
138
  class SSLError < ClientError
57
139
  end
58
140
 
59
- [:MissingDependency, :ClientError, :ConnectionFailed, :ResourceNotFound,
60
- :ParsingError, :TimeoutError, :SSLError].each do |const|
61
- Error.const_set(const, Faraday.const_get(const))
141
+ # Raised by FaradayMiddleware::ResponseMiddleware
142
+ class ParsingError < ClientError
143
+ end
144
+
145
+ # Exception used to control the Retry middleware.
146
+ #
147
+ # @see Faraday::Request::Retry
148
+ class RetriableResponse < ClientError
149
+ end
150
+
151
+ [:ClientError, :ConnectionFailed, :ResourceNotFound,
152
+ :ParsingError, :TimeoutError, :SSLError, :RetriableResponse].each do |const|
153
+ Error.const_set(
154
+ const,
155
+ DeprecatedClass.proxy_class(Faraday.const_get(const))
156
+ )
62
157
  end
63
158
  end
@@ -18,23 +18,20 @@ module Faraday
18
18
  # Public
19
19
  def update(obj)
20
20
  obj.each do |key, value|
21
- if sub_options = self.class.options_for(key)
22
- value = sub_options.from(value) if value
23
- elsif Hash === value
24
- hash = {}
25
- value.each do |hash_key, hash_value|
26
- hash[hash_key] = hash_value
27
- end
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}=", value) unless value.nil?
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,17 +45,26 @@ module Faraday
48
45
  end
49
46
 
50
47
  # Public
51
- def merge(value)
52
- dup.update(value)
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)
53
61
  end
54
-
62
+
55
63
  # Public
56
- def dup
64
+ def deep_dup
57
65
  self.class.from(self)
58
66
  end
59
67
 
60
- alias clone dup
61
-
62
68
  # Public
63
69
  def fetch(key, *args)
64
70
  unless symbolized_key_set.include?(key.to_sym)
@@ -66,7 +72,7 @@ module Faraday
66
72
  if args.size > 0
67
73
  send(key_setter, args.first)
68
74
  elsif block_given?
69
- send(key_setter, Proc.new.call(key))
75
+ send(key_setter, yield(key))
70
76
  else
71
77
  raise self.class.fetch_error_class, "key not found: #{key.inspect}"
72
78
  end
@@ -156,8 +162,8 @@ module Faraday
156
162
  @attribute_options ||= {}
157
163
  end
158
164
 
159
- def self.memoized(key)
160
- memoized_attributes[key.to_sym] = Proc.new
165
+ def self.memoized(key, &block)
166
+ memoized_attributes[key.to_sym] = block
161
167
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
162
168
  def #{key}() self[:#{key}]; end
163
169
  RUBY
@@ -196,8 +202,7 @@ module Faraday
196
202
  end
197
203
 
198
204
  class RequestOptions < Options.new(:params_encoder, :proxy, :bind,
199
- :timeout, :open_timeout, :boundary,
200
- :oauth)
205
+ :timeout, :open_timeout, :write_timeout, :boundary, :oauth, :context)
201
206
 
202
207
  def []=(key, value)
203
208
  if key && key.to_sym == :proxy
@@ -209,7 +214,8 @@ module Faraday
209
214
  end
210
215
 
211
216
  class SSLOptions < Options.new(:verify, :ca_file, :ca_path, :verify_mode,
212
- :cert_store, :client_cert, :client_key, :certificate, :private_key, :verify_depth, :version)
217
+ :cert_store, :client_cert, :client_key, :certificate, :private_key, :verify_depth,
218
+ :version, :min_version, :max_version)
213
219
 
214
220
  def verify?
215
221
  verify != false
@@ -238,8 +244,8 @@ module Faraday
238
244
  super(value)
239
245
  end
240
246
 
241
- memoized(:user) { uri.user && Utils.unescape(uri.user) }
242
- 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) }
243
249
  end
244
250
 
245
251
  class ConnectionOptions < Options.new(:request, :proxy, :ssl, :builder, :url,
@@ -40,9 +40,10 @@ 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
@@ -49,10 +49,10 @@ module Faraday
49
49
  end
50
50
  end
51
51
 
52
- def initialize(handlers = [])
52
+ def initialize(handlers = [], &block)
53
53
  @handlers = handlers
54
54
  if block_given?
55
- build(&Proc.new)
55
+ build(&block)
56
56
  elsif @handlers.empty?
57
57
  # default stack, if nothing else is configured
58
58
  self.request :url_encoded
@@ -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
 
@@ -200,6 +204,26 @@ module Faraday
200
204
  raise StackLocked, "can't modify middleware stack after making a request" if locked?
201
205
  end
202
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
+
203
227
  def use_symbol(mod, key, *args, &block)
204
228
  use(mod.lookup_middleware(key), *args, &block)
205
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
- DEFAULT_BOUNDARY = "-----------RubyMultipartPost".freeze unless defined? DEFAULT_BOUNDARY
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 ||= DEFAULT_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,11 +21,15 @@ 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
29
  class Options < Faraday::Options.new(:max, :interval, :max_interval, :interval_randomness,
26
- :backoff_factor, :exceptions, :methods, :retry_if)
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)
@@ -55,8 +61,7 @@ module Faraday
55
61
  end
56
62
 
57
63
  def exceptions
58
- Array(self[:exceptions] ||= [Errno::ETIMEDOUT, 'Timeout::Error',
59
- Error::TimeoutError])
64
+ Array(self[:exceptions] ||= DEFAULT_EXCEPTIONS)
60
65
  end
61
66
 
62
67
  def methods
@@ -67,6 +72,13 @@ module Faraday
67
72
  self[:retry_if] ||= DEFAULT_CHECK
68
73
  end
69
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
70
82
  end
71
83
 
72
84
  # Public: Initialize middleware
@@ -83,8 +95,8 @@ module Faraday
83
95
  # (default: 1)
84
96
  # exceptions - The list of exceptions to handle. Exceptions can be
85
97
  # given as Class, Module, or String. (default:
86
- # [Errno::ETIMEDOUT, Timeout::Error,
87
- # Error::TimeoutError])
98
+ # [Errno::ETIMEDOUT, 'Timeout::Error',
99
+ # Faraday::TimeoutError, Faraday::RetriableResponse])
88
100
  # methods - A list of HTTP methods to retry without calling retry_if. Pass
89
101
  # an empty Array to call retry_if for all exceptions.
90
102
  # (defaults to the idempotent HTTP methods in IDEMPOTENT_METHODS)
@@ -94,18 +106,21 @@ module Faraday
94
106
  # if the exception produced is non-recoverable or if the
95
107
  # the HTTP method called is not idempotent.
96
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.
97
111
  def initialize(app, options = nil)
98
112
  super(app)
99
113
  @options = Options.from(options)
100
114
  @errmatch = build_exception_matcher(@options.exceptions)
101
115
  end
102
116
 
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
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
109
124
  end
110
125
 
111
126
  def call(env)
@@ -113,14 +128,25 @@ module Faraday
113
128
  request_body = env[:body]
114
129
  begin
115
130
  env[:body] = request_body # after failure env[:body] is set to the response body
116
- @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
117
134
  rescue @errmatch => exception
118
135
  if retries > 0 && retry_request?(env, exception)
119
136
  retries -= 1
120
- sleep sleep_amount(retries + 1)
121
- retry
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
122
149
  end
123
- raise
124
150
  end
125
151
  end
126
152
 
@@ -150,5 +176,38 @@ module Faraday
150
176
  @options.methods.include?(env[:method]) || @options.retry_if.call(env, exception)
151
177
  end
152
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
153
212
  end
154
213
  end