2 # Catches exceptions and retries each request a limited number of times.
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.
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']
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
21 class Request::Retry < Faraday::Middleware
23 IDEMPOTENT_METHODS = [:delete, :get, :head, :options, :put]
25 class Options < Faraday::Options.new(:max, :interval, :interval_randomness, :backoff_factor, :exceptions, :retry_if)
26 DEFAULT_CHECK = lambda { |env,exception| false }
37 (self[:max] ||= 2).to_i
41 (self[:interval] ||= 0).to_f
44 def interval_randomness
45 (self[:interval_randomness] ||= 0).to_i
49 (self[:backoff_factor] ||= 1).to_f
53 Array(self[:exceptions] ||= [Errno::ETIMEDOUT, 'Timeout::Error',
58 self[:retry_if] ||= DEFAULT_CHECK
63 # Public: Initialize middleware
66 # max - Maximum number of retries (default: 2)
67 # interval - Pause in seconds between retries (default: 0)
68 # interval_randomness - The maximum random interval amount expressed
69 # as a float between 0 and 1 to use in addition to the
70 # interval. (default: 0)
71 # backoff_factor - The amount to multiple each successive retry's
72 # interval amount by in order to provide backoff
74 # exceptions - The list of exceptions to handle. Exceptions can be
75 # given as Class, Module, or String. (default:
76 # [Errno::ETIMEDOUT, Timeout::Error,
77 # Error::TimeoutError])
78 # retry_if - block that will receive the env object and the exception raised
79 # and should decide if the code should retry still the action or
80 # not independent of the retry count. This would be useful
81 # if the exception produced is non-recoverable or if the
82 # the HTTP method called is not idempotent.
83 # (defaults to return false)
84 def initialize(app, options = nil)
86 @options = Options.from(options)
87 @errmatch = build_exception_matcher(@options.exceptions)
90 def sleep_amount(retries)
91 retry_index = @options.max - retries
92 current_interval = @options.interval * (@options.backoff_factor ** retry_index)
93 random_interval = rand * @options.interval_randomness.to_f * @options.interval
94 current_interval + random_interval
98 retries = @options.max
99 request_body = env[:body]
101 env[:body] = request_body # after failure env[:body] is set to the response body
103 rescue @errmatch => exception
104 if retries > 0 && retry_request?(env, exception)
106 sleep sleep_amount(retries + 1)
113 # Private: construct an exception matcher object.
115 # An exception matcher for the rescue clause can usually be any object that
116 # responds to `===`, but for Ruby 1.8 it has to be a Class or Module.
117 def build_exception_matcher(exceptions)
119 (class << matcher; self; end).class_eval do
120 define_method(:===) do |error|
121 exceptions.any? do |ex|
125 error.class.to_s == ex.to_s
135 def retry_request?(env, exception)
136 IDEMPOTENT_METHODS.include?(env[:method]) || @options.retry_if.call(env, exception)