mirror of
https://github.com/HoneyryderChuck/httpx.git
synced 2025-10-04 00:00:37 -04:00
150 lines
4.2 KiB
Ruby
150 lines
4.2 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module HTTPX
|
|
module Plugins
|
|
#
|
|
# This plugin adds support for retrying requests when certain errors happen.
|
|
#
|
|
# https://gitlab.com/honeyryderchuck/httpx/wikis/Retries
|
|
#
|
|
module Retries
|
|
MAX_RETRIES = 3
|
|
# TODO: pass max_retries in a configure/load block
|
|
|
|
IDEMPOTENT_METHODS = %i[get options head put delete].freeze
|
|
RETRYABLE_ERRORS = [
|
|
IOError,
|
|
EOFError,
|
|
Errno::ECONNRESET,
|
|
Errno::ECONNABORTED,
|
|
Errno::EPIPE,
|
|
Errno::EINVAL,
|
|
Errno::ETIMEDOUT,
|
|
Parser::Error,
|
|
TLSError,
|
|
TimeoutError,
|
|
Connection::HTTP2::GoawayError,
|
|
].freeze
|
|
DEFAULT_JITTER = ->(interval) { interval * (0.5 * (1 + rand)) }
|
|
|
|
if ENV.key?("HTTPX_NO_JITTER")
|
|
def self.extra_options(options)
|
|
options.merge(max_retries: MAX_RETRIES)
|
|
end
|
|
else
|
|
def self.extra_options(options)
|
|
options.merge(max_retries: MAX_RETRIES, retry_jitter: DEFAULT_JITTER)
|
|
end
|
|
end
|
|
|
|
module OptionsMethods
|
|
def option_retry_after(value)
|
|
# return early if callable
|
|
unless value.respond_to?(:call)
|
|
value = Float(value)
|
|
raise TypeError, ":retry_after must be positive" unless value.positive?
|
|
end
|
|
|
|
value
|
|
end
|
|
|
|
def option_retry_jitter(value)
|
|
# return early if callable
|
|
raise TypeError, ":retry_jitter must be callable" unless value.respond_to?(:call)
|
|
|
|
value
|
|
end
|
|
|
|
def option_max_retries(value)
|
|
num = Integer(value)
|
|
raise TypeError, ":max_retries must be positive" unless num.positive?
|
|
|
|
num
|
|
end
|
|
|
|
def option_retry_change_requests(v)
|
|
v
|
|
end
|
|
|
|
def option_retry_on(value)
|
|
raise TypeError, ":retry_on must be called with the response" unless value.respond_to?(:call)
|
|
|
|
value
|
|
end
|
|
end
|
|
|
|
module InstanceMethods
|
|
def max_retries(n)
|
|
with(max_retries: n.to_i)
|
|
end
|
|
|
|
private
|
|
|
|
def fetch_response(request, connections, options)
|
|
response = super
|
|
|
|
if response &&
|
|
request.retries.positive? &&
|
|
__repeatable_request?(request, options) &&
|
|
(
|
|
# rubocop:disable Style/MultilineTernaryOperator
|
|
options.retry_on ?
|
|
options.retry_on.call(response) :
|
|
(
|
|
response.is_a?(ErrorResponse) && __retryable_error?(response.error)
|
|
)
|
|
# rubocop:enable Style/MultilineTernaryOperator
|
|
)
|
|
response.close if response.respond_to?(:close)
|
|
log { "failed to get response, #{request.retries} tries to go..." }
|
|
request.retries -= 1
|
|
request.transition(:idle)
|
|
|
|
retry_after = options.retry_after
|
|
retry_after = retry_after.call(request, response) if retry_after.respond_to?(:call)
|
|
|
|
if retry_after
|
|
# apply jitter
|
|
if (jitter = request.options.retry_jitter)
|
|
retry_after = jitter.call(retry_after)
|
|
end
|
|
|
|
retry_start = Utils.now
|
|
log { "retrying after #{retry_after} secs..." }
|
|
pool.after(retry_after) do
|
|
log { "retrying (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
|
|
connection = find_connection(request, connections, options)
|
|
connection.send(request)
|
|
end
|
|
else
|
|
connection = find_connection(request, connections, options)
|
|
connection.send(request)
|
|
end
|
|
|
|
return
|
|
end
|
|
response
|
|
end
|
|
|
|
def __repeatable_request?(request, options)
|
|
IDEMPOTENT_METHODS.include?(request.verb) || options.retry_change_requests
|
|
end
|
|
|
|
def __retryable_error?(ex)
|
|
RETRYABLE_ERRORS.any? { |klass| ex.is_a?(klass) }
|
|
end
|
|
end
|
|
|
|
module RequestMethods
|
|
attr_accessor :retries
|
|
|
|
def initialize(*args)
|
|
super
|
|
@retries = @options.max_retries
|
|
end
|
|
end
|
|
end
|
|
register_plugin :retries, Retries
|
|
end
|
|
end
|