faraday 0.9.1 → 0.17.1

Sign up to get free protection for your applications and to get access to all the features.
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