From 93a693b9efdf846c54793873d4a9d5a5260e246b Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 2 Jan 2022 10:19:15 +0000 Subject: [PATCH] * Remove retry middleware and all its documentation and tests. (#1356) --- UPGRADING.md | 12 +- docs/index.md | 4 - docs/middleware/index.md | 14 +- docs/middleware/list.md | 5 +- docs/middleware/request/instrumentation.md | 4 +- docs/middleware/request/json.md | 4 +- docs/middleware/request/retry.md | 126 --------- docs/usage/index.md | 7 +- lib/faraday/error.rb | 8 +- lib/faraday/request.rb | 1 - lib/faraday/request/retry.rb | 241 ------------------ spec/faraday/request/retry_spec.rb | 282 --------------------- 12 files changed, 23 insertions(+), 685 deletions(-) delete mode 100644 docs/middleware/request/retry.md delete mode 100644 lib/faraday/request/retry.rb delete mode 100644 spec/faraday/request/retry_spec.rb diff --git a/UPGRADING.md b/UPGRADING.md index 8acd82401..6c816373a 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -37,12 +37,12 @@ We did our best to make this transition as painless as possible for you, so here `faraday` altogether as these gems usually have Faraday already in their dependencies. * If you're relying on `Faraday.default_adapter` (e.g. if you use `Faraday.get` or other verb class methods, or not specifying an adapter in your connection initializer), then you'll now need to set it yourself. It previously - defaulted to `:net_http`, but it now defaults to `:test`. You can do so simply by using the setter: + defaulted to `:net_http`, but it now requires to be explicitly set. You can do so simply by using the setter: - ```ruby - # For example, to use net_http (previous default value, will now require `gem 'faraday-net_http'` in your gemfile) - Faraday.default_adapter = :net_http - ``` +```ruby +# For example, to use net_http (previous default value, will now require `gem 'faraday-net_http'` in your gemfile) +Faraday.default_adapter = :net_http +``` ### Faraday Middleware Deprecation @@ -86,7 +86,7 @@ For more details, see https://github.com/lostisland/faraday/pull/1306 * Remove `Faraday::Response::Middleware`. You can now use the new `on_complete` callback provided by `Faraday::Middleware`. * Drop `Faraday::UploadIO` in favour of `Faraday::FilePart`. * `Faraday.default_connection_options` will now be deep-merged into new connections to avoid overriding them (e.g. headers). -* Retry middleware `retry_block` is not called if retry will not happen due to `max_interval`. (#1350) +* Retry middleware has been moved to a separate `faraday-retry` gem. * `Faraday::Builder#build` method is not exposed through `Faraday::Connection` anymore and does not reset the handlers if called multiple times. This method should be used internally only. ## Faraday 1.0 diff --git a/docs/index.md b/docs/index.md index 31dbb8ccd..51c8afc44 100644 --- a/docs/index.md +++ b/docs/index.md @@ -37,9 +37,6 @@ Or install it yourself as: $ gem install faraday ``` -You can also install the [`faraday_middleware`][faraday_middleware] -extension gem to access a collection of useful Faraday middleware. - {: .mt-60} {: .text-center} @@ -47,5 +44,4 @@ extension gem to access a collection of useful Faraday middleware. [github]: https://github.com/lostisland/faraday [gitter]: https://gitter.im/lostisland/faraday -[faraday_middleware]: https://github.com/lostisland/faraday_middleware [usage]: ./usage diff --git a/docs/middleware/index.md b/docs/middleware/index.md index 8d640a148..98a66995c 100644 --- a/docs/middleware/index.md +++ b/docs/middleware/index.md @@ -25,13 +25,15 @@ To use these great features, create a `Faraday::Connection` with `Faraday.new` and add the correct middleware in a block. For example: ```ruby -require 'faraday_middleware' +require 'faraday' +require 'faraday/net_http' +require 'faraday/retry' conn = Faraday.new do |f| f.request :json # encode req bodies as JSON f.request :retry # retry transient failures - f.response :follow_redirects # follow redirects f.response :json # decode response bodies as JSON + f.adapter :net_http # Use the Net::HTTP adapter end response = conn.get("http://httpbingo.org/get") ``` @@ -64,7 +66,7 @@ For example, the `Faraday::Request::UrlEncoded` middleware registers itself in # Faraday::Response and Faraday::Adapter registries conn = Faraday.new do |f| f.request :url_encoded - f.response :follow_redirects + f.response :logger f.adapter :httpclient end ``` @@ -75,7 +77,7 @@ or: # identical, but add the class directly instead of using lookups conn = Faraday.new do |f| f.use Faraday::Request::UrlEncoded - f.use FaradayMiddleware::FollowRedirects + f.use Faraday::Response::Logger f.use Faraday::Adapter::HTTPClient end ``` @@ -84,7 +86,7 @@ This is also the place to pass options. For example: ```ruby conn = Faraday.new do |f| - f.request :retry, max: 10 + f.request :logger, bodies: true end ``` @@ -93,7 +95,7 @@ end The [Awesome Faraday](https://github.com/lostisland/awesome-faraday/) project has a complete list of useful, well-maintained Faraday middleware. Middleware is often provided by external gems, like the -[faraday-middleware](https://github.com/lostisland/faraday_middleware) gem. +[faraday-retry](https://github.com/lostisland/faraday-retry) gem. We also have [great documentation](list) for the middleware that ships with Faraday. diff --git a/docs/middleware/list.md b/docs/middleware/list.md index 88f4422cb..467f6d0e8 100644 --- a/docs/middleware/list.md +++ b/docs/middleware/list.md @@ -29,8 +29,6 @@ multipart form request. * [`UrlEncoded`][url_encoded] converts a `Faraday::Request#body` hash of key/value pairs into a url-encoded request body. * [`Json Request`][json-request] converts a `Faraday::Request#body` hash of key/value pairs into a JSON request body. * [`Json Response`][json-response] parses response body into a hash of key/value pairs. -* [`Retry`][retry] automatically retries requests that fail due to intermittent client -or server errors (such as network hiccups). * [`Instrumentation`][instrumentation] allows to instrument requests using different tools. @@ -47,8 +45,7 @@ before returning it. [multipart]: ./multipart [url_encoded]: ./url-encoded [json-request]: ./json-request -[retry]: ./retry [instrumentation]: ./instrumentation -[json-response]: ./json-response +[json-response]: ./json-response [logger]: ./logger [raise_error]: ./raise-error diff --git a/docs/middleware/request/instrumentation.md b/docs/middleware/request/instrumentation.md index 83adec6e6..58da200cb 100644 --- a/docs/middleware/request/instrumentation.md +++ b/docs/middleware/request/instrumentation.md @@ -3,8 +3,8 @@ layout: documentation title: "Instrumentation Middleware" permalink: /middleware/instrumentation hide: true -prev_name: Retry Middleware -prev_link: ./retry +prev_name: JSON Request Middleware +prev_link: ./json-request next_name: JSON Response Middleware next_link: ./json-response top_name: Back to Middleware diff --git a/docs/middleware/request/json.md b/docs/middleware/request/json.md index 9ba064ad9..c07b94ba4 100644 --- a/docs/middleware/request/json.md +++ b/docs/middleware/request/json.md @@ -5,8 +5,8 @@ permalink: /middleware/json-request hide: true prev_name: UrlEncoded Middleware prev_link: ./url-encoded -next_name: Retry Middleware -next_link: ./retry +next_name: Instrumentation Middleware +next_link: ./instrumentation top_name: Back to Middleware top_link: ./list --- diff --git a/docs/middleware/request/retry.md b/docs/middleware/request/retry.md deleted file mode 100644 index f1115042d..000000000 --- a/docs/middleware/request/retry.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -layout: documentation -title: "Retry Middleware" -permalink: /middleware/retry -hide: true -prev_name: UrlEncoded Middleware -prev_link: ./url-encoded -next_name: Instrumentation Middleware -next_link: ./instrumentation -top_name: Back to Middleware -top_link: ./list ---- - -The `Retry` middleware automatically retries requests that fail due to intermittent client -or server errors (such as network hiccups). -By default, it retries 2 times and handles only timeout exceptions. -It can be configured with an arbitrary number of retries, a list of exceptions to handle, -a retry interval, a percentage of randomness to add to the retry interval, and a backoff factor. -The middleware can also handle the [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) header automatically when configured with the right status codes (see below for an example). - -### Example Usage - -This example will result in a first interval that is random between 0.05 and 0.075 -and a second interval that is random between 0.1 and 0.125. - -```ruby -retry_options = { - max: 2, - interval: 0.05, - interval_randomness: 0.5, - backoff_factor: 2 -} - -conn = Faraday.new(...) do |f| - f.request :retry, retry_options - ... -end - -conn.get('/') -``` - -### Control when the middleware will retry requests - -By default, the `Retry` middleware will only retry idempotent methods and the most common network-related exceptions. -You can change this behaviour by providing the right option when adding the middleware to your connection. - -#### Specify which methods will be retried - -You can provide a `methods` option with a list of HTTP methods. -This will replace the default list of HTTP methods: `delete`, `get`, `head`, `options`, `put`. - -```ruby -retry_options = { - methods: %i[get post] -} -``` - -#### Specify which exceptions should trigger a retry - -You can provide an `exceptions` option with a list of exceptions that will replace -the default list of network-related exceptions: `Errno::ETIMEDOUT`, `Timeout::Error`, `Faraday::TimeoutError`. -This can be particularly useful when combined with the [RaiseError][raise_error] middleware. - -```ruby -retry_options = { - exceptions: [Faraday::ResourceNotFound, Faraday::UnauthorizedError] -} -``` - -#### Specify on which response statuses to retry - -By default the `Retry` middleware will only retry the request if one of the expected exceptions arise. -However, you can specify a list of HTTP statuses you'd like to be retried. When you do so, the middleware will -check the response `status` code and will retry the request if included in the list. - -```ruby -retry_options = { - retry_statuses: [401, 409] -} -``` - -#### Automatically handle the `Retry-After` header - -Some APIs, like the [Slack API](https://api.slack.com/docs/rate-limits), will inform you when you reach their API limits by replying with a response status code of `429` and a response header of `Retry-After` containing a time in seconds. You should then only retry querying after the amount of time provided by the `Retry-After` header, otherwise you won't get a response. - -You can automatically handle this and have Faraday pause and retry for the right amount of time by including the `429` status code in the retry statuses list: - -```ruby -retry_options = { - retry_statuses: [429] -} -``` - -#### Specify a custom retry logic - -You can also specify a custom retry logic with the `retry_if` option. -This option accepts a 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. - -**NOTE:** this option will only be used for methods that are not included in the `methods` option. -If you want this to apply to all HTTP methods, pass `methods: []` as an additional option. - -```ruby -# Retries the request if response contains { success: false } -retry_options = { - retry_if: -> (env, _exc) { env.body[:success] == 'false' } -} -``` - -### Call a block on every retry - -You can specify a block through the `retry_block` option that will be called before every retry. -There are many different applications for this feature, spacing from instrumentation to monitoring. -Request environment, middleware options, current number of retries and the exception is passed to the block as parameters. -For example, you might want to keep track of the response statuses: - -```ruby -response_statuses = [] -retry_options = { - retry_block: -> (env, options, retries, exc) { response_statuses << env.status } -} -``` - - -[raise_error]: ../middleware/raise-error diff --git a/docs/usage/index.md b/docs/usage/index.md index c65852b0b..53c96753f 100644 --- a/docs/usage/index.md +++ b/docs/usage/index.md @@ -156,18 +156,17 @@ To use these great features, create a `Faraday::Connection` with `Faraday.new` and add the correct middleware in a block. For example: ```ruby -require 'faraday_middleware' +require 'faraday' +require 'faraday/retry' conn = Faraday.new('http://httpbingo.org') do |f| f.request :json # encode req bodies as JSON and automatically set the Content-Type header f.request :retry # retry transient failures - f.response :follow_redirects # follow redirects (3xx HTTP response codes) f.response :json # decode response bodies as JSON f.adapter :net_http # adds the adapter to the connection, defaults to `Faraday.default_adapter` end -# Sends a GET request with JSON body that will automatically retry in case of failure -# and follow 3xx redirects. +# Sends a GET request with JSON body that will automatically retry in case of failure. response = conn.get('get', boom: 'zap') # response body is automatically decoded from JSON to a Ruby hash diff --git a/lib/faraday/error.rb b/lib/faraday/error.rb index c543c5ccc..2f0c76ce0 100644 --- a/lib/faraday/error.rb +++ b/lib/faraday/error.rb @@ -141,13 +141,7 @@ class ConnectionFailed < Error class SSLError < Error end - # Raised by FaradayMiddleware::ResponseMiddleware + # Raised by middlewares that parse the response, like the JSON response middleware. class ParsingError < Error end - - # Exception used to control the Retry middleware. - # - # @see Faraday::Request::Retry - class RetriableResponse < Error - end end diff --git a/lib/faraday/request.rb b/lib/faraday/request.rb index d139727b8..b213e0e8d 100644 --- a/lib/faraday/request.rb +++ b/lib/faraday/request.rb @@ -134,5 +134,4 @@ def to_env(connection) require 'faraday/request/instrumentation' require 'faraday/request/json' require 'faraday/request/multipart' -require 'faraday/request/retry' require 'faraday/request/url_encoded' diff --git a/lib/faraday/request/retry.rb b/lib/faraday/request/retry.rb deleted file mode 100644 index 6df392846..000000000 --- a/lib/faraday/request/retry.rb +++ /dev/null @@ -1,241 +0,0 @@ -# frozen_string_literal: true - -module Faraday - class Request - # Catches exceptions and retries each request a limited number of times. - # - # By default, it retries 2 times and handles only timeout exceptions. It can - # be configured with an arbitrary number of retries, a list of exceptions to - # handle, a retry interval, a percentage of randomness to add to the retry - # interval, and a backoff factor. - # - # @example Configure Retry middleware using intervals - # Faraday.new do |conn| - # conn.request(:retry, max: 2, - # interval: 0.05, - # interval_randomness: 0.5, - # backoff_factor: 2, - # exceptions: [CustomException, 'Timeout::Error']) - # - # conn.adapter(:net_http) # NB: Last middleware must be the adapter - # end - # - # This example will result in a first interval that is random between 0.05 - # and 0.075 and a second interval that is random between 0.1 and 0.125. - class Retry < Faraday::Middleware - 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) - - 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 - 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] :methods (the idempotent HTTP methods - # in IDEMPOTENT_METHODS) A list of HTTP methods 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. Request environment, middleware options, current number - # of retries and the exception is passed to the block as parameters. - # @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. - 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) - 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] - begin - # after failure env[:body] is set to the response body - env[:body] = request_body - @app.call(env).tap do |resp| - if @options.retry_statuses.include?(resp.status) - raise Faraday::RetriableResponse.new(nil, resp) - end - end - rescue @errmatch => e - if retries.positive? && retry_request?(env, e) - retries -= 1 - rewind_files(request_body) - if (sleep_amount = calculate_sleep_amount(retries + 1, env)) - @options.retry_block.call(env, @options, retries, e) - sleep sleep_amount - retry - end - end - - raise unless e.is_a?(Faraday::RetriableResponse) - - e.response - 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 body.is_a?(Hash) - - body.each do |_, value| - value.rewind if value.is_a?(UploadIO) - end - end - - # MDN spec for Retry-After header: - # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After - def calculate_retry_after(env) - response_headers = env[:response_headers] - return unless response_headers - - retry_after_value = env[:response_headers]['Retry-After'] - - # 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 - - 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 - end - end -end - -Faraday::Request.register_middleware(retry: Faraday::Request::Retry) diff --git a/spec/faraday/request/retry_spec.rb b/spec/faraday/request/retry_spec.rb deleted file mode 100644 index 2bd9211b5..000000000 --- a/spec/faraday/request/retry_spec.rb +++ /dev/null @@ -1,282 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Faraday::Request::Retry do - let(:calls) { [] } - let(:times_called) { calls.size } - let(:options) { [] } - let(:conn) do - Faraday.new do |b| - b.request :retry, *options - - b.adapter :test do |stub| - %w[get post].each do |method| - stub.send(method, '/unstable') do |env| - calls << env.dup - env[:body] = nil # simulate blanking out response body - callback.call - end - end - end - end - end - - context 'when an unexpected error happens' do - let(:callback) { -> { raise 'boom!' } } - - before { expect { conn.get('/unstable') }.to raise_error(RuntimeError) } - - it { expect(times_called).to eq(1) } - - context 'and this is passed as a custom exception' do - let(:options) { [{ exceptions: StandardError }] } - - it { expect(times_called).to eq(3) } - end - - context 'and this is passed as a string custom exception' do - let(:options) { [{ exceptions: 'StandardError' }] } - - it { expect(times_called).to eq(3) } - end - - context 'and a non-existent string custom exception is passed' do - let(:options) { [{ exceptions: 'WrongStandardErrorNotExisting' }] } - - it { expect(times_called).to eq(1) } - end - end - - context 'when an expected error happens' do - let(:callback) { -> { raise Errno::ETIMEDOUT } } - - before do - @started = Time.now - expect { conn.get('/unstable') }.to raise_error(Errno::ETIMEDOUT) - end - - it { expect(times_called).to eq(3) } - - context 'and legacy max_retry set to 1' do - let(:options) { [1] } - - it { expect(times_called).to eq(2) } - end - - context 'and legacy max_retry set to -9' do - let(:options) { [-9] } - - it { expect(times_called).to eq(1) } - end - - context 'and new max_retry set to 3' do - let(:options) { [{ max: 3 }] } - - it { expect(times_called).to eq(4) } - end - - context 'and new max_retry set to -9' do - let(:options) { [{ max: -9 }] } - - it { expect(times_called).to eq(1) } - end - - context 'and both max_retry and interval are set' do - let(:options) { [{ max: 2, interval: 0.1 }] } - - it { expect(Time.now - @started).to be_within(0.04).of(0.2) } - end - - context 'and retry_block is set' do - let(:options) { [{ retry_block: ->(env, options, retries, exc) { retry_block_calls << [env, options, retries, exc] } }] } - let(:retry_block_calls) { [] } - let(:retry_block_times_called) { retry_block_calls.size } - - it 'calls retry block for each retry' do - expect(retry_block_times_called).to eq(2) - end - end - end - - context 'when no exception raised' do - let(:options) { [{ max: 1, retry_statuses: 429 }] } - - before { conn.get('/unstable') } - - context 'and response code is in retry_statuses' do - let(:callback) { -> { [429, {}, ''] } } - - it { expect(times_called).to eq(2) } - end - - context 'and response code is not in retry_statuses' do - let(:callback) { -> { [503, {}, ''] } } - - it { expect(times_called).to eq(1) } - end - end - - describe '#calculate_retry_interval' do - context 'with exponential backoff' do - let(:options) { { max: 5, interval: 0.1, backoff_factor: 2 } } - let(:middleware) { Faraday::Request::Retry.new(nil, options) } - - it { expect(middleware.send(:calculate_retry_interval, 5)).to eq(0.1) } - it { expect(middleware.send(:calculate_retry_interval, 4)).to eq(0.2) } - it { expect(middleware.send(:calculate_retry_interval, 3)).to eq(0.4) } - end - - context 'with exponential backoff and max_interval' do - let(:options) { { max: 5, interval: 0.1, backoff_factor: 2, max_interval: 0.3 } } - let(:middleware) { Faraday::Request::Retry.new(nil, options) } - - it { expect(middleware.send(:calculate_retry_interval, 5)).to eq(0.1) } - it { expect(middleware.send(:calculate_retry_interval, 4)).to eq(0.2) } - it { expect(middleware.send(:calculate_retry_interval, 3)).to eq(0.3) } - it { expect(middleware.send(:calculate_retry_interval, 2)).to eq(0.3) } - end - - context 'with exponential backoff and interval_randomness' do - let(:options) { { max: 2, interval: 0.1, interval_randomness: 0.05 } } - let(:middleware) { Faraday::Request::Retry.new(nil, options) } - - it { expect(middleware.send(:calculate_retry_interval, 2)).to be_between(0.1, 0.105) } - end - end - - context 'when method is not idempotent' do - let(:callback) { -> { raise Errno::ETIMEDOUT } } - - before { expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT) } - - it { expect(times_called).to eq(1) } - end - - describe 'retry_if option' do - let(:callback) { -> { raise Errno::ETIMEDOUT } } - let(:options) { [{ retry_if: @check }] } - - it 'retries if retry_if block always returns true' do - body = { foo: :bar } - @check = ->(_, _) { true } - expect { conn.post('/unstable', body) }.to raise_error(Errno::ETIMEDOUT) - expect(times_called).to eq(3) - expect(calls.all? { |env| env[:body] == body }).to be_truthy - end - - it 'does not retry if retry_if block returns false checking env' do - @check = ->(env, _) { env[:method] != :post } - expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT) - expect(times_called).to eq(1) - end - - it 'does not retry if retry_if block returns false checking exception' do - @check = ->(_, exception) { !exception.is_a?(Errno::ETIMEDOUT) } - expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT) - expect(times_called).to eq(1) - end - - it 'FilePart: should rewind files on retry' do - io = StringIO.new('Test data') - filepart = Faraday::FilePart.new(io, 'application/octet/stream') - - rewound = 0 - rewind = -> { rewound += 1 } - - @check = ->(_, _) { true } - allow(filepart).to receive(:rewind, &rewind) - expect { conn.post('/unstable', file: filepart) }.to raise_error(Errno::ETIMEDOUT) - expect(times_called).to eq(3) - expect(rewound).to eq(2) - end - - it 'UploadIO: should rewind files on retry' do - io = StringIO.new('Test data') - upload_io = Faraday::FilePart.new(io, 'application/octet/stream') - - rewound = 0 - rewind = -> { rewound += 1 } - - @check = ->(_, _) { true } - allow(upload_io).to receive(:rewind, &rewind) - expect { conn.post('/unstable', file: upload_io) }.to raise_error(Errno::ETIMEDOUT) - expect(times_called).to eq(3) - expect(rewound).to eq(2) - end - - context 'when explicitly specifying methods to retry' do - let(:options) { [{ retry_if: @check, methods: [:post] }] } - - it 'does not call retry_if for specified methods' do - @check = ->(_, _) { raise 'this should have never been called' } - expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT) - expect(times_called).to eq(3) - end - end - - context 'with empty list of methods to retry' do - let(:options) { [{ retry_if: @check, methods: [] }] } - - it 'calls retry_if for all methods' do - @check = ->(_, _) { calls.size < 2 } - expect { conn.get('/unstable') }.to raise_error(Errno::ETIMEDOUT) - expect(times_called).to eq(2) - end - end - end - - describe 'retry_after header support' do - let(:callback) { -> { [504, headers, ''] } } - let(:elapsed) { Time.now - @started } - - before do - @started = Time.now - conn.get('/unstable') - end - - context 'when retry_after bigger than interval' do - let(:headers) { { 'Retry-After' => '0.5' } } - let(:options) { [{ max: 1, interval: 0.1, retry_statuses: 504 }] } - - it { expect(elapsed).to be > 0.5 } - end - - context 'when retry_after smaller than interval' do - let(:headers) { { 'Retry-After' => '0.1' } } - let(:options) { [{ max: 1, interval: 0.2, retry_statuses: 504 }] } - - it { expect(elapsed).to be > 0.2 } - end - - context 'when retry_after is a timestamp' do - let(:headers) { { 'Retry-After' => (Time.now.utc + 2).strftime('%a, %d %b %Y %H:%M:%S GMT') } } - let(:options) { [{ max: 1, interval: 0.1, retry_statuses: 504 }] } - - it { expect(elapsed).to be > 1 } - end - - context 'when retry_after is bigger than max_interval' do - let(:headers) { { 'Retry-After' => (Time.now.utc + 20).strftime('%a, %d %b %Y %H:%M:%S GMT') } } - let(:options) { [{ max: 2, interval: 0.1, max_interval: 5, retry_statuses: 504 }] } - - it { expect(times_called).to eq(1) } - - context 'and retry_block is set' do - let(:options) do - [{ - retry_block: ->(env, options, retries, exc) { retry_block_calls << [env, options, retries, exc] }, - max: 2, - max_interval: 5, - retry_statuses: 504 - }] - end - - let(:retry_block_calls) { [] } - let(:retry_block_times_called) { retry_block_calls.size } - - it 'retry_block is not called' do - expect(retry_block_times_called).to eq(0) - end - end - end - end -end