Compare commits

...

19 Commits
main ... v1.9.3

Author SHA1 Message Date
Matt
47b3979850
Version bump to 1.9.3 2022-01-06 21:48:24 +00:00
Matt
ef0040fe4c
Re-add support for Ruby 2.4+ (#1371) 2022-01-06 21:45:11 +00:00
Matt
0ee48490b0
Version bump to 1.9.2 2022-01-06 17:50:53 +00:00
Matt
3f127bb10f
Add alias with legacy name to gemified middleware (#1372) 2022-01-06 17:50:29 +00:00
Matt
554930c6c5
Version bump to 1.9.1 2022-01-06 17:05:58 +00:00
Matt
c65d3be5fb
Update adapter dependencies in Gemspec (#1370)
Adapters v2 have hard dependencies, hence can't be used with Faraday v1
2022-01-06 17:05:29 +00:00
Mattia Giuffrida
aa17e5b3df Update CI branches 2022-01-06 16:07:21 +01:00
Mattia Giuffrida
537f5d7f2d Update CI with correct ruby versions 2022-01-06 16:07:21 +01:00
Mattia Giuffrida
4a8a504fe2 Require Ruby 2.6+ 2022-01-06 16:07:21 +01:00
Mattia Giuffrida
1d7c8ad2fc Use external multipart and retry middleware 2022-01-06 16:07:21 +01:00
Matt
80407137f1
Version bump to 1.8.1 2022-01-05 21:35:46 +00:00
Matt
3f0acb63b1
Remove version lock from faraday-net_http 2022-01-05 21:21:33 +00:00
Matt
d0efa09282
Version bump to 1.8.0 2021-09-18 09:20:47 +02:00
Matt
8cd1bf1190
Proc auth value backport cleanup (#1326) 2021-09-18 09:19:37 +02:00
Jarl Friis
2a5702a616
Backport authorization procs to 1.x branch (#1322) 2021-09-17 09:39:56 +02:00
Matt
d881f163c0
Version bump to 1.7.2 2021-09-13 17:41:35 +02:00
Matt
18ba248d14
Fix deprecation warning (#1323) 2021-09-13 15:45:18 +02:00
Matt
a84e6b4dba
Version bump to 1.7.1 2021-08-30 09:30:03 +01:00
Yutaka Kamei
8f32d7d59d
Respect the params_encoder in Faraday::Adapter::Test (#1316) 2021-08-30 09:25:24 +01:00
22 changed files with 172 additions and 1141 deletions

View File

@ -2,9 +2,8 @@ name: CI
on:
pull_request:
push:
branches: [main, 0.1x]
branches: [main, 0.1x, 1.x]
env:
GIT_COMMIT_SHA: ${{ github.sha }}
@ -43,7 +42,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ruby: ['2.4', '2.5', '2.6', '2.7', '3.0']
ruby: ['2.4', '2.5', '2.6', '2.7', '3.0', '3.1']
steps:
- uses: actions/checkout@v1
@ -53,6 +52,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install libcurl4-openssl-dev
- name: Build

View File

@ -14,7 +14,7 @@ Metrics/AbcSize:
# Offense count: 5
# Configuration parameters: CountComments, CountAsOne.
Metrics/ClassLength:
Max: 256
Max: 260
# Offense count: 15
# Configuration parameters: IgnoredMethods.

View File

@ -7,6 +7,7 @@ ruby RUBY_VERSION
gem 'jruby-openssl', '~> 0.10.7', platforms: :jruby
group :development, :test do
gem 'irb'
gem 'pry'
gem 'rake'
end

View File

@ -23,6 +23,16 @@ Faraday.new(...) do |conn|
end
```
### With a proc
You can also provide a proc, which will be evaluated on each request:
```ruby
Faraday.new(...) do |conn|
conn.request :authorization, 'Bearer', -> { MyAuthStorage.get_auth_token }
end
```
### Basic Authentication
`BasicAuthentication` adds a 'Basic' type Authorization header to a Faraday request.

View File

@ -81,4 +81,17 @@ RSpec.describe Client do
stubs.verify_stubbed_calls
end
end
context 'When the Faraday connection is configured with FlatParamsEncoder' do
let(:conn) { Faraday.new(request: { params_encoder: Faraday::FlatParamsEncoder }) { |b| b.adapter(:test, stubs) } }
it 'handles the same multiple URL parameters' do
stubs.get('/ebi?a=x&a=y&a=z') { [200, { 'Content-Type' => 'application/json' }, '{"name": "shrimp"}'] }
# uncomment to raise Stubs::NotFound
# expect(client.sushi('ebi', params: { a: %w[x y] })).to eq('shrimp')
expect(client.sushi('ebi', params: { a: %w[x y z] })).to eq('shrimp')
stubs.verify_stubbed_calls
end
end
end

View File

@ -13,8 +13,8 @@ class Client
@conn = conn
end
def sushi(jname)
res = @conn.get("/#{jname}")
def sushi(jname, params: {})
res = @conn.get("/#{jname}", params)
data = JSON.parse(res.body)
data['name']
end
@ -70,6 +70,45 @@ class ClientTest < Test::Unit::TestCase
stubs.verify_stubbed_calls
end
def test_strict_mode
stubs = Faraday::Adapter::Test::Stubs.new(strict_mode: true)
stubs.get('/ebi?abc=123') do
[
200,
{ 'Content-Type': 'application/javascript' },
'{"name": "shrimp"}'
]
end
cli = client(stubs)
assert_equal 'shrimp', cli.sushi('ebi', params: { abc: 123 })
# uncomment to raise Stubs::NotFound
# assert_equal 'shrimp', cli.sushi('ebi', params: { abc: 123, foo: 'Kappa' })
stubs.verify_stubbed_calls
end
def test_non_default_params_encoder
stubs = Faraday::Adapter::Test::Stubs.new(strict_mode: true)
stubs.get('/ebi?a=x&a=y&a=z') do
[
200,
{ 'Content-Type': 'application/javascript' },
'{"name": "shrimp"}'
]
end
conn = Faraday.new(request: { params_encoder: Faraday::FlatParamsEncoder }) do |builder|
builder.adapter :test, stubs
end
cli = Client.new(conn)
assert_equal 'shrimp', cli.sushi('ebi', params: { a: %w[x y z] })
# uncomment to raise Stubs::NotFound
# assert_equal 'shrimp', cli.sushi('ebi', params: { a: %w[x y] })
stubs.verify_stubbed_calls
end
def client(stubs)
conn = Faraday.new do |builder|
builder.adapter :test, stubs

View File

@ -18,12 +18,13 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
spec.add_dependency 'faraday-em_http', '~> 1.0'
spec.add_dependency 'faraday-em_synchrony', '~> 1.0'
spec.add_dependency 'faraday-excon', '~> 1.1'
spec.add_dependency 'faraday-httpclient', '~> 1.0.1'
spec.add_dependency 'faraday-httpclient', '~> 1.0'
spec.add_dependency 'faraday-multipart', '~> 1.0'
spec.add_dependency 'faraday-net_http', '~> 1.0'
spec.add_dependency 'faraday-net_http_persistent', '~> 1.1'
spec.add_dependency 'faraday-net_http_persistent', '~> 1.0'
spec.add_dependency 'faraday-patron', '~> 1.0'
spec.add_dependency 'faraday-rack', '~> 1.0'
spec.add_dependency 'multipart-post', '>= 1.2', '< 3'
spec.add_dependency 'faraday-retry', '~> 1.0'
spec.add_dependency 'ruby2_keywords', '>= 0.0.4'
# Includes `examples` and `spec` to allow external adapter gems to run Faraday unit and integration tests

View File

@ -24,9 +24,15 @@ require 'faraday/adapter'
require 'faraday/request'
require 'faraday/response'
require 'faraday/error'
require 'faraday/file_part'
require 'faraday/param_part'
require 'faraday/request/url_encoded' # needed by multipart
# External Middleware gems and their aliases
require 'faraday/multipart'
require 'faraday/retry'
Faraday::Request::Multipart = Faraday::Multipart::Middleware
Faraday::Request::Retry = Faraday::Retry::Middleware
# External Adapters gems
unless defined?(JRUBY_VERSION)
require 'faraday/em_http'
require 'faraday/em_synchrony'

View File

@ -62,18 +62,20 @@ module Faraday
@stack.empty?
end
def match(request_method, host, path, headers, body)
# @param env [Faraday::Env]
def match(env)
request_method = env[:method]
return false unless @stack.key?(request_method)
stack = @stack[request_method]
consumed = (@consumed[request_method] ||= [])
stub, meta = matches?(stack, host, path, headers, body)
stub, meta = matches?(stack, env)
if stub
consumed << stack.delete(stub)
return stub, meta
end
matches?(consumed, host, path, headers, body)
matches?(consumed, env)
end
def get(path, headers = {}, &block)
@ -142,15 +144,18 @@ module Faraday
Faraday::Utils.URI(path).host
]
end
path, query = normalized_path.respond_to?(:split) ? normalized_path.split('?') : normalized_path
headers = Utils::Headers.new(headers)
stub = Stub.new(host, normalized_path, headers, body, @strict_mode, block)
stub = Stub.new(host, path, query, headers, body, @strict_mode, block)
(@stack[request_method] ||= []) << stub
end
def matches?(stack, host, path, headers, body)
# @param stack [Hash]
# @param env [Faraday::Env]
def matches?(stack, env)
stack.each do |stub|
match_result, meta = stub.matches?(host, path, headers, body)
match_result, meta = stub.matches?(env)
return stub, meta if match_result
end
nil
@ -158,35 +163,20 @@ module Faraday
end
# Stub request
# rubocop:disable Style/StructInheritance
class Stub < Struct.new(:host, :path, :params, :headers, :body, :strict_mode, :block)
# rubocop:enable Style/StructInheritance
def initialize(host, full, headers, body, strict_mode, block) # rubocop:disable Metrics/ParameterLists
path, query = full.respond_to?(:split) ? full.split('?') : full
params =
if query
Faraday::Utils.parse_nested_query(query)
else
{}
end
class Stub < Struct.new(:host, :path, :query, :headers, :body, :strict_mode, :block) # rubocop:disable Style/StructInheritance
# @param env [Faraday::Env]
def matches?(env)
request_host = env[:url].host
request_path = Faraday::Utils.normalize_path(env[:url].path)
request_headers = env.request_headers
request_body = env[:body]
super(host, path, params, headers, body, strict_mode, block)
end
def matches?(request_host, request_uri, request_headers, request_body)
request_path, request_query = request_uri.split('?')
request_params =
if request_query
Faraday::Utils.parse_nested_query(request_query)
else
{}
end
# meta is a hash used as carrier
# that will be yielded to consumer block
meta = {}
[(host.nil? || host == request_host) &&
path_match?(request_path, meta) &&
params_match?(request_params) &&
params_match?(env) &&
(body.to_s.size.zero? || request_body == body) &&
headers_match?(request_headers), meta]
end
@ -199,7 +189,11 @@ module Faraday
end
end
def params_match?(request_params)
# @param env [Faraday::Env]
def params_match?(env)
request_params = env[:params]
params = env.params_encoder.decode(query) || {}
if strict_mode
return Set.new(params) == Set.new(request_params)
end
@ -239,26 +233,19 @@ module Faraday
yield(stubs)
end
# @param env [Faraday::Env]
def call(env)
super
host = env[:url].host
normalized_path = Faraday::Utils.normalize_path(env[:url])
params_encoder = env.request.params_encoder ||
Faraday::Utils.default_params_encoder
stub, meta = stubs.match(env[:method], host, normalized_path,
env.request_headers, env[:body])
env.request.params_encoder ||= Faraday::Utils.default_params_encoder
env[:params] = env.params_encoder.decode(env[:url].query) || {}
stub, meta = stubs.match(env)
unless stub
raise Stubs::NotFound, "no stubbed request for #{env[:method]} "\
"#{normalized_path} #{env[:body]}"
"#{env[:url]} #{env[:body]}"
end
env[:params] = if (query = env[:url].query)
params_encoder.decode(query)
else
{}
end
block_arity = stub.block.arity
status, headers, body =
if block_arity >= 0

View File

@ -433,13 +433,18 @@ module Faraday
uri.query = nil
with_uri_credentials(uri) do |user, password|
basic_auth user, password
set_basic_auth(user, password)
uri.user = uri.password = nil
end
@proxy = proxy_from_env(url) unless @manual_proxy
end
def set_basic_auth(user, password)
header = Faraday::Request::BasicAuthentication.header(user, password)
headers[Faraday::Request::Authorization::KEY] = header
end
# Sets the path prefix and ensures that it always has a leading
# slash.
#

View File

@ -143,10 +143,4 @@ module Faraday
# Raised by FaradayMiddleware::ResponseMiddleware
class ParsingError < Error
end
# Exception used to control the Retry middleware.
#
# @see Faraday::Request::Retry
class RetriableResponse < Error
end
end

View File

@ -1,128 +0,0 @@
# frozen_string_literal: true
require 'stringio'
# multipart-post gem
require 'composite_io'
require 'parts'
module Faraday
# Multipart value used to POST a binary data from a file or
#
# @example
# payload = { file: Faraday::FilePart.new("file_name.ext", "content/type") }
# http.post("/upload", payload)
#
# @!method initialize(filename_or_io, content_type, filename = nil, opts = {})
#
# @param filename_or_io [String, IO] Either a String filename to a local
# file or an open IO object.
# @param content_type [String] String content type of the file data.
# @param filename [String] Optional String filename, usually to add context
# to a given IO object.
# @param opts [Hash] Optional Hash of String key/value pairs to describethis
# this uploaded file. Expected Header keys include:
# * Content-Transfer-Encoding - Defaults to "binary"
# * Content-Disposition - Defaults to "form-data"
# * Content-Type - Defaults to the content_type argument.
# * Content-ID - Optional.
#
# @return [Faraday::FilePart]
#
# @!attribute [r] content_type
# The uploaded binary data's content type.
#
# @return [String]
#
# @!attribute [r] original_filename
# The base filename, taken either from the filename_or_io or filename
# arguments in #initialize.
#
# @return [String]
#
# @!attribute [r] opts
# Extra String key/value pairs to make up the header for this uploaded file.
#
# @return [Hash]
#
# @!attribute [r] io
# The open IO object for the uploaded file.
#
# @return [IO]
FilePart = ::UploadIO
# Multipart value used to POST a file.
#
# @deprecated Use FilePart instead of this class. It behaves identically, with
# a matching name to ParamPart.
UploadIO = ::UploadIO
Parts = ::Parts
# Similar to, but not compatible with CompositeReadIO provided by the
# multipart-post gem.
# https://github.com/nicksieger/multipart-post/blob/master/lib/composite_io.rb
class CompositeReadIO
def initialize(*parts)
@parts = parts.flatten
@ios = @parts.map(&:to_io)
@index = 0
end
# @return [Integer] sum of the lengths of all the parts
def length
@parts.inject(0) { |sum, part| sum + part.length }
end
# Rewind each of the IOs and reset the index to 0.
#
# @return [void]
def rewind
@ios.each(&:rewind)
@index = 0
end
# Read from IOs in order until `length` bytes have been received.
#
# @param length [Integer, nil]
# @param outbuf [String, nil]
def read(length = nil, outbuf = nil)
got_result = false
outbuf = outbuf ? (+outbuf).replace('') : +''
while (io = current_io)
if (result = io.read(length))
got_result ||= !result.nil?
result.force_encoding('BINARY') if result.respond_to?(:force_encoding)
outbuf << result
length -= result.length if length
break if length&.zero?
end
advance_io
end
!got_result && length ? nil : outbuf
end
# Close each of the IOs.
#
# @return [void]
def close
@ios.each(&:close)
end
def ensure_open_and_readable
# Rubinius compatibility
end
private
def current_io
@ios[@index]
end
def advance_io
@index += 1
end
end
end

View File

@ -1,53 +0,0 @@
# frozen_string_literal: true
module Faraday
# Multipart value used to POST data with a content type.
class ParamPart
# @param value [String] Uploaded content as a String.
# @param content_type [String] String content type of the value.
# @param content_id [String] Optional String of this value's Content-ID.
#
# @return [Faraday::ParamPart]
def initialize(value, content_type, content_id = nil)
@value = value
@content_type = content_type
@content_id = content_id
end
# Converts this value to a form part.
#
# @param boundary [String] String multipart boundary that must not exist in
# the content exactly.
# @param key [String] String key name for this value.
#
# @return [Faraday::Parts::Part]
def to_part(boundary, key)
Faraday::Parts::Part.new(boundary, key, value, headers)
end
# Returns a Hash of String key/value pairs.
#
# @return [Hash]
def headers
{
'Content-Type' => content_type,
'Content-ID' => content_id
}
end
# The content to upload.
#
# @return [String]
attr_reader :value
# The value's content type.
#
# @return [String]
attr_reader :content_type
# The value's content ID, if given.
#
# @return [String, nil]
attr_reader :content_id
end
end

View File

@ -35,8 +35,6 @@ module Faraday
register_middleware File.expand_path('request', __dir__),
url_encoded: [:UrlEncoded, 'url_encoded'],
multipart: [:Multipart, 'multipart'],
retry: [:Retry, 'retry'],
authorization: [:Authorization, 'authorization'],
basic_auth: [
:BasicAuthentication,

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
require 'base64'
module Faraday
class Request
# Request middleware for the Authorization HTTP header
@ -13,7 +15,8 @@ module Faraday
# @return [String] a header value
def self.header(type, token)
case token
when String, Symbol
when String, Symbol, Proc
token = token.call if token.is_a?(Proc)
"#{type} #{token}"
when Hash
build_hash(type.to_s, token)
@ -32,6 +35,7 @@ module Faraday
comma = ', '
values = []
hash.each do |key, value|
value = value.call if value.is_a?(Proc)
values << "#{key}=#{value.to_s.inspect}"
end
"#{type} #{values * comma}"
@ -39,16 +43,19 @@ module Faraday
# @param app [#call]
# @param type [String, Symbol] Type of Authorization
# @param token [String, Symbol, Hash] Token value for the Authorization
def initialize(app, type, token)
@header_value = self.class.header(type, token)
# @param param [String, Symbol, Hash, Proc] parameter to build the Authorization header.
# This value can be a proc, in which case it will be invoked on each request.
def initialize(app, type, param)
@type = type
@param = param
super(app)
end
# @param env [Faraday::Env]
def call(env)
env.request_headers[KEY] = @header_value unless env.request_headers[KEY]
@app.call(env)
def on_request(env)
return if env.request_headers[KEY]
env.request_headers[KEY] = self.class.header(@type, @param)
end
end
end

View File

@ -1,106 +0,0 @@
# frozen_string_literal: true
require File.expand_path('url_encoded', __dir__)
require 'securerandom'
module Faraday
class Request
# Middleware for supporting multi-part requests.
class Multipart < UrlEncoded
self.mime_type = 'multipart/form-data'
unless defined?(::Faraday::Request::Multipart::DEFAULT_BOUNDARY_PREFIX)
DEFAULT_BOUNDARY_PREFIX = '-----------RubyMultipartPost'
end
def initialize(app = nil, options = {})
super(app)
@options = options
end
# Checks for files in the payload, otherwise leaves everything untouched.
#
# @param env [Faraday::Env]
def call(env)
match_content_type(env) do |params|
env.request.boundary ||= unique_boundary
env.request_headers[CONTENT_TYPE] +=
"; boundary=#{env.request.boundary}"
env.body = create_multipart(env, params)
end
@app.call env
end
# @param env [Faraday::Env]
def process_request?(env)
type = request_type(env)
env.body.respond_to?(:each_key) && !env.body.empty? && (
(type.empty? && has_multipart?(env.body)) ||
(type == self.class.mime_type)
)
end
# Returns true if obj is an enumerable with values that are multipart.
#
# @param obj [Object]
# @return [Boolean]
def has_multipart?(obj) # rubocop:disable Naming/PredicateName
if obj.respond_to?(:each)
(obj.respond_to?(:values) ? obj.values : obj).each do |val|
return true if val.respond_to?(:content_type) || has_multipart?(val)
end
end
false
end
# @param env [Faraday::Env]
# @param params [Hash]
def create_multipart(env, params)
boundary = env.request.boundary
parts = process_params(params) do |key, value|
part(boundary, key, value)
end
parts << Faraday::Parts::EpiloguePart.new(boundary)
body = Faraday::CompositeReadIO.new(parts)
env.request_headers[Faraday::Env::ContentLength] = body.length.to_s
body
end
def part(boundary, key, value)
if value.respond_to?(:to_part)
value.to_part(boundary, key)
else
Faraday::Parts::Part.new(boundary, key, value)
end
end
# @return [String]
def unique_boundary
"#{DEFAULT_BOUNDARY_PREFIX}-#{SecureRandom.hex}"
end
# @param params [Hash]
# @param prefix [String]
# @param pieces [Array]
def process_params(params, prefix = nil, pieces = nil, &block)
params.inject(pieces || []) do |all, (key, value)|
if prefix
key = @options[:flat_encode] ? prefix.to_s : "#{prefix}[#{key}]"
end
case value
when Array
values = value.inject([]) { |a, v| a << [nil, v] }
process_params(values, key, all, &block)
when Hash
process_params(value, key, all, &block)
else
# rubocop:disable Performance/RedundantBlockCall
all << block.call(key, value)
# rubocop:enable Performance/RedundantBlockCall
end
end
end
end
end
end

View File

@ -1,239 +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)
@options.retry_block.call(env, @options, retries, e)
if (sleep_amount = calculate_sleep_amount(retries + 1, env))
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
error.class.to_s == 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

View File

@ -1,5 +1,5 @@
# frozen_string_literal: true
module Faraday
VERSION = '1.7.0'
VERSION = '1.9.3'
end

View File

@ -258,6 +258,38 @@ RSpec.describe Faraday::Adapter::Test do
end
end
describe 'for request with non default params encoder' do
let(:connection) do
Faraday.new(request: { params_encoder: Faraday::FlatParamsEncoder }) do |builder|
builder.adapter :test, stubs
end
end
let(:stubs) do
described_class::Stubs.new do |stubs|
stubs.get('/path?a=x&a=y&a=z') { [200, {}, 'a'] }
end
end
context 'when all flat param values are correctly set' do
subject(:request) { connection.get('/path?a=x&a=y&a=z') }
it { expect(request.status).to eq 200 }
end
shared_examples 'raise NotFound when params do not satisfy the flat param values' do |params|
subject(:request) { connection.get('/path', params) }
context "with #{params.inspect}" do
it { expect { request }.to raise_error described_class::Stubs::NotFound }
end
end
it_behaves_like 'raise NotFound when params do not satisfy the flat param values', { a: %w[x] }
it_behaves_like 'raise NotFound when params do not satisfy the flat param values', { a: %w[x y] }
it_behaves_like 'raise NotFound when params do not satisfy the flat param values', { a: %w[x z y] } # NOTE: The order of the value is also compared.
it_behaves_like 'raise NotFound when params do not satisfy the flat param values', { b: %w[x y z] }
end
describe 'strict_mode' do
let(:stubs) do
described_class::Stubs.new(strict_mode: true) do |stubs|

View File

@ -84,5 +84,13 @@ RSpec.describe Faraday::Request::Authorization do
include_examples 'does not interfere with existing authentication'
end
context 'when passed a string and a proc' do
let(:auth_config) { ['Bearer', -> { 'custom_from_proc' }] }
it { expect(response.body).to eq('Bearer custom_from_proc') }
include_examples 'does not interfere with existing authentication'
end
end
end

View File

@ -1,302 +0,0 @@
# frozen_string_literal: true
RSpec.describe Faraday::Request::Multipart do
let(:options) { {} }
let(:conn) do
Faraday.new do |b|
b.request :multipart, options
b.request :url_encoded
b.adapter :test do |stub|
stub.post('/echo') do |env|
posted_as = env[:request_headers]['Content-Type']
expect(env[:body]).to be_a_kind_of(Faraday::CompositeReadIO)
[200, { 'Content-Type' => posted_as }, env[:body].read]
end
end
end
end
shared_examples 'a multipart request' do
it 'generates a unique boundary for each request' do
response1 = conn.post('/echo', payload)
response2 = conn.post('/echo', payload)
b1 = parse_multipart_boundary(response1.headers['Content-Type'])
b2 = parse_multipart_boundary(response2.headers['Content-Type'])
expect(b1).to_not eq(b2)
end
end
context 'FilePart: when multipart objects in param' do
let(:payload) do
{
a: 1,
b: {
c: Faraday::FilePart.new(__FILE__, 'text/x-ruby', nil,
'Content-Disposition' => 'form-data; foo=1'),
d: 2
}
}
end
it_behaves_like 'a multipart request'
it 'forms a multipart request' do
response = conn.post('/echo', payload)
boundary = parse_multipart_boundary(response.headers['Content-Type'])
result = parse_multipart(boundary, response.body)
expect(result[:errors]).to be_empty
part_a, body_a = result.part('a')
expect(part_a).to_not be_nil
expect(part_a.filename).to be_nil
expect(body_a).to eq('1')
part_bc, body_bc = result.part('b[c]')
expect(part_bc).to_not be_nil
expect(part_bc.filename).to eq('multipart_spec.rb')
expect(part_bc.headers['content-disposition'])
.to eq(
'form-data; foo=1; name="b[c]"; filename="multipart_spec.rb"'
)
expect(part_bc.headers['content-type']).to eq('text/x-ruby')
expect(part_bc.headers['content-transfer-encoding']).to eq('binary')
expect(body_bc).to eq(File.read(__FILE__))
part_bd, body_bd = result.part('b[d]')
expect(part_bd).to_not be_nil
expect(part_bd.filename).to be_nil
expect(body_bd).to eq('2')
end
end
context 'FilePart: when providing json and IO content in the same payload' do
let(:io) { StringIO.new('io-content') }
let(:json) do
{
b: 1,
c: 2
}.to_json
end
let(:payload) do
{
json: Faraday::ParamPart.new(json, 'application/json'),
io: Faraday::FilePart.new(io, 'application/pdf')
}
end
it_behaves_like 'a multipart request'
it 'forms a multipart request' do
response = conn.post('/echo', payload)
boundary = parse_multipart_boundary(response.headers['Content-Type'])
result = parse_multipart(boundary, response.body)
expect(result[:errors]).to be_empty
part_json, body_json = result.part('json')
expect(part_json).to_not be_nil
expect(part_json.mime).to eq('application/json')
expect(part_json.filename).to be_nil
expect(body_json).to eq(json)
part_io, body_io = result.part('io')
expect(part_io).to_not be_nil
expect(part_io.mime).to eq('application/pdf')
expect(part_io.filename).to eq('local.path')
expect(body_io).to eq(io.string)
end
end
context 'FilePart: when multipart objects in array param' do
let(:payload) do
{
a: 1,
b: [{
c: Faraday::FilePart.new(__FILE__, 'text/x-ruby'),
d: 2
}]
}
end
it_behaves_like 'a multipart request'
it 'forms a multipart request' do
response = conn.post('/echo', payload)
boundary = parse_multipart_boundary(response.headers['Content-Type'])
result = parse_multipart(boundary, response.body)
expect(result[:errors]).to be_empty
part_a, body_a = result.part('a')
expect(part_a).to_not be_nil
expect(part_a.filename).to be_nil
expect(body_a).to eq('1')
part_bc, body_bc = result.part('b[][c]')
expect(part_bc).to_not be_nil
expect(part_bc.filename).to eq('multipart_spec.rb')
expect(part_bc.headers['content-disposition'])
.to eq(
'form-data; name="b[][c]"; filename="multipart_spec.rb"'
)
expect(part_bc.headers['content-type']).to eq('text/x-ruby')
expect(part_bc.headers['content-transfer-encoding']).to eq('binary')
expect(body_bc).to eq(File.read(__FILE__))
part_bd, body_bd = result.part('b[][d]')
expect(part_bd).to_not be_nil
expect(part_bd.filename).to be_nil
expect(body_bd).to eq('2')
end
end
context 'UploadIO: when multipart objects in param' do
let(:payload) do
{
a: 1,
b: {
c: Faraday::UploadIO.new(__FILE__, 'text/x-ruby', nil,
'Content-Disposition' => 'form-data; foo=1'),
d: 2
}
}
end
it_behaves_like 'a multipart request'
it 'forms a multipart request' do
response = conn.post('/echo', payload)
boundary = parse_multipart_boundary(response.headers['Content-Type'])
result = parse_multipart(boundary, response.body)
expect(result[:errors]).to be_empty
part_a, body_a = result.part('a')
expect(part_a).to_not be_nil
expect(part_a.filename).to be_nil
expect(body_a).to eq('1')
part_bc, body_bc = result.part('b[c]')
expect(part_bc).to_not be_nil
expect(part_bc.filename).to eq('multipart_spec.rb')
expect(part_bc.headers['content-disposition'])
.to eq(
'form-data; foo=1; name="b[c]"; filename="multipart_spec.rb"'
)
expect(part_bc.headers['content-type']).to eq('text/x-ruby')
expect(part_bc.headers['content-transfer-encoding']).to eq('binary')
expect(body_bc).to eq(File.read(__FILE__))
part_bd, body_bd = result.part('b[d]')
expect(part_bd).to_not be_nil
expect(part_bd.filename).to be_nil
expect(body_bd).to eq('2')
end
end
context 'UploadIO: when providing json and IO content in the same payload' do
let(:io) { StringIO.new('io-content') }
let(:json) do
{
b: 1,
c: 2
}.to_json
end
let(:payload) do
{
json: Faraday::ParamPart.new(json, 'application/json'),
io: Faraday::UploadIO.new(io, 'application/pdf')
}
end
it_behaves_like 'a multipart request'
it 'forms a multipart request' do
response = conn.post('/echo', payload)
boundary = parse_multipart_boundary(response.headers['Content-Type'])
result = parse_multipart(boundary, response.body)
expect(result[:errors]).to be_empty
part_json, body_json = result.part('json')
expect(part_json).to_not be_nil
expect(part_json.mime).to eq('application/json')
expect(part_json.filename).to be_nil
expect(body_json).to eq(json)
part_io, body_io = result.part('io')
expect(part_io).to_not be_nil
expect(part_io.mime).to eq('application/pdf')
expect(part_io.filename).to eq('local.path')
expect(body_io).to eq(io.string)
end
end
context 'UploadIO: when multipart objects in array param' do
let(:payload) do
{
a: 1,
b: [{
c: Faraday::UploadIO.new(__FILE__, 'text/x-ruby'),
d: 2
}]
}
end
it_behaves_like 'a multipart request'
it 'forms a multipart request' do
response = conn.post('/echo', payload)
boundary = parse_multipart_boundary(response.headers['Content-Type'])
result = parse_multipart(boundary, response.body)
expect(result[:errors]).to be_empty
part_a, body_a = result.part('a')
expect(part_a).to_not be_nil
expect(part_a.filename).to be_nil
expect(body_a).to eq('1')
part_bc, body_bc = result.part('b[][c]')
expect(part_bc).to_not be_nil
expect(part_bc.filename).to eq('multipart_spec.rb')
expect(part_bc.headers['content-disposition'])
.to eq(
'form-data; name="b[][c]"; filename="multipart_spec.rb"'
)
expect(part_bc.headers['content-type']).to eq('text/x-ruby')
expect(part_bc.headers['content-transfer-encoding']).to eq('binary')
expect(body_bc).to eq(File.read(__FILE__))
part_bd, body_bd = result.part('b[][d]')
expect(part_bd).to_not be_nil
expect(part_bd.filename).to be_nil
expect(body_bd).to eq('2')
end
end
context 'when passing flat_encode=true option' do
let(:options) { { flat_encode: true } }
let(:io) { StringIO.new('io-content') }
let(:payload) do
{
a: 1,
b: [
Faraday::UploadIO.new(io, 'application/pdf'),
Faraday::UploadIO.new(io, 'application/pdf')
]
}
end
it_behaves_like 'a multipart request'
it 'encode params using flat encoder' do
response = conn.post('/echo', payload)
expect(response.body).to include('name="b"')
expect(response.body).not_to include('name="b[]"')
end
end
end

View File

@ -1,242 +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
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
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::UploadIO.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) }
end
end
end