Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
@@ -0,0 +1,101 @@
# frozen_string_literal: true
module Faraday
# Base class for all Faraday adapters. Adapters are
# responsible for fulfilling a Faraday request.
class Adapter
extend MiddlewareRegistry
CONTENT_LENGTH = 'Content-Length'
# This module marks an Adapter as supporting parallel requests.
module Parallelism
attr_writer :supports_parallel
def supports_parallel?
@supports_parallel
end
def inherited(subclass)
super
subclass.supports_parallel = supports_parallel?
end
end
extend Parallelism
self.supports_parallel = false
def initialize(_app = nil, opts = {}, &block)
@app = ->(env) { env.response }
@connection_options = opts
@config_block = block
end
# Yields or returns an adapter's configured connection. Depends on
# #build_connection being defined on this adapter.
#
# @param env [Faraday::Env, Hash] The env object for a faraday request.
#
# @return The return value of the given block, or the HTTP connection object
# if no block is given.
def connection(env)
conn = build_connection(env)
return conn unless block_given?
yield conn
end
# Close any persistent connections. The adapter should still be usable
# after calling close.
def close
# Possible implementation:
# @app.close if @app.respond_to?(:close)
end
def call(env)
env.clear_body if env.needs_body?
env.response = Response.new
end
private
def save_response(env, status, body, headers = nil, reason_phrase = nil, finished: true)
env.status = status
env.body = body
env.reason_phrase = reason_phrase&.to_s&.strip
env.response_headers = Utils::Headers.new.tap do |response_headers|
response_headers.update headers unless headers.nil?
yield(response_headers) if block_given?
end
env.response.finish(env) unless env.parallel? || !finished
env.response
end
# Fetches either a read, write, or open timeout setting. Defaults to the
# :timeout value if a more specific one is not given.
#
# @param type [Symbol] Describes which timeout setting to get: :read,
# :write, or :open.
# @param options [Hash] Hash containing Symbol keys like :timeout,
# :read_timeout, :write_timeout, or :open_timeout
#
# @return [Integer, nil] Timeout duration in seconds, or nil if no timeout
# has been set.
def request_timeout(type, options)
key = TIMEOUT_KEYS.fetch(type) do
msg = "Expected :read, :write, :open. Got #{type.inspect} :("
raise ArgumentError, msg
end
options[key] || options[:timeout]
end
TIMEOUT_KEYS = {
read: :read_timeout,
open: :open_timeout,
write: :write_timeout
}.freeze
end
end
require 'faraday/adapter/test'
@@ -0,0 +1,311 @@
# frozen_string_literal: true
require 'timeout'
module Faraday
class Adapter
# @example
# test = Faraday::Connection.new do
# use Faraday::Adapter::Test do |stub|
# # Define matcher to match the request
# stub.get '/resource.json' do
# # return static content
# [200, {'Content-Type' => 'application/json'}, 'hi world']
# end
#
# # response with content generated based on request
# stub.get '/showget' do |env|
# [200, {'Content-Type' => 'text/plain'}, env[:method].to_s]
# end
#
# # A regular expression can be used as matching filter
# stub.get /\A\/items\/(\d+)\z/ do |env, meta|
# # in case regular expression is used, an instance of MatchData
# # can be received
# [200,
# {'Content-Type' => 'text/plain'},
# "showing item: #{meta[:match_data][1]}"
# ]
# end
#
# # Test the request body is the same as the stubbed body
# stub.post('/bar', 'name=YK&word=call') { [200, {}, ''] }
#
# # You can pass a proc as a stubbed body and check the request body in your way.
# # In this case, the proc should return true or false.
# stub.post('/foo', ->(request_body) do
# JSON.parse(request_body).slice('name') == { 'name' => 'YK' } }) { [200, {}, '']
# end
#
# # You can set strict_mode to exactly match the stubbed requests.
# stub.strict_mode = true
# end
# end
#
# resp = test.get '/resource.json'
# resp.body # => 'hi world'
#
# resp = test.get '/showget'
# resp.body # => 'get'
#
# resp = test.get '/items/1'
# resp.body # => 'showing item: 1'
#
# resp = test.get '/items/2'
# resp.body # => 'showing item: 2'
#
# resp = test.post '/bar', 'name=YK&word=call'
# resp.status # => 200
#
# resp = test.post '/foo', JSON.dump(name: 'YK', created_at: Time.now)
# resp.status # => 200
class Test < Faraday::Adapter
attr_accessor :stubs
# A stack of Stubs
class Stubs
class NotFound < StandardError
end
def initialize(strict_mode: false)
# { get: [Stub, Stub] }
@stack = {}
@consumed = {}
@strict_mode = strict_mode
@stubs_mutex = Monitor.new
yield(self) if block_given?
end
def empty?
@stack.empty?
end
# @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] ||= [])
@stubs_mutex.synchronize do
stub, meta = matches?(stack, env)
if stub
removed = stack.delete(stub)
consumed << removed unless removed.nil?
return stub, meta
end
end
matches?(consumed, env)
end
def get(path, headers = {}, &block)
new_stub(:get, path, headers, &block)
end
def head(path, headers = {}, &block)
new_stub(:head, path, headers, &block)
end
def post(path, body = nil, headers = {}, &block)
new_stub(:post, path, headers, body, &block)
end
def put(path, body = nil, headers = {}, &block)
new_stub(:put, path, headers, body, &block)
end
def patch(path, body = nil, headers = {}, &block)
new_stub(:patch, path, headers, body, &block)
end
def delete(path, headers = {}, &block)
new_stub(:delete, path, headers, &block)
end
def options(path, headers = {}, &block)
new_stub(:options, path, headers, &block)
end
# Raises an error if any of the stubbed calls have not been made.
def verify_stubbed_calls
failed_stubs = []
@stack.each do |method, stubs|
next if stubs.empty?
failed_stubs.concat(
stubs.map do |stub|
"Expected #{method} #{stub}."
end
)
end
raise failed_stubs.join(' ') unless failed_stubs.empty?
end
# Set strict_mode. If the value is true, this adapter tries to find matched requests strictly,
# which means that all of a path, parameters, and headers must be the same as an actual request.
def strict_mode=(value)
@strict_mode = value
@stack.each_value do |stubs|
stubs.each do |stub|
stub.strict_mode = value
end
end
end
protected
def new_stub(request_method, path, headers = {}, body = nil, &block)
normalized_path, host =
if path.is_a?(Regexp)
path
else
[
Faraday::Utils.normalize_path(path),
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, path, query, headers, body, @strict_mode, block)
(@stack[request_method] ||= []) << stub
end
# @param stack [Hash]
# @param env [Faraday::Env]
def matches?(stack, env)
stack.each do |stub|
match_result, meta = stub.matches?(env)
return stub, meta if match_result
end
nil
end
end
# Stub request
Stub = Struct.new(:host, :path, :query, :headers, :body, :strict_mode, :block) do
# @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]
# 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?(env) &&
body_match?(request_body) &&
headers_match?(request_headers), meta]
end
def path_match?(request_path, meta)
if path.is_a?(Regexp)
!!(meta[:match_data] = path.match(request_path))
else
path == request_path
end
end
# @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
params.keys.all? do |key|
request_params[key] == params[key]
end
end
def headers_match?(request_headers)
if strict_mode
headers_with_user_agent = headers.dup.tap do |hs|
# NOTE: Set User-Agent in case it's not set when creating Stubs.
# Users would not want to set Faraday's User-Agent explicitly.
hs[:user_agent] ||= Connection::USER_AGENT
end
return Set.new(headers_with_user_agent) == Set.new(request_headers)
end
headers.keys.all? do |key|
request_headers[key] == headers[key]
end
end
def body_match?(request_body)
return true if body.to_s.empty?
case body
when Proc
body.call(request_body)
else
request_body == body
end
end
def to_s
"#{path} #{body}"
end
end
def initialize(app, stubs = nil, &block)
super(app)
@stubs = stubs || Stubs.new
configure(&block) if block
end
def configure
yield(stubs)
end
# @param env [Faraday::Env]
def call(env)
super
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]} " \
"#{env[:url]} #{env[:body]} #{env[:headers]}"
end
block_arity = stub.block.arity
params = if block_arity >= 0
[env, meta].take(block_arity)
else
[env, meta]
end
timeout = request_timeout(:open, env[:request])
timeout ||= request_timeout(:read, env[:request])
status, headers, body =
if timeout
::Timeout.timeout(timeout, Faraday::TimeoutError) do
stub.block.call(*params)
end
else
stub.block.call(*params)
end
# We need to explicitly pass `reason_phrase = nil` here to avoid keyword args conflicts.
# See https://github.com/lostisland/faraday/issues/1444
# TODO: remove `nil` explicit reason_phrase once Ruby 3.0 becomes minimum req. version
save_response(env, status, body, headers, nil)
@app.call(env)
end
end
end
end
Faraday::Adapter.register_middleware(test: Faraday::Adapter::Test)
@@ -0,0 +1,30 @@
# frozen_string_literal: true
require 'monitor'
module Faraday
# AdapterRegistry registers adapter class names so they can be looked up by a
# String or Symbol name.
class AdapterRegistry
def initialize
@lock = Monitor.new
@constants = {}
end
def get(name)
klass = @lock.synchronize do
@constants[name]
end
return klass if klass
Object.const_get(name).tap { |c| set(c, name) }
end
def set(klass, name = nil)
name ||= klass.to_s
@lock.synchronize do
@constants[name] = klass
end
end
end
end
@@ -0,0 +1,555 @@
# frozen_string_literal: true
module Faraday
# Connection objects manage the default properties and the middleware
# stack for fulfilling an HTTP request.
#
# @example
#
# conn = Faraday::Connection.new 'http://httpbingo.org'
#
# # GET http://httpbingo.org/nigiri
# conn.get 'nigiri'
# # => #<Faraday::Response>
#
class Connection
# A Set of allowed HTTP verbs.
METHODS = Set.new %i[get post put delete head patch options trace]
USER_AGENT = "Faraday v#{VERSION}".freeze
# @return [Hash] URI query unencoded key/value pairs.
attr_reader :params
# @return [Hash] unencoded HTTP header key/value pairs.
attr_reader :headers
# @return [String] a URI with the prefix used for all requests from this
# Connection. This includes a default host name, scheme, port, and path.
attr_reader :url_prefix
# @return [Faraday::RackBuilder] Builder for this Connection.
attr_reader :builder
# @return [Hash] SSL options.
attr_reader :ssl
# @return [Object] the parallel manager for this Connection.
attr_reader :parallel_manager
# Sets the default parallel manager for this connection.
attr_writer :default_parallel_manager
# @return [Hash] proxy options.
attr_reader :proxy
# Initializes a new Faraday::Connection.
#
# @param url [URI, String] URI or String base URL to use as a prefix for all
# requests (optional).
# @param options [Hash, Faraday::ConnectionOptions]
# @option options [URI, String] :url ('http:/') URI or String base URL
# @option options [Hash<String => String>] :params URI query unencoded
# key/value pairs.
# @option options [Hash<String => String>] :headers Hash of unencoded HTTP
# header key/value pairs.
# @option options [Hash] :request Hash of request options.
# @option options [Hash] :ssl Hash of SSL options.
# @option options [Hash, URI, String] :proxy proxy options, either as a URL
# or as a Hash
# @option options [URI, String] :proxy[:uri]
# @option options [String] :proxy[:user]
# @option options [String] :proxy[:password]
# @yield [self] after all setup has been done
def initialize(url = nil, options = nil)
options = ConnectionOptions.from(options)
if url.is_a?(Hash) || url.is_a?(ConnectionOptions)
options = Utils.deep_merge(options, url)
url = options.url
end
@parallel_manager = nil
@headers = Utils::Headers.new
@params = Utils::ParamsHash.new
@options = options.request
@ssl = options.ssl
@default_parallel_manager = options.parallel_manager
@manual_proxy = nil
@builder = options.builder || begin
# pass an empty block to Builder so it doesn't assume default middleware
options.new_builder(block_given? ? proc { |b| } : nil)
end
self.url_prefix = url || 'http:/'
@params.update(options.params) if options.params
@headers.update(options.headers) if options.headers
initialize_proxy(url, options)
yield(self) if block_given?
@headers[:user_agent] ||= USER_AGENT
end
def initialize_proxy(url, options)
@manual_proxy = !!options.proxy
@proxy =
if options.proxy
ProxyOptions.from(options.proxy)
else
proxy_from_env(url)
end
end
# Sets the Hash of URI query unencoded key/value pairs.
# @param hash [Hash]
def params=(hash)
@params.replace hash
end
# Sets the Hash of unencoded HTTP header key/value pairs.
# @param hash [Hash]
def headers=(hash)
@headers.replace hash
end
extend Forwardable
def_delegators :builder, :use, :request, :response, :adapter, :app
# Closes the underlying resources and/or connections. In the case of
# persistent connections, this closes all currently open connections
# but does not prevent new connections from being made.
def close
app.close
end
# @!method get(url = nil, params = nil, headers = nil)
# Makes a GET HTTP request without a body.
# @!scope class
#
# @param url [String, URI, nil] The optional String base URL to use as a prefix for
# all requests. Can also be the options Hash.
# @param params [Hash, nil] Hash of URI query unencoded key/value pairs.
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
#
# @example
# conn.get '/items', { page: 1 }, :accept => 'application/json'
#
# # ElasticSearch example sending a body with GET.
# conn.get '/twitter/tweet/_search' do |req|
# req.headers[:content_type] = 'application/json'
# req.params[:routing] = 'kimchy'
# req.body = JSON.generate(query: {...})
# end
#
# @yield [Faraday::Request] for further request customizations
# @return [Faraday::Response]
# @!method head(url = nil, params = nil, headers = nil)
# Makes a HEAD HTTP request without a body.
# @!scope class
#
# @param url [String, URI, nil] The optional String base URL to use as a prefix for
# all requests. Can also be the options Hash.
# @param params [Hash, nil] Hash of URI query unencoded key/value pairs.
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
#
# @example
# conn.head '/items/1'
#
# @yield [Faraday::Request] for further request customizations
# @return [Faraday::Response]
# @!method delete(url = nil, params = nil, headers = nil)
# Makes a DELETE HTTP request without a body.
# @!scope class
#
# @param url [String, URI, nil] The optional String base URL to use as a prefix for
# all requests. Can also be the options Hash.
# @param params [Hash, nil] Hash of URI query unencoded key/value pairs.
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
#
# @example
# conn.delete '/items/1'
#
# @yield [Faraday::Request] for further request customizations
# @return [Faraday::Response]
# @!method trace(url = nil, params = nil, headers = nil)
# Makes a TRACE HTTP request without a body.
# @!scope class
#
# @param url [String, URI, nil] The optional String base URL to use as a prefix for
# all requests. Can also be the options Hash.
# @param params [Hash, nil] Hash of URI query unencoded key/value pairs.
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
#
# @example
# conn.connect '/items/1'
#
# @yield [Faraday::Request] for further request customizations
# @return [Faraday::Response]
# @!visibility private
METHODS_WITH_QUERY.each do |method|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{method}(url = nil, params = nil, headers = nil)
run_request(:#{method}, url, nil, headers) do |request|
request.params.update(params) if params
yield request if block_given?
end
end
RUBY
end
# @overload options()
# Returns current Connection options.
#
# @overload options(url, params = nil, headers = nil)
# Makes an OPTIONS HTTP request to the given URL.
# @param url [String, URI, nil] String base URL to sue as a prefix for all requests.
# @param params [Hash, nil] Hash of URI query unencoded key/value pairs.
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
#
# @example
# conn.options '/items/1'
#
# @yield [Faraday::Request] for further request customizations
# @return [Faraday::Response]
def options(*args)
return @options if args.empty?
url, params, headers = *args
run_request(:options, url, nil, headers) do |request|
request.params.update(params) if params
yield request if block_given?
end
end
# @!method post(url = nil, body = nil, headers = nil)
# Makes a POST HTTP request with a body.
# @!scope class
#
# @param url [String, URI, nil] The optional String base URL to use as a prefix for
# all requests. Can also be the options Hash.
# @param body [String, nil] body for the request.
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
#
# @example
# conn.post '/items', data, content_type: 'application/json'
#
# # Simple ElasticSearch indexing sample.
# conn.post '/twitter/tweet' do |req|
# req.headers[:content_type] = 'application/json'
# req.params[:routing] = 'kimchy'
# req.body = JSON.generate(user: 'kimchy', ...)
# end
#
# @yield [Faraday::Request] for further request customizations
# @return [Faraday::Response]
# @!method put(url = nil, body = nil, headers = nil)
# Makes a PUT HTTP request with a body.
# @!scope class
#
# @param url [String, URI, nil] The optional String base URL to use as a prefix for
# all requests. Can also be the options Hash.
# @param body [String, nil] body for the request.
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
#
# @example
# conn.put '/products/123', data, content_type: 'application/json'
#
# # Star a gist.
# conn.put 'https://api.github.com/gists/GIST_ID/star' do |req|
# req.headers['Accept'] = 'application/vnd.github+json'
# req.headers['Authorization'] = 'Bearer <YOUR-TOKEN>'
# req.headers['X-GitHub-Api-Version'] = '2022-11-28'
# end
#
# @yield [Faraday::Request] for further request customizations
# @return [Faraday::Response]
# @!visibility private
METHODS_WITH_BODY.each do |method|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{method}(url = nil, body = nil, headers = nil, &block)
run_request(:#{method}, url, body, headers, &block)
end
RUBY
end
# Check if the adapter is parallel-capable.
#
# @yield if the adapter isn't parallel-capable, or if no adapter is set yet.
#
# @return [Object, nil] a parallel manager or nil if yielded
# @api private
def default_parallel_manager
@default_parallel_manager ||= begin
adapter = @builder.adapter.klass if @builder.adapter
if support_parallel?(adapter)
adapter.setup_parallel_manager
elsif block_given?
yield
end
end
end
# Determine if this Faraday::Connection can make parallel requests.
#
# @return [Boolean]
def in_parallel?
!!@parallel_manager
end
# Sets up the parallel manager to make a set of requests.
#
# @param manager [Object] The parallel manager that this Connection's
# Adapter uses.
#
# @yield a block to execute multiple requests.
# @return [void]
def in_parallel(manager = nil)
@parallel_manager = manager || default_parallel_manager do
warn 'Warning: `in_parallel` called but no parallel-capable adapter ' \
'on Faraday stack'
warn caller[2, 10].join("\n")
nil
end
yield
@parallel_manager&.run
ensure
@parallel_manager = nil
end
# Sets the Hash proxy options.
#
# @param new_value [Object]
def proxy=(new_value)
@manual_proxy = true
@proxy = new_value ? ProxyOptions.from(new_value) : nil
end
def_delegators :url_prefix, :scheme, :scheme=, :host, :host=, :port, :port=
def_delegator :url_prefix, :path, :path_prefix
# Parses the given URL with URI and stores the individual
# components in this connection. These components serve as defaults for
# requests made by this connection.
#
# @param url [String, URI]
# @param encoder [Object]
#
# @example
#
# conn = Faraday::Connection.new { ... }
# conn.url_prefix = "https://httpbingo.org/api"
# conn.scheme # => https
# conn.path_prefix # => "/api"
#
# conn.get("nigiri?page=2") # accesses https://httpbingo.org/api/nigiri
def url_prefix=(url, encoder = nil)
uri = @url_prefix = Utils.URI(url)
self.path_prefix = uri.path
params.merge_query(uri.query, encoder)
uri.query = nil
with_uri_credentials(uri) do |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::Utils.basic_header_from(user, password)
headers[Faraday::Request::Authorization::KEY] = header
end
# Sets the path prefix and ensures that it always has a leading
# slash.
#
# @param value [String]
#
# @return [String] the new path prefix
def path_prefix=(value)
url_prefix.path = if value
value = "/#{value}" unless value[0, 1] == '/'
value
end
end
# Takes a relative url for a request and combines it with the defaults
# set on the connection instance.
#
# @param url [String, URI, nil]
# @param extra_params [Hash]
#
# @example
# conn = Faraday::Connection.new { ... }
# conn.url_prefix = "https://httpbingo.org/api?token=abc"
# conn.scheme # => https
# conn.path_prefix # => "/api"
#
# conn.build_url("nigiri?page=2")
# # => https://httpbingo.org/api/nigiri?token=abc&page=2
#
# conn.build_url("nigiri", page: 2)
# # => https://httpbingo.org/api/nigiri?token=abc&page=2
#
def build_url(url = nil, extra_params = nil)
uri = build_exclusive_url(url)
query_values = params.dup.merge_query(uri.query, options.params_encoder)
query_values.update(extra_params) if extra_params
uri.query =
if query_values.empty?
nil
else
query_values.to_query(options.params_encoder)
end
uri
end
# Builds and runs the Faraday::Request.
#
# @param method [Symbol] HTTP method.
# @param url [String, URI, nil] String or URI to access.
# @param body [String, nil] The request body that will eventually be converted to
# a string.
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
#
# @return [Faraday::Response]
def run_request(method, url, body, headers)
unless METHODS.include?(method)
raise ArgumentError, "unknown http method: #{method}"
end
request = build_request(method) do |req|
req.options.proxy = proxy_for_request(url)
req.url(url) if url
req.headers.update(headers) if headers
req.body = body if body
yield(req) if block_given?
end
builder.build_response(self, request)
end
# Creates and configures the request object.
#
# @param method [Symbol]
#
# @yield [Faraday::Request] if block given
# @return [Faraday::Request]
def build_request(method)
Request.create(method) do |req|
req.params = params.dup
req.headers = headers.dup
req.options = options.dup
yield(req) if block_given?
end
end
# Build an absolute URL based on url_prefix.
#
# @param url [String, URI, nil]
# @param params [Faraday::Utils::ParamsHash] A Faraday::Utils::ParamsHash to
# replace the query values
# of the resulting url (default: nil).
#
# @return [URI]
def build_exclusive_url(url = nil, params = nil, params_encoder = nil)
url = nil if url.respond_to?(:empty?) && url.empty?
base = url_prefix.dup
if url && !base.path.end_with?('/')
base.path = "#{base.path}/" # ensure trailing slash
end
url = url.to_s.gsub(':', '%3A') if URI.parse(url.to_s).opaque
uri = url ? base + url : base
if params
uri.query = params.to_query(params_encoder || options.params_encoder)
end
uri.query = nil if uri.query && uri.query.empty?
uri
end
# Creates a duplicate of this Faraday::Connection.
#
# @api private
#
# @return [Faraday::Connection]
def dup
self.class.new(build_exclusive_url,
headers: headers.dup,
params: params.dup,
builder: builder.dup,
ssl: ssl.dup,
request: options.dup)
end
# Yields username and password extracted from a URI if they both exist.
#
# @param uri [URI]
# @yield [username, password] any username and password
# @yieldparam username [String] any username from URI
# @yieldparam password [String] any password from URI
# @return [void]
# @api private
def with_uri_credentials(uri)
return unless uri.user && uri.password
yield(Utils.unescape(uri.user), Utils.unescape(uri.password))
end
def proxy_from_env(url)
return if Faraday.ignore_env_proxy
uri = nil
case url
when String
uri = Utils.URI(url)
uri = if uri.host.nil?
find_default_proxy
else
URI.parse("#{uri.scheme}://#{uri.host}").find_proxy
end
when URI
uri = url.find_proxy
when nil
uri = find_default_proxy
end
ProxyOptions.from(uri) if uri
end
def find_default_proxy
uri = ENV.fetch('http_proxy', nil)
return unless uri && !uri.empty?
uri = "http://#{uri}" unless uri.match?(/^http/i)
uri
end
def proxy_for_request(url)
return proxy if @manual_proxy
if url && Utils.URI(url).absolute?
proxy_from_env(url)
else
proxy
end
end
def support_parallel?(adapter)
adapter.respond_to?(:supports_parallel?) && adapter&.supports_parallel?
end
end
end
@@ -0,0 +1,105 @@
# frozen_string_literal: true
module Faraday
# FlatParamsEncoder manages URI params as a flat hash. Any Array values repeat
# the parameter multiple times.
module FlatParamsEncoder
class << self
extend Forwardable
def_delegators :'Faraday::Utils', :escape, :unescape
end
# Encode converts the given param into a URI querystring. Keys and values
# will converted to strings and appropriately escaped for the URI.
#
# @param params [Hash] query arguments to convert.
#
# @example
#
# encode({a: %w[one two three], b: true, c: "C"})
# # => 'a=one&a=two&a=three&b=true&c=C'
#
# @return [String] the URI querystring (without the leading '?')
def self.encode(params)
return nil if params.nil?
unless params.is_a?(Array)
unless params.respond_to?(:to_hash)
raise TypeError,
"Can't convert #{params.class} into Hash."
end
params = params.to_hash
params = params.map do |key, value|
key = key.to_s if key.is_a?(Symbol)
[key, value]
end
# Only to be used for non-Array inputs. Arrays should preserve order.
params.sort! if @sort_params
end
# The params have form [['key1', 'value1'], ['key2', 'value2']].
buffer = +''
params.each do |key, value|
encoded_key = escape(key)
if value.nil?
buffer << "#{encoded_key}&"
elsif value.is_a?(Array)
if value.empty?
buffer << "#{encoded_key}=&"
else
value.each do |sub_value|
encoded_value = escape(sub_value)
buffer << "#{encoded_key}=#{encoded_value}&"
end
end
else
encoded_value = escape(value)
buffer << "#{encoded_key}=#{encoded_value}&"
end
end
buffer.chop
end
# Decode converts the given URI querystring into a hash.
#
# @param query [String] query arguments to parse.
#
# @example
#
# decode('a=one&a=two&a=three&b=true&c=C')
# # => {"a"=>["one", "two", "three"], "b"=>"true", "c"=>"C"}
#
# @return [Hash] parsed keys and value strings from the querystring.
def self.decode(query)
return nil if query.nil?
empty_accumulator = {}
split_query = (query.split('&').map do |pair|
pair.split('=', 2) if pair && !pair.empty?
end).compact
split_query.each_with_object(empty_accumulator.dup) do |pair, accu|
pair[0] = unescape(pair[0])
pair[1] = true if pair[1].nil?
if pair[1].respond_to?(:to_str)
pair[1] = unescape(pair[1].to_str.tr('+', ' '))
end
if accu[pair[0]].is_a?(Array)
accu[pair[0]] << pair[1]
elsif accu[pair[0]]
accu[pair[0]] = [accu[pair[0]], pair[1]]
else
accu[pair[0]] = pair[1]
end
end
end
class << self
attr_accessor :sort_params
end
# Useful default for OAuth and caching.
@sort_params = true
end
end
@@ -0,0 +1,183 @@
# frozen_string_literal: true
module Faraday
# Sub-module for encoding parameters into query-string.
module EncodeMethods
# @param params [nil, Array, #to_hash] parameters to be encoded
#
# @return [String] the encoded params
#
# @raise [TypeError] if params can not be converted to a Hash
def encode(params)
return nil if params.nil?
unless params.is_a?(Array)
unless params.respond_to?(:to_hash)
raise TypeError, "Can't convert #{params.class} into Hash."
end
params = params.to_hash
params = params.map do |key, value|
key = key.to_s if key.is_a?(Symbol)
[key, value]
end
# Only to be used for non-Array inputs. Arrays should preserve order.
params.sort! if @sort_params
end
# The params have form [['key1', 'value1'], ['key2', 'value2']].
buffer = +''
params.each do |parent, value|
encoded_parent = escape(parent)
buffer << "#{encode_pair(encoded_parent, value)}&"
end
buffer.chop
end
protected
def encode_pair(parent, value)
if value.is_a?(Hash)
encode_hash(parent, value)
elsif value.is_a?(Array)
encode_array(parent, value)
elsif value.nil?
parent
else
encoded_value = escape(value)
"#{parent}=#{encoded_value}"
end
end
def encode_hash(parent, value)
value = value.map { |key, val| [escape(key), val] }.sort
buffer = +''
value.each do |key, val|
new_parent = "#{parent}%5B#{key}%5D"
buffer << "#{encode_pair(new_parent, val)}&"
end
buffer.chop
end
def encode_array(parent, value)
return "#{parent}%5B%5D" if value.empty?
buffer = +''
value.each_with_index do |val, index|
new_parent = if @array_indices
"#{parent}%5B#{index}%5D"
else
"#{parent}%5B%5D"
end
buffer << "#{encode_pair(new_parent, val)}&"
end
buffer.chop
end
end
# Sub-module for decoding query-string into parameters.
module DecodeMethods
# @param query [nil, String]
#
# @return [Array<Array, String>] the decoded params
#
# @raise [TypeError] if the nesting is incorrect
def decode(query)
return nil if query.nil?
params = {}
query.split('&').each do |pair|
next if pair.empty?
key, value = pair.split('=', 2)
key = unescape(key)
value = unescape(value.tr('+', ' ')) if value
decode_pair(key, value, params)
end
dehash(params, 0)
end
protected
SUBKEYS_REGEX = /[^\[\]]+(?:\]?\[\])?/
def decode_pair(key, value, context)
subkeys = key.scan(SUBKEYS_REGEX)
subkeys.each_with_index do |subkey, i|
is_array = subkey =~ /[\[\]]+\Z/
subkey = Regexp.last_match.pre_match if is_array
last_subkey = i == subkeys.length - 1
context = prepare_context(context, subkey, is_array, last_subkey)
add_to_context(is_array, context, value, subkey) if last_subkey
end
end
def prepare_context(context, subkey, is_array, last_subkey)
if !last_subkey || is_array
context = new_context(subkey, is_array, context)
end
if context.is_a?(Array) && !is_array
context = match_context(context, subkey)
end
context
end
def new_context(subkey, is_array, context)
value_type = is_array ? Array : Hash
if context[subkey] && !context[subkey].is_a?(value_type)
raise TypeError, "expected #{value_type.name} " \
"(got #{context[subkey].class.name}) for param `#{subkey}'"
end
context[subkey] ||= value_type.new
end
def match_context(context, subkey)
context << {} if !context.last.is_a?(Hash) || context.last.key?(subkey)
context.last
end
def add_to_context(is_array, context, value, subkey)
is_array ? context << value : context[subkey] = value
end
# Internal: convert a nested hash with purely numeric keys into an array.
# FIXME: this is not compatible with Rack::Utils.parse_nested_query
# @!visibility private
def dehash(hash, depth)
hash.each do |key, value|
hash[key] = dehash(value, depth + 1) if value.is_a?(Hash)
end
if depth.positive? && !hash.empty? && hash.keys.all? { |k| k =~ /^\d+$/ }
hash.sort.map(&:last)
else
hash
end
end
end
# This is the default encoder for Faraday requests.
# Using this encoder, parameters will be encoded respecting their structure,
# so you can send objects such as Arrays or Hashes as parameters
# for your requests.
module NestedParamsEncoder
class << self
attr_accessor :sort_params, :array_indices
extend Forwardable
def_delegators :'Faraday::Utils', :escape, :unescape
end
# Useful default for OAuth and caching.
@sort_params = true
@array_indices = false
extend EncodeMethods
extend DecodeMethods
end
end
+161
View File
@@ -0,0 +1,161 @@
# frozen_string_literal: true
# Faraday namespace.
module Faraday
# Faraday error base class.
class Error < StandardError
attr_reader :response, :wrapped_exception
def initialize(exc = nil, response = nil)
@wrapped_exception = nil unless defined?(@wrapped_exception)
@response = nil unless defined?(@response)
super(exc_msg_and_response!(exc, response))
end
def backtrace
if @wrapped_exception
@wrapped_exception.backtrace
else
super
end
end
def inspect
inner = +''
inner << " wrapped=#{@wrapped_exception.inspect}" if @wrapped_exception
inner << " response=#{@response.inspect}" if @response
inner << " #{super}" if inner.empty?
%(#<#{self.class}#{inner}>)
end
def response_status
return unless @response
@response.is_a?(Faraday::Response) ? @response.status : @response[:status]
end
def response_headers
return unless @response
@response.is_a?(Faraday::Response) ? @response.headers : @response[:headers]
end
def response_body
return unless @response
@response.is_a?(Faraday::Response) ? @response.body : @response[:body]
end
protected
# Pulls out potential parent exception and response hash, storing them in
# instance variables.
# exc - Either an Exception, a string message, or a response hash.
# response - Hash
# :status - Optional integer HTTP response status
# :headers - String key/value hash of HTTP response header
# values.
# :body - Optional string HTTP response body.
# :request - Hash
# :method - Symbol with the request HTTP method.
# :url - URI object with the url requested.
# :url_path - String with the url path requested.
# :params - String key/value hash of query params
# present in the request.
# :headers - String key/value hash of HTTP request
# header values.
# :body - String HTTP request body.
#
# If a subclass has to call this, then it should pass a string message
# to `super`. See NilStatusError.
def exc_msg_and_response!(exc, response = nil)
if @response.nil? && @wrapped_exception.nil?
@wrapped_exception, msg, @response = exc_msg_and_response(exc, response)
return msg
end
exc.to_s
end
# Pulls out potential parent exception and response hash.
def exc_msg_and_response(exc, response = nil)
return [exc, exc.message, response] if exc.respond_to?(:backtrace)
return [nil, "the server responded with status #{exc[:status]}", exc] \
if exc.respond_to?(:each_key)
[nil, exc.to_s, response]
end
end
# Faraday client error class. Represents 4xx status responses.
class ClientError < Error
end
# Raised by Faraday::Response::RaiseError in case of a 400 response.
class BadRequestError < ClientError
end
# Raised by Faraday::Response::RaiseError in case of a 401 response.
class UnauthorizedError < ClientError
end
# Raised by Faraday::Response::RaiseError in case of a 403 response.
class ForbiddenError < ClientError
end
# Raised by Faraday::Response::RaiseError in case of a 404 response.
class ResourceNotFound < ClientError
end
# Raised by Faraday::Response::RaiseError in case of a 407 response.
class ProxyAuthError < ClientError
end
# Raised by Faraday::Response::RaiseError in case of a 408 response.
class RequestTimeoutError < ClientError
end
# Raised by Faraday::Response::RaiseError in case of a 409 response.
class ConflictError < ClientError
end
# Raised by Faraday::Response::RaiseError in case of a 422 response.
class UnprocessableEntityError < ClientError
end
# Raised by Faraday::Response::RaiseError in case of a 429 response.
class TooManyRequestsError < ClientError
end
# Faraday server error class. Represents 5xx status responses.
class ServerError < Error
end
# A unified client error for timeouts.
class TimeoutError < ServerError
def initialize(exc = 'timeout', response = nil)
super(exc, response)
end
end
# Raised by Faraday::Response::RaiseError in case of a nil status in response.
class NilStatusError < ServerError
def initialize(exc, response = nil)
exc_msg_and_response!(exc, response)
super('http status could not be derived from the server response')
end
end
# A unified error for failed connections.
class ConnectionFailed < Error
end
# A unified client error for SSL errors.
class SSLError < Error
end
# Raised by middlewares that parse the response, like the JSON response middleware.
class ParsingError < Error
end
end
@@ -0,0 +1,118 @@
# frozen_string_literal: true
require 'pp' # This require is necessary for Hash#pretty_inspect to work, do not remove it, people rely on it.
module Faraday
module Logging
# Serves as an integration point to customize logging
class Formatter
extend Forwardable
DEFAULT_OPTIONS = { headers: true, bodies: false, errors: false,
log_level: :info }.freeze
def initialize(logger:, options:)
@logger = logger
@options = DEFAULT_OPTIONS.merge(options)
unless %i[debug info warn error fatal].include?(@options[:log_level])
@options[:log_level] = :info
end
@filter = []
end
def_delegators :@logger, :debug, :info, :warn, :error, :fatal
def request(env)
public_send(log_level, 'request') do
"#{env.method.upcase} #{apply_filters(env.url.to_s)}"
end
log_headers('request', env.request_headers) if log_headers?(:request)
log_body('request', env[:body]) if env[:body] && log_body?(:request)
end
def response(env)
public_send(log_level, 'response') { "Status #{env.status}" }
log_headers('response', env.response_headers) if log_headers?(:response)
log_body('response', env[:body]) if env[:body] && log_body?(:response)
end
def exception(exc)
return unless log_errors?
public_send(log_level, 'error') { exc.full_message }
log_headers('error', exc.response_headers) if exc.respond_to?(:response_headers) && log_headers?(:error)
return unless exc.respond_to?(:response_body) && exc.response_body && log_body?(:error)
log_body('error', exc.response_body)
end
def filter(filter_word, filter_replacement)
@filter.push([filter_word, filter_replacement])
end
private
def dump_headers(headers)
return if headers.nil?
headers.map { |k, v| "#{k}: #{v.inspect}" }.join("\n")
end
def dump_body(body)
if body.respond_to?(:to_str)
body.to_str
else
pretty_inspect(body)
end
end
def pretty_inspect(body)
body.pretty_inspect
end
def log_headers?(type)
case @options[:headers]
when Hash
@options[:headers][type]
else
@options[:headers]
end
end
def log_body?(type)
case @options[:bodies]
when Hash
@options[:bodies][type]
else
@options[:bodies]
end
end
def log_errors?
@options[:errors]
end
def apply_filters(output)
@filter.each do |pattern, replacement|
output = output.to_s.gsub(pattern, replacement)
end
output
end
def log_level
@options[:log_level]
end
def log_headers(type, headers)
public_send(log_level, type) { apply_filters(dump_headers(headers)) }
end
def log_body(type, body)
public_send(log_level, type) { apply_filters(dump_body(body)) }
end
end
end
end
@@ -0,0 +1,6 @@
# frozen_string_literal: true
module Faraday
METHODS_WITH_QUERY = %w[get head delete trace].freeze
METHODS_WITH_BODY = %w[post put patch].freeze
end
@@ -0,0 +1,33 @@
# frozen_string_literal: true
module Faraday
# Middleware is the basic base class of any Faraday middleware.
class Middleware
extend MiddlewareRegistry
attr_reader :app, :options
def initialize(app = nil, options = {})
@app = app
@options = options
end
def call(env)
on_request(env) if respond_to?(:on_request)
app.call(env).on_complete do |environment|
on_complete(environment) if respond_to?(:on_complete)
end
rescue StandardError => e
on_error(e) if respond_to?(:on_error)
raise
end
def close
if app.respond_to?(:close)
app.close
else
warn "#{app} does not implement \#close!"
end
end
end
end
@@ -0,0 +1,83 @@
# frozen_string_literal: true
require 'monitor'
module Faraday
# Adds the ability for other modules to register and lookup
# middleware classes.
module MiddlewareRegistry
def registered_middleware
@registered_middleware ||= {}
end
# Register middleware class(es) on the current module.
#
# @param mappings [Hash] Middleware mappings from a lookup symbol to a middleware class.
# @return [void]
#
# @example Lookup by a constant
#
# module Faraday
# class Whatever < Middleware
# # Middleware looked up by :foo returns Faraday::Whatever::Foo.
# register_middleware(foo: Whatever)
# end
# end
def register_middleware(**mappings)
middleware_mutex do
registered_middleware.update(mappings)
end
end
# Unregister a previously registered middleware class.
#
# @param key [Symbol] key for the registered middleware.
def unregister_middleware(key)
registered_middleware.delete(key)
end
# Lookup middleware class with a registered Symbol shortcut.
#
# @param key [Symbol] key for the registered middleware.
# @return [Class] a middleware Class.
# @raise [Faraday::Error] if given key is not registered
#
# @example
#
# module Faraday
# class Whatever < Middleware
# register_middleware(foo: Whatever)
# end
# end
#
# Faraday::Middleware.lookup_middleware(:foo)
# # => Faraday::Whatever
def lookup_middleware(key)
load_middleware(key) ||
raise(Faraday::Error, "#{key.inspect} is not registered on #{self}")
end
private
def middleware_mutex(&block)
@middleware_mutex ||= Monitor.new
@middleware_mutex.synchronize(&block)
end
def load_middleware(key)
value = registered_middleware[key]
case value
when Module
value
when Symbol, String
middleware_mutex do
@registered_middleware[key] = const_get(value)
end
when Proc
middleware_mutex do
@registered_middleware[key] = value.call
end
end
end
end
end
@@ -0,0 +1,219 @@
# frozen_string_literal: true
module Faraday
# Subclasses Struct with some special helpers for converting from a Hash to
# a Struct.
class Options < Struct
# Public
def self.from(value)
value ? new.update(value) : new
end
# Public
def each
return to_enum(:each) unless block_given?
members.each do |key|
yield(key.to_sym, send(key))
end
end
# Public
def update(obj)
obj.each do |key, value|
sub_options = self.class.options_for(key)
if sub_options
new_value = sub_options.from(value) if value
elsif value.is_a?(Hash)
new_value = value.dup
else
new_value = value
end
send(:"#{key}=", new_value) unless new_value.nil?
end
self
end
# Public
def delete(key)
value = send(key)
send(:"#{key}=", nil)
value
end
# Public
def clear
members.each { |member| delete(member) }
end
# Public
def merge!(other)
other.each do |key, other_value|
self_value = send(key)
sub_options = self.class.options_for(key)
new_value = if self_value && sub_options && other_value
self_value.merge(other_value)
else
other_value
end
send(:"#{key}=", new_value) unless new_value.nil?
end
self
end
# Public
def merge(other)
dup.merge!(other)
end
# Public
def deep_dup
self.class.from(self)
end
# Public
def fetch(key, *args)
unless symbolized_key_set.include?(key.to_sym)
key_setter = "#{key}="
if !args.empty?
send(key_setter, args.first)
elsif block_given?
send(key_setter, yield(key))
else
raise self.class.fetch_error_class, "key not found: #{key.inspect}"
end
end
send(key)
end
# Public
def values_at(*keys)
keys.map { |key| send(key) }
end
# Public
def keys
members.reject { |member| send(member).nil? }
end
# Public
def empty?
keys.empty?
end
# Public
def each_key(&block)
return to_enum(:each_key) unless block
keys.each(&block)
end
# Public
def key?(key)
keys.include?(key)
end
alias has_key? key?
# Public
def each_value(&block)
return to_enum(:each_value) unless block
values.each(&block)
end
# Public
def value?(value)
values.include?(value)
end
alias has_value? value?
# Public
def to_hash
hash = {}
members.each do |key|
value = send(key)
hash[key.to_sym] = value unless value.nil?
end
hash
end
# Internal
def inspect
values = []
members.each do |member|
value = send(member)
values << "#{member}=#{value.inspect}" if value
end
values = values.empty? ? '(empty)' : values.join(', ')
%(#<#{self.class} #{values}>)
end
# Internal
def self.options(mapping)
attribute_options.update(mapping)
end
# Internal
def self.options_for(key)
attribute_options[key]
end
# Internal
def self.attribute_options
@attribute_options ||= {}
end
def self.memoized(key, &block)
unless block
raise ArgumentError, '#memoized must be called with a block'
end
memoized_attributes[key.to_sym] = block
class_eval <<-RUBY, __FILE__, __LINE__ + 1
remove_method(key) if method_defined?(key, false)
def #{key}() self[:#{key}]; end
RUBY
end
def self.memoized_attributes
@memoized_attributes ||= {}
end
def [](key)
key = key.to_sym
if (method = self.class.memoized_attributes[key])
super(key) || (self[key] = instance_eval(&method))
else
super
end
end
def symbolized_key_set
@symbolized_key_set ||= Set.new(keys.map(&:to_sym))
end
def self.inherited(subclass)
super
subclass.attribute_options.update(attribute_options)
subclass.memoized_attributes.update(memoized_attributes)
end
def self.fetch_error_class
@fetch_error_class ||= if Object.const_defined?(:KeyError)
::KeyError
else
::IndexError
end
end
end
end
require 'faraday/options/request_options'
require 'faraday/options/ssl_options'
require 'faraday/options/proxy_options'
require 'faraday/options/connection_options'
require 'faraday/options/env'
@@ -0,0 +1,23 @@
# frozen_string_literal: true
module Faraday
# @!parse
# # ConnectionOptions contains the configurable properties for a Faraday
# # connection object.
# class ConnectionOptions < Options; end
ConnectionOptions = Options.new(:request, :proxy, :ssl, :builder, :url,
:parallel_manager, :params, :headers,
:builder_class) do
options request: RequestOptions, ssl: SSLOptions
memoized(:request) { self.class.options_for(:request).new }
memoized(:ssl) { self.class.options_for(:ssl).new }
memoized(:builder_class) { RackBuilder }
def new_builder(block)
builder_class.new(&block)
end
end
end
@@ -0,0 +1,204 @@
# frozen_string_literal: true
module Faraday
# @!parse
# # @!attribute method
# # @return [Symbol] HTTP method (`:get`, `:post`)
# #
# # @!attribute body
# # @return [String] The request body that will eventually be converted to a
# # string.
# #
# # @!attribute url
# # @return [URI] URI instance for the current request.
# #
# # @!attribute request
# # @return [Hash] options for configuring the request.
# # Options for configuring the request.
# #
# # - `:timeout` - time limit for the entire request (Integer in
# # seconds)
# # - `:open_timeout` - time limit for just the connection phase (e.g.
# # handshake) (Integer in seconds)
# # - `:read_timeout` - time limit for the first response byte received from
# # the server (Integer in seconds)
# # - `:write_timeout` - time limit for the client to send the request to the
# # server (Integer in seconds)
# # - `:on_data` - Proc for streaming
# # - `:proxy` - Hash of proxy options
# # - `:uri` - Proxy server URI
# # - `:user` - Proxy server username
# # - `:password` - Proxy server password
# #
# # @!attribute request_headers
# # @return [Hash] HTTP Headers to be sent to the server.
# #
# # @!attribute ssl
# # @return [Hash] options for configuring SSL requests
# #
# # @!attribute parallel_manager
# # @return [Object] sent if the connection is in parallel mode
# #
# # @!attribute params
# # @return [Hash]
# #
# # @!attribute response
# # @return [Response]
# #
# # @!attribute response_headers
# # @return [Hash] HTTP headers from the server
# #
# # @!attribute status
# # @return [Integer] HTTP response status code
# #
# # @!attribute reason_phrase
# # @return [String]
# class Env < Options; end
Env = Options.new(:method, :request_body, :url, :request,
:request_headers, :ssl, :parallel_manager, :params,
:response, :response_headers, :status,
:reason_phrase, :response_body) do
const_set(:ContentLength, 'Content-Length')
const_set(:StatusesWithoutBody, Set.new([204, 304]))
const_set(:SuccessfulStatuses, (200..299))
# A Set of HTTP verbs that typically send a body. If no body is set for
# these requests, the Content-Length header is set to 0.
const_set(:MethodsWithBodies, Set.new(Faraday::METHODS_WITH_BODY.map(&:to_sym)))
options request: RequestOptions,
request_headers: Utils::Headers, response_headers: Utils::Headers
extend Forwardable
def_delegators :request, :params_encoder
# Build a new Env from given value. Respects and updates `custom_members`.
#
# @param value [Object] a value fitting Option.from(v).
# @return [Env] from given value
def self.from(value)
env = super(value)
if value.respond_to?(:custom_members)
env.custom_members.update(value.custom_members)
end
env
end
# @param key [Object]
def [](key)
return self[current_body] if key == :body
if in_member_set?(key)
super(key)
else
custom_members[key]
end
end
# @param key [Object]
# @param value [Object]
def []=(key, value)
if key == :body
super(current_body, value)
return
end
if in_member_set?(key)
super(key, value)
else
custom_members[key] = value
end
end
def current_body
!!status ? :response_body : :request_body
end
def body
self[:body]
end
def body=(value)
self[:body] = value
end
# @return [Boolean] true if status is in the set of {SuccessfulStatuses}.
def success?
Env::SuccessfulStatuses.include?(status)
end
# @return [Boolean] true if there's no body yet, and the method is in the
# set of {Env::MethodsWithBodies}.
def needs_body?
!body && Env::MethodsWithBodies.include?(method)
end
# Sets content length to zero and the body to the empty string.
def clear_body
request_headers[Env::ContentLength] = '0'
self.body = +''
end
# @return [Boolean] true if the status isn't in the set of
# {Env::StatusesWithoutBody}.
def parse_body?
!Env::StatusesWithoutBody.include?(status)
end
# @return [Boolean] true if there is a parallel_manager
def parallel?
!!parallel_manager
end
def inspect
attrs = [nil]
members.each do |mem|
if (value = send(mem))
attrs << "@#{mem}=#{value.inspect}"
end
end
attrs << "@custom=#{custom_members.inspect}" unless custom_members.empty?
%(#<#{self.class}#{attrs.join(' ')}>)
end
def stream_response?
request.stream_response?
end
def stream_response(&block)
size = 0
yielded = false
block_result = block.call do |chunk| # rubocop:disable Performance/RedundantBlockCall
if chunk.bytesize.positive? || size.positive?
yielded = true
size += chunk.bytesize
request.on_data.call(chunk, size, self)
end
end
request.on_data.call(+'', 0, self) unless yielded
block_result
end
# @private
def custom_members
@custom_members ||= {}
end
# @private
if members.first.is_a?(Symbol)
def in_member_set?(key)
self.class.member_set.include?(key.to_sym)
end
else
def in_member_set?(key)
self.class.member_set.include?(key.to_s)
end
end
# @private
def self.member_set
@member_set ||= Set.new(members)
end
end
end
@@ -0,0 +1,36 @@
# frozen_string_literal: true
module Faraday
# @!parse
# # ProxyOptions contains the configurable properties for the proxy
# # configuration used when making an HTTP request.
# class ProxyOptions < Options; end
ProxyOptions = Options.new(:uri, :user, :password) do
extend Forwardable
def_delegators :uri, :scheme, :scheme=, :host, :host=, :port, :port=,
:path, :path=
def self.from(value)
case value
when ''
value = nil
when String
# URIs without a scheme should default to http (like 'example:123').
# This fixes #1282 and prevents a silent failure in some adapters.
value = "http://#{value}" unless value.include?('://')
value = { uri: Utils.URI(value) }
when URI
value = { uri: value }
when Hash, Options
if (uri = value.delete(:uri))
value[:uri] = Utils.URI(uri)
end
end
super(value)
end
memoized(:user) { uri&.user && Utils.unescape(uri.user) }
memoized(:password) { uri&.password && Utils.unescape(uri.password) }
end
end
@@ -0,0 +1,23 @@
# frozen_string_literal: true
module Faraday
# @!parse
# # RequestOptions contains the configurable properties for a Faraday request.
# class RequestOptions < Options; end
RequestOptions = Options.new(:params_encoder, :proxy, :bind,
:timeout, :open_timeout, :read_timeout,
:write_timeout, :boundary, :oauth,
:context, :on_data) do
def []=(key, value)
if key && key.to_sym == :proxy
super(key, value ? ProxyOptions.from(value) : nil)
else
super(key, value)
end
end
def stream_response?
on_data.is_a?(Proc)
end
end
end
@@ -0,0 +1,70 @@
# frozen_string_literal: true
module Faraday
# @!parse
# # SSL-related options.
# #
# # @!attribute verify
# # @return [Boolean] whether to verify SSL certificates or not
# #
# # @!attribute verify_hostname
# # @return [Boolean] whether to enable hostname verification on server certificates
# # during the handshake or not (see https://github.com/ruby/openssl/pull/60)
# #
# # @!attribute ca_file
# # @return [String] CA file
# #
# # @!attribute ca_path
# # @return [String] CA path
# #
# # @!attribute verify_mode
# # @return [Integer] Any `OpenSSL::SSL::` constant (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL.html)
# #
# # @!attribute cert_store
# # @return [OpenSSL::X509::Store] certificate store
# #
# # @!attribute client_cert
# # @return [String, OpenSSL::X509::Certificate] client certificate
# #
# # @!attribute client_key
# # @return [String, OpenSSL::PKey::RSA, OpenSSL::PKey::DSA] client key
# #
# # @!attribute certificate
# # @return [OpenSSL::X509::Certificate] certificate (Excon only)
# #
# # @!attribute private_key
# # @return [OpenSSL::PKey::RSA, OpenSSL::PKey::DSA] private key (Excon only)
# #
# # @!attribute verify_depth
# # @return [Integer] maximum depth for the certificate chain verification
# #
# # @!attribute version
# # @return [String, Symbol] SSL version (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html#method-i-ssl_version-3D)
# #
# # @!attribute min_version
# # @return [String, Symbol] minimum SSL version (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html#method-i-min_version-3D)
# #
# # @!attribute max_version
# # @return [String, Symbol] maximum SSL version (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html#method-i-max_version-3D)
# class SSLOptions < Options; end
SSLOptions = Options.new(:verify, :verify_hostname,
:ca_file, :ca_path, :verify_mode,
:cert_store, :client_cert, :client_key,
:certificate, :private_key, :verify_depth,
:version, :min_version, :max_version) do
# @return [Boolean] true if should verify
def verify?
verify != false
end
# @return [Boolean] true if should not verify
def disable?
!verify?
end
# @return [Boolean] true if should verify_hostname
def verify_hostname?
verify_hostname != false
end
end
end
@@ -0,0 +1,5 @@
# frozen_string_literal: true
require 'forwardable'
require 'faraday/encoders/nested_params_encoder'
require 'faraday/encoders/flat_params_encoder'
@@ -0,0 +1,251 @@
# frozen_string_literal: true
require 'faraday/adapter_registry'
module Faraday
# A Builder that processes requests into responses by passing through an inner
# middleware stack (heavily inspired by Rack).
#
# @example
# Faraday::Connection.new(url: 'http://httpbingo.org') do |builder|
# builder.request :url_encoded # Faraday::Request::UrlEncoded
# builder.adapter :net_http # Faraday::Adapter::NetHttp
# end
class RackBuilder
# Used to detect missing arguments
NO_ARGUMENT = Object.new
attr_accessor :handlers
# Error raised when trying to modify the stack after calling `lock!`
class StackLocked < RuntimeError; end
# borrowed from ActiveSupport::Dependencies::Reference &
# ActionDispatch::MiddlewareStack::Middleware
class Handler
REGISTRY = Faraday::AdapterRegistry.new
attr_reader :name
ruby2_keywords def initialize(klass, *args, &block)
@name = klass.to_s
REGISTRY.set(klass) if klass.respond_to?(:name)
@args = args
@block = block
end
def klass
REGISTRY.get(@name)
end
def inspect
@name
end
def ==(other)
if other.is_a? Handler
name == other.name
elsif other.respond_to? :name
klass == other
else
@name == other.to_s
end
end
def build(app = nil)
klass.new(app, *@args, &@block)
end
end
def initialize(&block)
@adapter = nil
@handlers = []
build(&block)
end
def initialize_dup(original)
super
@adapter = original.adapter
@handlers = original.handlers.dup
end
def build
raise_if_locked
block_given? ? yield(self) : request(:url_encoded)
adapter(Faraday.default_adapter, **Faraday.default_adapter_options) unless @adapter
end
def [](idx)
@handlers[idx]
end
# Locks the middleware stack to ensure no further modifications are made.
def lock!
@handlers.freeze
end
def locked?
@handlers.frozen?
end
ruby2_keywords def use(klass, *args, &block)
if klass.is_a? Symbol
use_symbol(Faraday::Middleware, klass, *args, &block)
else
raise_if_locked
raise_if_adapter(klass)
@handlers << self.class::Handler.new(klass, *args, &block)
end
end
ruby2_keywords def request(key, *args, &block)
use_symbol(Faraday::Request, key, *args, &block)
end
ruby2_keywords def response(key, *args, &block)
use_symbol(Faraday::Response, key, *args, &block)
end
ruby2_keywords def adapter(klass = NO_ARGUMENT, *args, &block)
return @adapter if klass == NO_ARGUMENT || klass.nil?
klass = Faraday::Adapter.lookup_middleware(klass) if klass.is_a?(Symbol)
@adapter = self.class::Handler.new(klass, *args, &block)
end
## methods to push onto the various positions in the stack:
ruby2_keywords def insert(index, *args, &block)
raise_if_locked
index = assert_index(index)
handler = self.class::Handler.new(*args, &block)
@handlers.insert(index, handler)
end
alias insert_before insert
ruby2_keywords def insert_after(index, *args, &block)
index = assert_index(index)
insert(index + 1, *args, &block)
end
ruby2_keywords def swap(index, *args, &block)
raise_if_locked
index = assert_index(index)
@handlers.delete_at(index)
insert(index, *args, &block)
end
def delete(handler)
raise_if_locked
@handlers.delete(handler)
end
# Processes a Request into a Response by passing it through this Builder's
# middleware stack.
#
# @param connection [Faraday::Connection]
# @param request [Faraday::Request]
#
# @return [Faraday::Response]
def build_response(connection, request)
app.call(build_env(connection, request))
end
# The "rack app" wrapped in middleware. All requests are sent here.
#
# The builder is responsible for creating the app object. After this,
# the builder gets locked to ensure no further modifications are made
# to the middleware stack.
#
# Returns an object that responds to `call` and returns a Response.
def app
@app ||= begin
lock!
ensure_adapter!
to_app
end
end
def to_app
# last added handler is the deepest and thus closest to the inner app
# adapter is always the last one
@handlers.reverse.inject(@adapter.build) do |app, handler|
handler.build(app)
end
end
def ==(other)
other.is_a?(self.class) &&
@handlers == other.handlers &&
@adapter == other.adapter
end
# ENV Keys
# :http_method - a symbolized request HTTP method (:get, :post)
# :body - the request body that will eventually be converted to a string.
# :url - URI instance for the current request.
# :status - HTTP response status code
# :request_headers - hash of HTTP Headers to be sent to the server
# :response_headers - Hash of HTTP headers from the server
# :parallel_manager - sent if the connection is in parallel mode
# :request - Hash of options for configuring the request.
# :timeout - open/read timeout Integer in seconds
# :open_timeout - read timeout Integer in seconds
# :proxy - Hash of proxy options
# :uri - Proxy Server URI
# :user - Proxy server username
# :password - Proxy server password
# :ssl - Hash of options for configuring SSL requests.
def build_env(connection, request)
exclusive_url = connection.build_exclusive_url(
request.path, request.params,
request.options.params_encoder
)
Env.new(request.http_method, request.body, exclusive_url,
request.options, request.headers, connection.ssl,
connection.parallel_manager)
end
private
LOCK_ERR = "can't modify middleware stack after making a request"
MISSING_ADAPTER_ERROR = "An attempt to run a request with a Faraday::Connection without adapter has been made.\n" \
"Please set Faraday.default_adapter or provide one when initializing the connection.\n" \
'For more info, check https://lostisland.github.io/faraday/usage/.'
def raise_if_locked
raise StackLocked, LOCK_ERR if locked?
end
def raise_if_adapter(klass)
return unless is_adapter?(klass)
raise 'Adapter should be set using the `adapter` method, not `use`'
end
def ensure_adapter!
raise MISSING_ADAPTER_ERROR unless @adapter
end
def adapter_set?
!@adapter.nil?
end
def is_adapter?(klass) # rubocop:disable Naming/PredicateName
klass <= Faraday::Adapter
end
ruby2_keywords def use_symbol(mod, key, *args, &block)
use(mod.lookup_middleware(key), *args, &block)
end
def assert_index(index)
idx = index.is_a?(Integer) ? index : @handlers.index(index)
raise "No such handler: #{index.inspect}" unless idx
idx
end
end
end
@@ -0,0 +1,139 @@
# frozen_string_literal: true
module Faraday
# Used to setup URLs, params, headers, and the request body in a sane manner.
#
# @example
# @connection.post do |req|
# req.url 'http://localhost', 'a' => '1' # 'http://localhost?a=1'
# req.headers['b'] = '2' # Header
# req.params['c'] = '3' # GET Param
# req['b'] = '2' # also Header
# req.body = 'abc'
# end
#
# @!attribute http_method
# @return [Symbol] the HTTP method of the Request
# @!attribute path
# @return [URI, String] the path
# @!attribute params
# @return [Hash] query parameters
# @!attribute headers
# @return [Faraday::Utils::Headers] headers
# @!attribute body
# @return [String] body
# @!attribute options
# @return [RequestOptions] options
Request = Struct.new(:http_method, :path, :params, :headers, :body, :options) do
extend MiddlewareRegistry
alias_method :member_get, :[]
private :member_get
alias_method :member_set, :[]=
private :member_set
# @param request_method [String]
# @yield [request] for block customization, if block given
# @yieldparam request [Request]
# @return [Request]
def self.create(request_method)
new(request_method).tap do |request|
yield(request) if block_given?
end
end
remove_method :params=
# Replace params, preserving the existing hash type.
#
# @param hash [Hash] new params
def params=(hash)
if params
params.replace hash
else
member_set(:params, hash)
end
end
remove_method :headers=
# Replace request headers, preserving the existing hash type.
#
# @param hash [Hash] new headers
def headers=(hash)
if headers
headers.replace hash
else
member_set(:headers, hash)
end
end
# Update path and params.
#
# @param path [URI, String]
# @param params [Hash, nil]
# @return [void]
def url(path, params = nil)
if path.respond_to? :query
if (query = path.query)
path = path.dup
path.query = nil
end
else
anchor_index = path.index('#')
path = path.slice(0, anchor_index) unless anchor_index.nil?
path, query = path.split('?', 2)
end
self.path = path
self.params.merge_query query, options.params_encoder
self.params.update(params) if params
end
# @param key [Object] key to look up in headers
# @return [Object] value of the given header name
def [](key)
headers[key]
end
# @param key [Object] key of header to write
# @param value [Object] value of header
def []=(key, value)
headers[key] = value
end
# Marshal serialization support.
#
# @return [Hash] the hash ready to be serialized in Marshal.
def marshal_dump
{
http_method: http_method,
body: body,
headers: headers,
path: path,
params: params,
options: options
}
end
# Marshal serialization support.
# Restores the instance variables according to the +serialised+.
# @param serialised [Hash] the serialised object.
def marshal_load(serialised)
self.http_method = serialised[:http_method]
self.body = serialised[:body]
self.headers = serialised[:headers]
self.path = serialised[:path]
self.params = serialised[:params]
self.options = serialised[:options]
end
# @return [Env] the Env for this Request
def to_env(connection)
Env.new(http_method, body, connection.build_exclusive_url(path, params),
options, headers, connection.ssl, connection.parallel_manager)
end
end
end
require 'faraday/request/authorization'
require 'faraday/request/instrumentation'
require 'faraday/request/json'
require 'faraday/request/url_encoded'
@@ -0,0 +1,54 @@
# frozen_string_literal: true
module Faraday
class Request
# Request middleware for the Authorization HTTP header
class Authorization < Faraday::Middleware
KEY = 'Authorization'
# @param app [#call]
# @param type [String, Symbol] Type of Authorization
# @param params [Array<String, Proc, #call>] parameters to build the Authorization header.
# If the type is `:basic`, then these can be a login and password pair.
# Otherwise, a single value is expected that will be appended after the type.
# This value can be a proc or an object responding to `.call`, in which case
# it will be invoked on each request.
def initialize(app, type, *params)
@type = type
@params = params
super(app)
end
# @param env [Faraday::Env]
def on_request(env)
return if env.request_headers[KEY]
env.request_headers[KEY] = header_from(@type, env, *@params)
end
private
# @param type [String, Symbol]
# @param env [Faraday::Env]
# @param params [Array]
# @return [String] a header value
def header_from(type, env, *params)
if type.to_s.casecmp('basic').zero? && params.size == 2
Utils.basic_header_from(*params)
elsif params.size != 1
raise ArgumentError, "Unexpected params received (got #{params.size} instead of 1)"
else
value = params.first
if (value.is_a?(Proc) && value.arity == 1) || (value.respond_to?(:call) && value.method(:call).arity == 1)
value = value.call(env)
elsif value.is_a?(Proc) || value.respond_to?(:call)
value = value.call
end
"#{type} #{value}"
end
end
end
end
end
Faraday::Request.register_middleware(authorization: Faraday::Request::Authorization)
@@ -0,0 +1,58 @@
# frozen_string_literal: true
module Faraday
class Request
# Middleware for instrumenting Requests.
class Instrumentation < Faraday::Middleware
# Options class used in Request::Instrumentation class.
Options = Faraday::Options.new(:name, :instrumenter) do
remove_method :name
# @return [String]
def name
self[:name] ||= 'request.faraday'
end
remove_method :instrumenter
# @return [Class]
def instrumenter
self[:instrumenter] ||= ActiveSupport::Notifications
end
end
# Instruments requests using Active Support.
#
# Measures time spent only for synchronous requests.
#
# @example Using ActiveSupport::Notifications to measure time spent
# for Faraday requests.
# ActiveSupport::Notifications
# .subscribe('request.faraday') do |name, starts, ends, _, env|
# url = env[:url]
# http_method = env[:method].to_s.upcase
# duration = ends - starts
# $stderr.puts '[%s] %s %s (%.3f s)' %
# [url.host, http_method, url.request_uri, duration]
# end
# @param app [#call]
# @param options [nil, Hash] Options hash
# @option options [String] :name ('request.faraday')
# Name of the instrumenter
# @option options [Class] :instrumenter (ActiveSupport::Notifications)
# Active Support instrumenter class.
def initialize(app, options = nil)
super(app)
@name, @instrumenter = Options.from(options)
.values_at(:name, :instrumenter)
end
# @param env [Faraday::Env]
def call(env)
@instrumenter.instrument(@name, env) do
@app.call(env)
end
end
end
end
end
Faraday::Request.register_middleware(instrumentation: Faraday::Request::Instrumentation)
@@ -0,0 +1,70 @@
# frozen_string_literal: true
require 'json'
module Faraday
class Request
# Request middleware that encodes the body as JSON.
#
# Processes only requests with matching Content-type or those without a type.
# If a request doesn't have a type but has a body, it sets the Content-type
# to JSON MIME-type.
#
# Doesn't try to encode bodies that already are in string form.
class Json < Middleware
MIME_TYPE = 'application/json'
MIME_TYPE_REGEX = %r{^application/(vnd\..+\+)?json$}
def on_request(env)
match_content_type(env) do |data|
env[:body] = encode(data)
end
end
private
def encode(data)
if options[:encoder].is_a?(Array) && options[:encoder].size >= 2
options[:encoder][0].public_send(options[:encoder][1], data)
elsif options[:encoder].respond_to?(:dump)
options[:encoder].dump(data)
else
::JSON.generate(data)
end
end
def match_content_type(env)
return unless process_request?(env)
env[:request_headers][CONTENT_TYPE] ||= MIME_TYPE
yield env[:body] unless env[:body].respond_to?(:to_str)
end
def process_request?(env)
type = request_type(env)
body?(env) && (type.empty? || type.match?(MIME_TYPE_REGEX))
end
def body?(env)
body = env[:body]
case body
when true, false
true
when nil
# NOTE: nil can be converted to `"null"`, but this middleware doesn't process `nil` for the compatibility.
false
else
!(body.respond_to?(:to_str) && body.empty?)
end
end
def request_type(env)
type = env[:request_headers][CONTENT_TYPE].to_s
type = type.split(';', 2).first if type.index(';')
type
end
end
end
end
Faraday::Request.register_middleware(json: Faraday::Request::Json)
@@ -0,0 +1,60 @@
# frozen_string_literal: true
module Faraday
class Request
# Middleware for supporting urlencoded requests.
class UrlEncoded < Faraday::Middleware
unless defined?(::Faraday::Request::UrlEncoded::CONTENT_TYPE)
CONTENT_TYPE = 'Content-Type'
end
class << self
attr_accessor :mime_type
end
self.mime_type = 'application/x-www-form-urlencoded'
# Encodes as "application/x-www-form-urlencoded" if not already encoded or
# of another type.
#
# @param env [Faraday::Env]
def call(env)
match_content_type(env) do |data|
params = Faraday::Utils::ParamsHash[data]
env.body = params.to_query(env.params_encoder)
end
@app.call env
end
# @param env [Faraday::Env]
# @yield [request_body] Body of the request
def match_content_type(env)
return unless process_request?(env)
env.request_headers[CONTENT_TYPE] ||= self.class.mime_type
return if env.body.respond_to?(:to_str) || env.body.respond_to?(:read)
yield(env.body)
end
# @param env [Faraday::Env]
#
# @return [Boolean] True if the request has a body and its Content-Type is
# urlencoded.
def process_request?(env)
type = request_type(env)
env.body && (type.empty? || (type == self.class.mime_type))
end
# @param env [Faraday::Env]
#
# @return [String]
def request_type(env)
type = env.request_headers[CONTENT_TYPE].to_s
type = type.split(';', 2).first if type.index(';')
type
end
end
end
end
Faraday::Request.register_middleware(url_encoded: Faraday::Request::UrlEncoded)
@@ -0,0 +1,91 @@
# frozen_string_literal: true
require 'forwardable'
module Faraday
# Response represents an HTTP response from making an HTTP request.
class Response
extend Forwardable
extend MiddlewareRegistry
def initialize(env = nil)
@env = Env.from(env) if env
@on_complete_callbacks = []
end
attr_reader :env
def status
finished? ? env.status : nil
end
def reason_phrase
finished? ? env.reason_phrase : nil
end
def headers
finished? ? env.response_headers : {}
end
def_delegator :headers, :[]
def body
finished? ? env.body : nil
end
def finished?
!!env
end
def on_complete(&block)
if finished?
yield(env)
else
@on_complete_callbacks << block
end
self
end
def finish(env)
raise 'response already finished' if finished?
@env = env.is_a?(Env) ? env : Env.from(env)
@on_complete_callbacks.each { |callback| callback.call(@env) }
self
end
def success?
finished? && env.success?
end
def to_hash
{
status: env.status, body: env.body,
response_headers: env.response_headers,
url: env.url
}
end
# because @on_complete_callbacks cannot be marshalled
def marshal_dump
finished? ? to_hash : nil
end
def marshal_load(env)
@env = Env.from(env)
end
# Expand the env with more properties, without overriding existing ones.
# Useful for applying request params after restoring a marshalled Response.
def apply_request(request_env)
raise "response didn't finish yet" unless finished?
@env = Env.from(request_env).update(@env)
self
end
end
end
require 'faraday/response/json'
require 'faraday/response/logger'
require 'faraday/response/raise_error'
@@ -0,0 +1,73 @@
# frozen_string_literal: true
require 'json'
module Faraday
class Response
# Parse response bodies as JSON.
class Json < Middleware
def initialize(app = nil, parser_options: nil, content_type: /\bjson$/, preserve_raw: false)
super(app)
@parser_options = parser_options
@content_types = Array(content_type)
@preserve_raw = preserve_raw
process_parser_options
end
def on_complete(env)
process_response(env) if parse_response?(env)
end
private
def process_response(env)
env[:raw_body] = env[:body] if @preserve_raw
env[:body] = parse(env[:body])
rescue StandardError, SyntaxError => e
raise Faraday::ParsingError.new(e, env[:response])
end
def parse(body)
return if body.strip.empty?
decoder, method_name = @decoder_options
decoder.public_send(method_name, body, @parser_options || {})
end
def parse_response?(env)
process_response_type?(env) &&
env[:body].respond_to?(:to_str)
end
def process_response_type?(env)
type = response_type(env)
@content_types.empty? || @content_types.any? do |pattern|
pattern.is_a?(Regexp) ? type.match?(pattern) : type == pattern
end
end
def response_type(env)
type = env[:response_headers][CONTENT_TYPE].to_s
type = type.split(';', 2).first if type.index(';')
type
end
def process_parser_options
@decoder_options = @parser_options&.delete(:decoder)
@decoder_options =
if @decoder_options.is_a?(Array) && @decoder_options.size >= 2
@decoder_options.slice(0, 2)
elsif @decoder_options.respond_to?(:load)
[@decoder_options, :load]
else
[::JSON, :parse]
end
end
end
end
end
Faraday::Response.register_middleware(json: Faraday::Response::Json)
@@ -0,0 +1,37 @@
# frozen_string_literal: true
require 'forwardable'
require 'logger'
require 'faraday/logging/formatter'
module Faraday
class Response
# Logger is a middleware that logs internal events in the HTTP request
# lifecycle to a given Logger object. By default, this logs to STDOUT. See
# Faraday::Logging::Formatter to see specifically what is logged.
class Logger < Middleware
def initialize(app, logger = nil, options = {})
super(app)
logger ||= ::Logger.new($stdout)
formatter_class = options.delete(:formatter) || Logging::Formatter
@formatter = formatter_class.new(logger: logger, options: options)
yield @formatter if block_given?
end
def call(env)
@formatter.request(env)
super
end
def on_complete(env)
@formatter.response(env)
end
def on_error(exc)
@formatter.exception(exc) if @formatter.respond_to?(:exception)
end
end
end
end
Faraday::Response.register_middleware(logger: Faraday::Response::Logger)
@@ -0,0 +1,83 @@
# frozen_string_literal: true
module Faraday
class Response
# RaiseError is a Faraday middleware that raises exceptions on common HTTP
# client or server error responses.
class RaiseError < Middleware
# rubocop:disable Naming/ConstantName
ClientErrorStatuses = (400...500)
ServerErrorStatuses = (500...600)
# rubocop:enable Naming/ConstantName
def on_complete(env)
case env[:status]
when 400
raise Faraday::BadRequestError, response_values(env)
when 401
raise Faraday::UnauthorizedError, response_values(env)
when 403
raise Faraday::ForbiddenError, response_values(env)
when 404
raise Faraday::ResourceNotFound, response_values(env)
when 407
# mimic the behavior that we get with proxy requests with HTTPS
msg = %(407 "Proxy Authentication Required")
raise Faraday::ProxyAuthError.new(msg, response_values(env))
when 408
raise Faraday::RequestTimeoutError, response_values(env)
when 409
raise Faraday::ConflictError, response_values(env)
when 422
raise Faraday::UnprocessableEntityError, response_values(env)
when 429
raise Faraday::TooManyRequestsError, response_values(env)
when ClientErrorStatuses
raise Faraday::ClientError, response_values(env)
when ServerErrorStatuses
raise Faraday::ServerError, response_values(env)
when nil
raise Faraday::NilStatusError, response_values(env)
end
end
# Returns a hash of response data with the following keys:
# - status
# - headers
# - body
# - request
#
# The `request` key is omitted when the middleware is explicitly
# configured with the option `include_request: false`.
def response_values(env)
response = {
status: env.status,
headers: env.response_headers,
body: env.body
}
# Include the request data by default. If the middleware was explicitly
# configured to _not_ include request data, then omit it.
return response unless options.fetch(:include_request, true)
response.merge(
request: {
method: env.method,
url: env.url,
url_path: env.url.path,
params: query_params(env),
headers: env.request_headers,
body: env.request_body
}
)
end
def query_params(env)
env.request.params_encoder ||= Faraday::Utils.default_params_encoder
env.params_encoder.decode(env.url.query)
end
end
end
end
Faraday::Response.register_middleware(raise_error: Faraday::Response::RaiseError)
+121
View File
@@ -0,0 +1,121 @@
# frozen_string_literal: true
require 'uri'
require 'faraday/utils/headers'
require 'faraday/utils/params_hash'
module Faraday
# Utils contains various static helper methods.
module Utils
module_function
def build_query(params)
FlatParamsEncoder.encode(params)
end
def build_nested_query(params)
NestedParamsEncoder.encode(params)
end
def default_space_encoding
@default_space_encoding ||= '+'
end
class << self
attr_writer :default_space_encoding
end
ESCAPE_RE = /[^a-zA-Z0-9 .~_-]/
def escape(str)
str.to_s.gsub(ESCAPE_RE) do |match|
"%#{match.unpack('H2' * match.bytesize).join('%').upcase}"
end.gsub(' ', default_space_encoding)
end
def unescape(str)
CGI.unescape str.to_s
end
DEFAULT_SEP = /[&;] */n
# Adapted from Rack
def parse_query(query)
FlatParamsEncoder.decode(query)
end
def parse_nested_query(query)
NestedParamsEncoder.decode(query)
end
def default_params_encoder
@default_params_encoder ||= NestedParamsEncoder
end
def basic_header_from(login, pass)
value = ["#{login}:#{pass}"].pack('m') # Base64 encoding
value.delete!("\n")
"Basic #{value}"
end
class << self
attr_writer :default_params_encoder
end
# Normalize URI() behavior across Ruby versions
#
# url - A String or URI.
#
# Returns a parsed URI.
def URI(url) # rubocop:disable Naming/MethodName
if url.respond_to?(:host)
url
elsif url.respond_to?(:to_str)
default_uri_parser.call(url)
else
raise ArgumentError, 'bad argument (expected URI object or URI string)'
end
end
def default_uri_parser
@default_uri_parser ||= Kernel.method(:URI)
end
def default_uri_parser=(parser)
@default_uri_parser = if parser.respond_to?(:call) || parser.nil?
parser
else
parser.method(:parse)
end
end
# Receives a String or URI and returns just
# the path with the query string sorted.
def normalize_path(url)
url = URI(url)
(url.path.start_with?('/') ? url.path : "/#{url.path}") +
(url.query ? "?#{sort_query_params(url.query)}" : '')
end
# Recursive hash update
def deep_merge!(target, hash)
hash.each do |key, value|
target[key] = if value.is_a?(Hash) && (target[key].is_a?(Hash) || target[key].is_a?(Options))
deep_merge(target[key], value)
else
value
end
end
target
end
# Recursive hash merge
def deep_merge(source, hash)
deep_merge!(source.dup, hash)
end
def sort_query_params(query)
query.split('&').sort.join('&')
end
end
end
@@ -0,0 +1,144 @@
# frozen_string_literal: true
module Faraday
module Utils
# A case-insensitive Hash that preserves the original case of a header
# when set.
#
# Adapted from Rack::Utils::HeaderHash
class Headers < ::Hash
def self.from(value)
new(value)
end
def self.allocate
new_self = super
new_self.initialize_names
new_self
end
def initialize(hash = nil)
super()
@names = {}
update(hash || {})
end
def initialize_names
@names = {}
end
# on dup/clone, we need to duplicate @names hash
def initialize_copy(other)
super
@names = other.names.dup
end
# need to synchronize concurrent writes to the shared KeyMap
keymap_mutex = Mutex.new
# symbol -> string mapper + cache
KeyMap = Hash.new do |map, key|
value = if key.respond_to?(:to_str)
key
else
key.to_s.split('_') # user_agent: %w(user agent)
.each(&:capitalize!) # => %w(User Agent)
.join('-') # => "User-Agent"
end
keymap_mutex.synchronize { map[key] = value }
end
KeyMap[:etag] = 'ETag'
def [](key)
key = KeyMap[key]
super(key) || super(@names[key.downcase])
end
def []=(key, val)
key = KeyMap[key]
key = (@names[key.downcase] ||= key)
# join multiple values with a comma
val = val.to_ary.join(', ') if val.respond_to?(:to_ary)
super(key, val)
end
def fetch(key, *args, &block)
key = KeyMap[key]
key = @names.fetch(key.downcase, key)
super(key, *args, &block)
end
def delete(key)
key = KeyMap[key]
key = @names[key.downcase]
return unless key
@names.delete key.downcase
super(key)
end
def include?(key)
@names.include? key.downcase
end
alias has_key? include?
alias member? include?
alias key? include?
def merge!(other)
other.each { |k, v| self[k] = v }
self
end
alias update merge!
def merge(other)
hash = dup
hash.merge! other
end
def replace(other)
clear
@names.clear
update other
self
end
def to_hash
{}.update(self)
end
def parse(header_string)
return unless header_string && !header_string.empty?
headers = header_string.split("\r\n")
# Find the last set of response headers.
start_index = headers.rindex { |x| x.start_with?('HTTP/') } || 0
last_response = headers.slice(start_index, headers.size)
last_response
.tap { |a| a.shift if a.first.start_with?('HTTP/') }
.map { |h| h.split(/:\s*/, 2) } # split key and value
.reject { |p| p[0].nil? } # ignore blank lines
.each { |key, value| add_parsed(key, value) }
end
protected
attr_reader :names
private
# Join multiple values with a comma.
def add_parsed(key, value)
if key?(key)
self[key] = self[key].to_s
self[key] << ', ' << value
else
self[key] = value
end
end
end
end
end
@@ -0,0 +1,61 @@
# frozen_string_literal: true
module Faraday
module Utils
# A hash with stringified keys.
class ParamsHash < Hash
def [](key)
super(convert_key(key))
end
def []=(key, value)
super(convert_key(key), value)
end
def delete(key)
super(convert_key(key))
end
def include?(key)
super(convert_key(key))
end
alias has_key? include?
alias member? include?
alias key? include?
def update(params)
params.each do |key, value|
self[key] = value
end
self
end
alias merge! update
def merge(params)
dup.update(params)
end
def replace(other)
clear
update(other)
end
def merge_query(query, encoder = nil)
return self unless query && !query.empty?
update((encoder || Utils.default_params_encoder).decode(query))
end
def to_query(encoder = nil)
(encoder || Utils.default_params_encoder).encode(self)
end
private
def convert_key(key)
key.to_s
end
end
end
end
@@ -0,0 +1,5 @@
# frozen_string_literal: true
module Faraday
VERSION = '2.9.0'
end