1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263
|
# frozen_string_literal: true
require_relative 'retryable'
module Faraday
module Retry
# This class provides the main implementation for your middleware.
# Your middleware can implement any of the following methods:
# * on_request - called when the request is being prepared
# * on_complete - called when the response is being processed
#
# Optionally, you can also override the following methods from Faraday::Middleware
# * initialize(app, options = {}) - the initializer method
# * call(env) - the main middleware invocation method.
# This already calls on_request and on_complete, so you normally don't need to override it.
# You may need to in case you need to "wrap" the request or need more control
# (see "retry" middleware: https://github.com/lostisland/faraday/blob/main/lib/faraday/request/retry.rb#L142).
# IMPORTANT: Remember to call `@app.call(env)` or `super` to not interrupt the middleware chain!
class Middleware < Faraday::Middleware
include Retryable
DEFAULT_EXCEPTIONS = [
Errno::ETIMEDOUT, 'Timeout::Error',
Faraday::TimeoutError, Faraday::RetriableResponse
].freeze
IDEMPOTENT_METHODS = %i[delete get head options put].freeze
# Options contains the configurable parameters for the Retry middleware.
class Options < Faraday::Options.new(:max, :interval, :max_interval,
:interval_randomness,
:backoff_factor, :exceptions,
:methods, :retry_if, :retry_block,
:retry_statuses, :rate_limit_retry_header,
:rate_limit_reset_header, :header_parser_block,
:exhausted_retries_block)
DEFAULT_CHECK = ->(_env, _exception) { false }
def self.from(value)
if value.is_a?(Integer)
new(value)
else
super(value)
end
end
def max
(self[:max] ||= 2).to_i
end
def interval
(self[:interval] ||= 0).to_f
end
def max_interval
(self[:max_interval] ||= Float::MAX).to_f
end
def interval_randomness
(self[:interval_randomness] ||= 0).to_f
end
def backoff_factor
(self[:backoff_factor] ||= 1).to_f
end
def exceptions
Array(self[:exceptions] ||= DEFAULT_EXCEPTIONS)
end
def methods
Array(self[:methods] ||= IDEMPOTENT_METHODS)
end
def retry_if
self[:retry_if] ||= DEFAULT_CHECK
end
def retry_block
self[:retry_block] ||= proc {}
end
def retry_statuses
Array(self[:retry_statuses] ||= [])
end
def exhausted_retries_block
self[:exhausted_retries_block] ||= proc {}
end
end
# @param app [#call]
# @param options [Hash]
# @option options [Integer] :max (2) Maximum number of retries
# @option options [Integer] :interval (0) Pause in seconds between retries
# @option options [Integer] :interval_randomness (0) The maximum random
# interval amount expressed as a float between
# 0 and 1 to use in addition to the interval.
# @option options [Integer] :max_interval (Float::MAX) An upper limit
# for the interval
# @option options [Integer] :backoff_factor (1) The amount to multiply
# each successive retry's interval amount by in order to provide backoff
# @option options [Array] :exceptions ([ Errno::ETIMEDOUT,
# 'Timeout::Error', Faraday::TimeoutError, Faraday::RetriableResponse])
# The list of exceptions to handle. Exceptions can be given as
# Class, Module, or String.
# @option options [Array<Symbol>] :methods (the idempotent HTTP methods
# in IDEMPOTENT_METHODS) A list of HTTP methods, as symbols, to retry without
# calling retry_if. Pass an empty Array to call retry_if
# for all exceptions.
# @option options [Block] :retry_if (false) block that will receive
# the env object and the exception raised
# and should decide if the code should retry still the action or
# not independent of the retry count. This would be useful
# if the exception produced is non-recoverable or if the
# the HTTP method called is not idempotent.
# @option options [Block] :retry_block block that is executed before
# every retry. The block will be yielded keyword arguments:
# * env [Faraday::Env]: Request environment
# * options [Faraday::Options]: middleware options
# * retry_count [Integer]: how many retries have already occured (starts at 0)
# * exception [Exception]: exception that triggered the retry,
# will be the synthetic `Faraday::RetriableResponse` if the
# retry was triggered by something other than an exception.
# * will_retry_in [Float]: retry_block is called *before* the retry
# delay, actual retry will happen in will_retry_in number of
# seconds.
# @option options [Array] :retry_statuses Array of Integer HTTP status
# codes or a single Integer value that determines whether to raise
# a Faraday::RetriableResponse exception based on the HTTP status code
# of an HTTP response.
# @option options [Block] :header_parser_block block that will receive
# the the value of the retry header and should return the number of
# seconds to wait before retrying the request. This is useful if the
# value of the header is not a number of seconds or a RFC 2822 formatted date.
# @option options [Block] :exhausted_retries_block block will receive
# when all attempts are exhausted. The block will be yielded keyword arguments:
# * env [Faraday::Env]: Request environment
# * exception [Exception]: exception that triggered the retry,
# will be the synthetic `Faraday::RetriableResponse` if the
# retry was triggered by something other than an exception.
# * options [Faraday::Options]: middleware options
def initialize(app, options = nil)
super(app)
@options = Options.from(options)
@errmatch = build_exception_matcher(@options.exceptions)
end
def calculate_sleep_amount(retries, env)
retry_after = [calculate_retry_after(env), calculate_rate_limit_reset(env)].compact.max
retry_interval = calculate_retry_interval(retries)
return if retry_after && retry_after > @options.max_interval
if retry_after && retry_after >= retry_interval
retry_after
else
retry_interval
end
end
# @param env [Faraday::Env]
def call(env)
retries = @options.max
request_body = env[:body]
with_retries(env: env, options: @options, retries: retries, body: request_body, errmatch: @errmatch) do
# after failure env[:body] is set to the response body
env[:body] = request_body
@app.call(env).tap do |resp|
raise Faraday::RetriableResponse.new(nil, resp) if @options.retry_statuses.include?(resp.status)
end
end
end
# An exception matcher for the rescue clause can usually be any object
# that responds to `===`, but for Ruby 1.8 it has to be a Class or Module.
#
# @param exceptions [Array]
# @api private
# @return [Module] an exception matcher
def build_exception_matcher(exceptions)
matcher = Module.new
(
class << matcher
self
end).class_eval do
define_method(:===) do |error|
exceptions.any? do |ex|
if ex.is_a? Module
error.is_a? ex
else
Object.const_defined?(ex.to_s) && error.is_a?(Object.const_get(ex.to_s))
end
end
end
end
matcher
end
private
def retry_request?(env, exception)
@options.methods.include?(env[:method]) ||
@options.retry_if.call(env, exception)
end
def rewind_files(body)
return unless defined?(Faraday::UploadIO)
return unless body.is_a?(Hash)
body.each do |_, value|
value.rewind if value.is_a?(Faraday::UploadIO)
end
end
# RFC for RateLimit Header Fields for HTTP:
# https://www.ietf.org/archive/id/draft-ietf-httpapi-ratelimit-headers-05.html#name-fields-definition
def calculate_rate_limit_reset(env)
reset_header = @options.rate_limit_reset_header || 'RateLimit-Reset'
parse_retry_header(env, reset_header)
end
# MDN spec for Retry-After header:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
def calculate_retry_after(env)
retry_header = @options.rate_limit_retry_header || 'Retry-After'
parse_retry_header(env, retry_header)
end
def calculate_retry_interval(retries)
retry_index = @options.max - retries
current_interval = @options.interval *
(@options.backoff_factor**retry_index)
current_interval = [current_interval, @options.max_interval].min
random_interval = rand * @options.interval_randomness.to_f *
@options.interval
current_interval + random_interval
end
def parse_retry_header(env, header)
response_headers = env[:response_headers]
return unless response_headers
retry_after_value = env[:response_headers][header]
if @options.header_parser_block
@options.header_parser_block.call(retry_after_value)
else
# Try to parse date from the header value
begin
datetime = DateTime.rfc2822(retry_after_value)
datetime.to_time - Time.now.utc
rescue ArgumentError
retry_after_value.to_f
end
end
end
end
end
end
|