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.
Files changed (74) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +196 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +192 -28
  5. data/Rakefile +6 -64
  6. data/lib/faraday/adapter/em_http.rb +17 -11
  7. data/lib/faraday/adapter/em_http_ssl_patch.rb +1 -1
  8. data/lib/faraday/adapter/em_synchrony.rb +19 -5
  9. data/lib/faraday/adapter/excon.rb +13 -11
  10. data/lib/faraday/adapter/httpclient.rb +31 -9
  11. data/lib/faraday/adapter/net_http.rb +36 -14
  12. data/lib/faraday/adapter/net_http_persistent.rb +37 -17
  13. data/lib/faraday/adapter/patron.rb +44 -21
  14. data/lib/faraday/adapter/rack.rb +1 -1
  15. data/lib/faraday/adapter/test.rb +79 -28
  16. data/lib/faraday/adapter/typhoeus.rb +4 -115
  17. data/lib/faraday/adapter.rb +10 -1
  18. data/lib/faraday/autoload.rb +1 -1
  19. data/lib/faraday/connection.rb +72 -20
  20. data/lib/faraday/deprecate.rb +101 -0
  21. data/lib/faraday/error.rb +90 -24
  22. data/lib/faraday/options.rb +43 -20
  23. data/lib/faraday/parameters.rb +56 -39
  24. data/lib/faraday/rack_builder.rb +27 -2
  25. data/lib/faraday/request/authorization.rb +1 -2
  26. data/lib/faraday/request/multipart.rb +7 -2
  27. data/lib/faraday/request/retry.rb +84 -19
  28. data/lib/faraday/request.rb +22 -0
  29. data/lib/faraday/response/logger.rb +29 -8
  30. data/lib/faraday/response/raise_error.rb +7 -3
  31. data/lib/faraday/response.rb +9 -5
  32. data/lib/faraday/utils.rb +32 -3
  33. data/lib/faraday.rb +14 -34
  34. data/spec/faraday/deprecate_spec.rb +69 -0
  35. data/spec/faraday/error_spec.rb +102 -0
  36. data/spec/faraday/response/raise_error_spec.rb +95 -0
  37. data/spec/spec_helper.rb +104 -0
  38. data/test/adapters/em_http_test.rb +10 -0
  39. data/test/adapters/em_synchrony_test.rb +22 -10
  40. data/test/adapters/excon_test.rb +10 -0
  41. data/test/adapters/httpclient_test.rb +14 -1
  42. data/test/adapters/integration.rb +17 -8
  43. data/test/adapters/logger_test.rb +65 -11
  44. data/test/adapters/net_http_persistent_test.rb +96 -2
  45. data/test/adapters/net_http_test.rb +67 -2
  46. data/test/adapters/patron_test.rb +28 -8
  47. data/test/adapters/rack_test.rb +8 -1
  48. data/test/adapters/test_middleware_test.rb +46 -3
  49. data/test/adapters/typhoeus_test.rb +19 -9
  50. data/test/composite_read_io_test.rb +16 -18
  51. data/test/connection_test.rb +294 -78
  52. data/test/env_test.rb +55 -5
  53. data/test/helper.rb +11 -17
  54. data/test/middleware/retry_test.rb +115 -10
  55. data/test/middleware_stack_test.rb +97 -10
  56. data/test/options_test.rb +97 -16
  57. data/test/parameters_test.rb +94 -1
  58. data/test/request_middleware_test.rb +24 -40
  59. data/test/response_middleware_test.rb +4 -4
  60. data/test/utils_test.rb +40 -0
  61. metadata +21 -66
  62. data/.document +0 -6
  63. data/CONTRIBUTING.md +0 -36
  64. data/Gemfile +0 -25
  65. data/faraday.gemspec +0 -34
  66. data/script/cached-bundle +0 -46
  67. data/script/console +0 -7
  68. data/script/generate_certs +0 -42
  69. data/script/package +0 -7
  70. data/script/proxy-server +0 -42
  71. data/script/release +0 -17
  72. data/script/s3-put +0 -71
  73. data/script/server +0 -36
  74. data/script/test +0 -172
data/lib/faraday/error.rb CHANGED
@@ -1,22 +1,25 @@
1
- module Faraday
2
- class Error < StandardError; end
3
- class MissingDependency < Error; end
1
+ # frozen_string_literal: true
4
2
 
5
- class ClientError < Error
6
- attr_reader :response
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(ex, response = nil)
11
+ def initialize(exc, response = nil)
9
12
  @wrapped_exception = nil
10
13
  @response = response
11
14
 
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
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(ex.to_s)
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
- %(#<#{self.class}>)
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 ConnectionFailed < ClientError; end
37
- class ResourceNotFound < ClientError; end
38
- class ParsingError < ClientError; end
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
- class TimeoutError < ClientError
41
- def initialize(ex = nil)
42
- super(ex || "timeout")
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
- class SSLError < ClientError
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
- [:MissingDependency, :ClientError, :ConnectionFailed, :ResourceNotFound,
50
- :ParsingError, :TimeoutError, :SSLError].each do |const|
51
- Error.const_set(const, Faraday.const_get(const))
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
@@ -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,8 +45,24 @@ 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)
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, Proc.new.call(key))
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, :version)
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)
@@ -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
- # Recursive helper lambda
67
- dehash = lambda do |hash|
68
- hash.each do |(key, value)|
69
- if value.kind_of?(Hash)
70
- hash[key] = dehash.call(value)
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
- end
73
- # Numeric keys implies an array
74
- if hash != {} && hash.keys.all? { |key| key =~ /^\d+$/ }
75
- hash.sort.inject([]) do |accu, (_, value)|
76
- accu << value; accu
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
- empty_accumulator = {}
84
- return ((query.split('&').map do |pair|
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
- array_notation = !!(key =~ /\[\]$/)
93
- subkeys = key.split(/[\[\]]+/)
94
- current_hash = accu
95
- for i in 0...(subkeys.size - 1)
96
- subkey = subkeys[i]
97
- current_hash[subkey] = {} unless current_hash[subkey]
98
- current_hash = current_hash[subkey]
99
- end
100
- if array_notation
101
- current_hash[subkeys.last] = [] unless current_hash[subkeys.last]
102
- current_hash[subkeys.last] << value
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
@@ -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
@@ -16,8 +16,7 @@ module Faraday
16
16
 
17
17
  # Internal
18
18
  def self.build_hash(type, hash)
19
- offset = KEY.size + type.size + 3
20
- comma = ",\n#{' ' * offset}"
19
+ comma = ", "
21
20
  values = []
22
21
  hash.each do |key, value|
23
22
  values << "#{key}=#{value.to_s.inspect}"
@@ -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,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, :interval_randomness, :backoff_factor,
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 Fixnum === value
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).to_i
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] ||= [Errno::ETIMEDOUT, 'Timeout::Error',
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
- # Error::TimeoutError])
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 sleep_amount(retries)
99
- retry_index = @options.max - retries
100
- current_interval = @options.interval * (@options.backoff_factor ** retry_index)
101
- random_interval = rand * @options.interval_randomness.to_f * @options.interval
102
- 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
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
- sleep sleep_amount(retries + 1)
115
- 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
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