mirror of
https://github.com/lostisland/faraday.git
synced 2025-08-15 00:04:38 -04:00
Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
47b3979850 | ||
|
ef0040fe4c | ||
|
0ee48490b0 | ||
|
3f127bb10f | ||
|
554930c6c5 | ||
|
c65d3be5fb | ||
|
aa17e5b3df | ||
|
537f5d7f2d | ||
|
4a8a504fe2 | ||
|
1d7c8ad2fc | ||
|
80407137f1 | ||
|
3f0acb63b1 | ||
|
d0efa09282 | ||
|
8cd1bf1190 | ||
|
2a5702a616 | ||
|
d881f163c0 | ||
|
18ba248d14 | ||
|
a84e6b4dba | ||
|
8f32d7d59d |
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
@ -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.
|
||||
|
1
Gemfile
1
Gemfile
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
#
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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
|
@ -1,5 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
VERSION = '1.7.0'
|
||||
VERSION = '1.9.3'
|
||||
end
|
||||
|
@ -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|
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user