├── docs ├── .gitignore ├── assets │ ├── img │ │ ├── logo.png │ │ ├── favicon.png │ │ ├── home-banner.jpg │ │ ├── middleware.png │ │ ├── repo-card.png │ │ ├── repo-card-slim.png │ │ └── featured-bg.svg │ ├── css │ │ └── main.scss │ └── js │ │ └── team.js ├── _layouts │ └── documentation.md ├── _sass │ ├── _variables.scss │ └── faraday.sass ├── 404.html ├── README.md ├── _includes │ ├── docs_nav.md │ ├── footer.html │ └── header.html ├── team.md ├── middleware │ ├── request │ │ ├── json.md │ │ ├── url_encoded.md │ │ ├── instrumentation.md │ │ └── authentication.md │ ├── response │ │ ├── json.md │ │ ├── raise_error.md │ │ └── logger.md │ ├── list.md │ ├── custom.md │ └── index.md ├── _posts │ └── 2019-03-12-welcome-to-jekyll.markdown ├── Gemfile ├── index.md ├── _config.yml ├── usage │ ├── streaming.md │ └── customize.md └── adapters │ ├── index.md │ ├── testing.md │ └── write_your_adapter.md ├── .rspec ├── lib └── faraday │ ├── version.rb │ ├── parameters.rb │ ├── methods.rb │ ├── adapter_registry.rb │ ├── options │ ├── request_options.rb │ ├── connection_options.rb │ ├── proxy_options.rb │ └── ssl_options.rb │ ├── middleware.rb │ ├── response │ ├── logger.rb │ ├── json.rb │ └── raise_error.rb │ ├── utils │ ├── params_hash.rb │ └── headers.rb │ ├── request │ ├── json.rb │ ├── url_encoded.rb │ ├── instrumentation.rb │ └── authorization.rb │ ├── response.rb │ ├── middleware_registry.rb │ ├── adapter.rb │ ├── utils.rb │ ├── encoders │ └── flat_params_encoder.rb │ ├── logging │ └── formatter.rb │ ├── request.rb │ ├── error.rb │ └── options.rb ├── config └── external.yaml ├── bin ├── setup ├── test └── console ├── Rakefile ├── .editorconfig ├── .yardopts ├── spec ├── support │ ├── disabling_stub.rb │ ├── fake_safe_buffer.rb │ ├── shared_examples │ │ ├── params_encoder.rb │ │ └── adapter.rb │ ├── streaming_response_checker.rb │ └── helper_methods.rb ├── external_adapters │ └── faraday_specs_setup.rb ├── faraday │ ├── options │ │ ├── request_options_spec.rb │ │ ├── proxy_options_spec.rb │ │ └── env_spec.rb │ ├── adapter_registry_spec.rb │ ├── middleware_registry_spec.rb │ ├── params_encoders │ │ ├── flat_spec.rb │ │ └── nested_spec.rb │ ├── adapter_spec.rb │ ├── middleware_spec.rb │ ├── request │ │ ├── instrumentation_spec.rb │ │ ├── json_spec.rb │ │ ├── url_encoded_spec.rb │ │ └── authorization_spec.rb │ ├── error_spec.rb │ ├── response_spec.rb │ ├── response │ │ └── json_spec.rb │ ├── utils_spec.rb │ ├── utils │ │ └── headers_spec.rb │ └── request_spec.rb └── faraday_spec.rb ├── .gitignore ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ ├── refresh_team_page.yml │ ├── publish.yml │ └── ci.yml ├── ISSUE_TEMPLATE.md ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md ├── Gemfile ├── LICENSE.md ├── faraday.gemspec ├── .rubocop_todo.yml ├── README.md └── examples ├── client_spec.rb └── client_test.rb /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-metadata 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --format documentation 3 | --color 4 | -------------------------------------------------------------------------------- /docs/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/faraday/main/docs/assets/img/logo.png -------------------------------------------------------------------------------- /docs/assets/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/faraday/main/docs/assets/img/favicon.png -------------------------------------------------------------------------------- /lib/faraday/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | VERSION = '2.7.4' 5 | end 6 | -------------------------------------------------------------------------------- /docs/assets/img/home-banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/faraday/main/docs/assets/img/home-banner.jpg -------------------------------------------------------------------------------- /docs/assets/img/middleware.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/faraday/main/docs/assets/img/middleware.png -------------------------------------------------------------------------------- /docs/assets/img/repo-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/faraday/main/docs/assets/img/repo-card.png -------------------------------------------------------------------------------- /docs/_layouts/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | --- 4 | 5 | {{ content }} 6 | 7 | {% include docs_nav.md %} 8 | -------------------------------------------------------------------------------- /docs/assets/img/repo-card-slim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/faraday/main/docs/assets/img/repo-card-slim.png -------------------------------------------------------------------------------- /config/external.yaml: -------------------------------------------------------------------------------- 1 | faraday-net_http: 2 | url: https://github.com/lostisland/faraday-net_http.git 3 | command: bundle exec rspec 4 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | gem install bundler 7 | bundle install --jobs 4 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec/core/rake_task' 4 | 5 | RSpec::Core::RakeTask.new(:spec) 6 | 7 | task default: :spec 8 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle exec rubocop -a --format progress 7 | bundle exec rspec 8 | -------------------------------------------------------------------------------- /lib/faraday/parameters.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'forwardable' 4 | require 'faraday/encoders/nested_params_encoder' 5 | require 'faraday/encoders/flat_params_encoder' 6 | -------------------------------------------------------------------------------- /lib/faraday/methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | METHODS_WITH_QUERY = %w[get head delete trace].freeze 5 | METHODS_WITH_BODY = %w[post put patch].freeze 6 | end 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /docs/assets/css/main.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "variables"; 5 | @import "type-theme"; 6 | @import "faraday"; 7 | @import "https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css" 8 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | --exclude test 3 | --exclude .github 4 | --exclude coverage 5 | --exclude doc 6 | --exclude script 7 | --markup markdown 8 | --readme README.md 9 | 10 | lib/**/*.rb 11 | - 12 | CHANGELOG.md 13 | -------------------------------------------------------------------------------- /spec/support/disabling_stub.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Allows to disable WebMock stubs 4 | module DisablingStub 5 | def disable 6 | @disabled = true 7 | end 8 | 9 | def disabled? 10 | @disabled 11 | end 12 | 13 | WebMock::RequestStub.prepend self 14 | end 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## PROJECT::GENERAL 2 | coverage 3 | rdoc 4 | doc 5 | log 6 | pkg/* 7 | tmp 8 | .rvmrc 9 | .ruby-version 10 | .yardoc 11 | 12 | ## BUNDLER 13 | *.gem 14 | .bundle 15 | Gemfile.lock 16 | vendor/bundle 17 | external 18 | 19 | ## PROJECT::SPECIFIC 20 | .rbx 21 | 22 | ## IDEs 23 | .idea/ 24 | .yardoc/ 25 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | A few sentences describing the overall goals of the pull request's commits. 3 | Link to related issues if any. (As `Fixes #XXX`) 4 | 5 | ## Todos 6 | List any remaining work that needs to be done, i.e: 7 | - [ ] Tests 8 | - [ ] Documentation 9 | 10 | ## Additional Notes 11 | Optional section 12 | -------------------------------------------------------------------------------- /docs/_sass/_variables.scss: -------------------------------------------------------------------------------- 1 | // Override theme variables. 2 | 3 | @import url('https://fonts.googleapis.com/css?family=Raleway:700'); 4 | 5 | $link-color: #EE4266; 6 | $text-color: #3C3C3C; 7 | $font-family-main: 'KohinoorTelugu-Regular', Helvetica, Arial, sans-serif; 8 | $font-family-headings: 'Raleway', Helvetica, Arial, sans-serif; 9 | $search-color: #EE4266; 10 | -------------------------------------------------------------------------------- /.github/workflows/refresh_team_page.yml: -------------------------------------------------------------------------------- 1 | name: Refresh Team Page 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | permissions: {} 8 | jobs: 9 | build: 10 | name: Refresh Contributors Stats 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Call GitHub API 14 | run: | 15 | curl "https://api.github.com/repos/${{ github.repository }}/stats/contributors" 16 | -------------------------------------------------------------------------------- /spec/support/fake_safe_buffer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # emulates ActiveSupport::SafeBuffer#gsub 4 | FakeSafeBuffer = Struct.new(:string) do 5 | def to_s 6 | self 7 | end 8 | 9 | def gsub(regex) 10 | string.gsub(regex) do 11 | match, = Regexp.last_match(0), '' =~ /a/ # rubocop:disable Performance/StringInclude 12 | yield(match) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'faraday' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | 18 | 19 |
20 |

404

21 | 22 |

Page not found :(

23 |

The requested page could not be found.

24 |
25 | -------------------------------------------------------------------------------- /spec/external_adapters/faraday_specs_setup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'webmock/rspec' 4 | WebMock.disable_net_connect!(allow_localhost: true) 5 | 6 | require_relative '../support/helper_methods' 7 | require_relative '../support/disabling_stub' 8 | require_relative '../support/streaming_response_checker' 9 | require_relative '../support/shared_examples/adapter' 10 | require_relative '../support/shared_examples/request_method' 11 | 12 | RSpec.configure do |config| 13 | config.include Faraday::HelperMethods 14 | end 15 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Faraday Website 2 | 3 | This is the root directory of the [Faraday Website][website]. 4 | If you want to apply changes to it, please test it locally using `Jekyll`. 5 | 6 | Here is how: 7 | 8 | ```bash 9 | # Navigate into the /docs folder 10 | $ cd docs 11 | 12 | # Install Jekyll dependencies, this bundle is different from Faraday's one. 13 | $ bundle install 14 | 15 | # Run the Jekyll server with the Faraday website 16 | $ bundle exec jekyll serve 17 | 18 | # The site will now be reachable at http://127.0.0.1:4000/faraday/ 19 | ``` 20 | 21 | [website]: https://lostisland.github.io/faraday 22 | -------------------------------------------------------------------------------- /spec/support/shared_examples/params_encoder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | shared_examples 'a params encoder' do 4 | it 'escapes safe buffer' do 5 | monies = FakeSafeBuffer.new('$32,000.00') 6 | expect(subject.encode('a' => monies)).to eq('a=%2432%2C000.00') 7 | end 8 | 9 | it 'raises type error for empty string' do 10 | expect { subject.encode('') }.to raise_error(TypeError) do |error| 11 | expect(error.message).to eq("Can't convert String into Hash.") 12 | end 13 | end 14 | 15 | it 'encodes nil' do 16 | expect(subject.encode('a' => nil)).to eq('a') 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /docs/_includes/docs_nav.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | {% if page.prev_link %} 5 | {{ page.prev_name }} 6 | {% endif %} 7 |

8 |

9 | {% if page.top_link %} 10 | {{ page.top_name }} 11 | {% endif %} 12 |

13 |

14 | {% if page.next_link %} 15 | {{ page.next_name }} 16 | {% endif %} 17 |

18 |
19 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read # to checkout the code (actions/checkout) 9 | jobs: 10 | build: 11 | name: Publish to Rubygems 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Set up Ruby 2.7 18 | uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: 2.7 21 | 22 | - name: Publish to RubyGems 23 | uses: dawidd6/action-publish-gem@v1 24 | with: 25 | api_key: ${{secrets.RUBYGEMS_AUTH_TOKEN}} 26 | -------------------------------------------------------------------------------- /spec/faraday/options/request_options_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Faraday::RequestOptions do 4 | subject(:options) { Faraday::RequestOptions.new } 5 | 6 | it 'allows to set the request proxy' do 7 | expect(options.proxy).to be_nil 8 | 9 | expect { options[:proxy] = { booya: 1 } }.to raise_error(NoMethodError) 10 | 11 | options[:proxy] = { user: 'user' } 12 | expect(options.proxy).to be_a_kind_of(Faraday::ProxyOptions) 13 | expect(options.proxy.user).to eq('user') 14 | 15 | options.proxy = nil 16 | expect(options.proxy).to be_nil 17 | expect(options.inspect).to eq('#') 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/faraday/adapter_registry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'monitor' 4 | 5 | module Faraday 6 | # AdapterRegistry registers adapter class names so they can be looked up by a 7 | # String or Symbol name. 8 | class AdapterRegistry 9 | def initialize 10 | @lock = Monitor.new 11 | @constants = {} 12 | end 13 | 14 | def get(name) 15 | klass = @lock.synchronize do 16 | @constants[name] 17 | end 18 | return klass if klass 19 | 20 | Object.const_get(name).tap { |c| set(c, name) } 21 | end 22 | 23 | def set(klass, name = nil) 24 | name ||= klass.to_s 25 | @lock.synchronize do 26 | @constants[name] = klass 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/faraday/options/request_options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | # @!parse 5 | # # RequestOptions contains the configurable properties for a Faraday request. 6 | # class RequestOptions < Options; end 7 | RequestOptions = Options.new(:params_encoder, :proxy, :bind, 8 | :timeout, :open_timeout, :read_timeout, 9 | :write_timeout, :boundary, :oauth, 10 | :context, :on_data) do 11 | def []=(key, value) 12 | if key && key.to_sym == :proxy 13 | super(key, value ? ProxyOptions.from(value) : nil) 14 | else 15 | super(key, value) 16 | end 17 | end 18 | 19 | def stream_response? 20 | on_data.is_a?(Proc) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/faraday/options/connection_options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | # @!parse 5 | # # ConnectionOptions contains the configurable properties for a Faraday 6 | # # connection object. 7 | # class ConnectionOptions < Options; end 8 | ConnectionOptions = Options.new(:request, :proxy, :ssl, :builder, :url, 9 | :parallel_manager, :params, :headers, 10 | :builder_class) do 11 | options request: RequestOptions, ssl: SSLOptions 12 | 13 | memoized(:request) { self.class.options_for(:request).new } 14 | 15 | memoized(:ssl) { self.class.options_for(:ssl).new } 16 | 17 | memoized(:builder_class) { RackBuilder } 18 | 19 | def new_builder(block) 20 | builder_class.new(&block) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Even though we don't officially support JRuby, this dependency makes Faraday 6 | # compatible with it, so we're leaving it in for jruby users to use it. 7 | gem 'jruby-openssl', '~> 0.11.0', platforms: :jruby 8 | 9 | group :development, :test do 10 | gem 'bake-test-external' 11 | gem 'coveralls_reborn', require: false 12 | gem 'pry' 13 | gem 'rack', '~> 2.2' 14 | gem 'rake' 15 | gem 'rspec', '~> 3.7' 16 | gem 'rspec_junit_formatter', '~> 0.4' 17 | gem 'simplecov' 18 | gem 'webmock', '~> 3.4' 19 | end 20 | 21 | group :development, :lint do 22 | gem 'rubocop' 23 | gem 'rubocop-packaging', github: 'utkarsh2102/rubocop-packaging' # '~> 0.5' 24 | gem 'rubocop-performance', '~> 1.0' 25 | gem 'yard-junk' 26 | end 27 | 28 | gemspec 29 | -------------------------------------------------------------------------------- /docs/team.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Team 4 | permalink: /team/ 5 | order: 4 6 | --- 7 | 8 |
9 |
10 |
11 | 12 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Basic Info 2 | 3 | * Faraday Version: 4 | * Ruby Version: 5 | 6 | ## Issue description 7 | Please provide a description of the issue you're experiencing. 8 | Please also provide the exception message/stacktrace or any other useful detail. 9 | 10 | ## Steps to reproduce 11 | If possible, please provide the steps to reproduce the issue. 12 | 13 | ## CHECKLIST (delete before creating the issue) 14 | * If you're not reporting a bug/issue, you can ignore this whole template. 15 | * Are you using the latest Faraday version? If not, please check the [Releases](https://github.com/lostisland/faraday/releases) page to see if the issue has already been fixed. 16 | * Provide the Faraday and Ruby Version you're using while experiencing the issue. 17 | * Fill the `Issue description` and `Steps to Reproduce` sections. 18 | * Delete this checklist before posting the issue. 19 | -------------------------------------------------------------------------------- /lib/faraday/middleware.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | # Middleware is the basic base class of any Faraday middleware. 5 | class Middleware 6 | extend MiddlewareRegistry 7 | 8 | attr_reader :app, :options 9 | 10 | def initialize(app = nil, options = {}) 11 | @app = app 12 | @options = options 13 | end 14 | 15 | def call(env) 16 | on_request(env) if respond_to?(:on_request) 17 | app.call(env).on_complete do |environment| 18 | on_complete(environment) if respond_to?(:on_complete) 19 | end 20 | rescue StandardError => e 21 | on_error(e) if respond_to?(:on_error) 22 | raise 23 | end 24 | 25 | def close 26 | if app.respond_to?(:close) 27 | app.close 28 | else 29 | warn "#{app} does not implement \#close!" 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /docs/_includes/footer.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | {% if site.theme_settings.katex and page.id %} 5 | 6 | {% endif %} 7 | 8 | {% if site.theme_settings.footer_text %} 9 | 12 | {% endif %} 13 | 14 | 15 | 22 | -------------------------------------------------------------------------------- /spec/faraday/adapter_registry_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Faraday::AdapterRegistry do 4 | describe '#initialize' do 5 | subject(:registry) { described_class.new } 6 | 7 | it { expect { registry.get(:FinFangFoom) }.to raise_error(NameError) } 8 | it { expect { registry.get('FinFangFoom') }.to raise_error(NameError) } 9 | 10 | it 'looks up class by string name' do 11 | expect(registry.get('Faraday::Connection')).to eq(Faraday::Connection) 12 | end 13 | 14 | it 'looks up class by symbol name' do 15 | expect(registry.get(:Faraday)).to eq(Faraday) 16 | end 17 | 18 | it 'caches lookups with implicit name' do 19 | registry.set :symbol 20 | expect(registry.get('symbol')).to eq(:symbol) 21 | end 22 | 23 | it 'caches lookups with explicit name' do 24 | registry.set 'string', :name 25 | expect(registry.get(:name)).to eq('string') 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /docs/middleware/request/json.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: "JSON Request Middleware" 4 | permalink: /middleware/json-request 5 | hide: true 6 | prev_name: UrlEncoded Middleware 7 | prev_link: ./url-encoded 8 | next_name: Instrumentation Middleware 9 | next_link: ./instrumentation 10 | top_name: Back to Middleware 11 | top_link: ./list 12 | --- 13 | 14 | The `JSON` request middleware converts a `Faraday::Request#body` hash of key/value pairs into a JSON request body. 15 | The middleware also automatically sets the `Content-Type` header to `application/json`, 16 | processes only requests with matching Content-Type or those without a type and 17 | doesn't try to encode bodies that already are in string form. 18 | 19 | ### Example Usage 20 | 21 | ```ruby 22 | conn = Faraday.new(...) do |f| 23 | f.request :json 24 | ... 25 | end 26 | 27 | conn.post('/', { a: 1, b: 2 }) 28 | # POST with 29 | # Content-Type: application/json 30 | # Body: {"a":1,"b":2} 31 | ``` 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2023 Rick Olson, Zack Hobson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /spec/faraday_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Faraday do 4 | it 'has a version number' do 5 | expect(Faraday::VERSION).not_to be nil 6 | end 7 | 8 | context 'proxies to default_connection' do 9 | let(:mock_connection) { double('Connection') } 10 | before do 11 | Faraday.default_connection = mock_connection 12 | end 13 | 14 | it 'proxies methods that exist on the default_connection' do 15 | expect(mock_connection).to receive(:this_should_be_proxied) 16 | 17 | Faraday.this_should_be_proxied 18 | end 19 | 20 | it 'uses method_missing on Faraday if there is no proxyable method' do 21 | expect { Faraday.this_method_does_not_exist }.to raise_error( 22 | NoMethodError, 23 | "undefined method `this_method_does_not_exist' for Faraday:Module" 24 | ) 25 | end 26 | 27 | it 'proxied methods can be accessed' do 28 | allow(mock_connection).to receive(:this_should_be_proxied) 29 | 30 | expect(Faraday.method(:this_should_be_proxied)).to be_a(Method) 31 | end 32 | 33 | after do 34 | Faraday.default_connection = nil 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/faraday/response/logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'forwardable' 4 | require 'logger' 5 | require 'faraday/logging/formatter' 6 | 7 | module Faraday 8 | class Response 9 | # Logger is a middleware that logs internal events in the HTTP request 10 | # lifecycle to a given Logger object. By default, this logs to STDOUT. See 11 | # Faraday::Logging::Formatter to see specifically what is logged. 12 | class Logger < Middleware 13 | def initialize(app, logger = nil, options = {}) 14 | super(app) 15 | logger ||= ::Logger.new($stdout) 16 | formatter_class = options.delete(:formatter) || Logging::Formatter 17 | @formatter = formatter_class.new(logger: logger, options: options) 18 | yield @formatter if block_given? 19 | end 20 | 21 | def call(env) 22 | @formatter.request(env) 23 | super 24 | end 25 | 26 | def on_complete(env) 27 | @formatter.response(env) 28 | end 29 | 30 | def on_error(exc) 31 | @formatter.exception(exc) if @formatter.respond_to?(:exception) 32 | end 33 | end 34 | end 35 | end 36 | 37 | Faraday::Response.register_middleware(logger: Faraday::Response::Logger) 38 | -------------------------------------------------------------------------------- /lib/faraday/options/proxy_options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | # @!parse 5 | # # ProxyOptions contains the configurable properties for the proxy 6 | # # configuration used when making an HTTP request. 7 | # class ProxyOptions < Options; end 8 | ProxyOptions = Options.new(:uri, :user, :password) do 9 | extend Forwardable 10 | def_delegators :uri, :scheme, :scheme=, :host, :host=, :port, :port=, 11 | :path, :path= 12 | 13 | def self.from(value) 14 | case value 15 | when String 16 | # URIs without a scheme should default to http (like 'example:123'). 17 | # This fixes #1282 and prevents a silent failure in some adapters. 18 | value = "http://#{value}" unless value.include?('://') 19 | value = { uri: Utils.URI(value) } 20 | when URI 21 | value = { uri: value } 22 | when Hash, Options 23 | if (uri = value.delete(:uri)) 24 | value[:uri] = Utils.URI(uri) 25 | end 26 | end 27 | 28 | super(value) 29 | end 30 | 31 | memoized(:user) { uri&.user && Utils.unescape(uri.user) } 32 | memoized(:password) { uri&.password && Utils.unescape(uri.password) } 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/faraday/middleware_registry_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Faraday::MiddlewareRegistry do 4 | before do 5 | stub_const('CustomMiddleware', custom_middleware_klass) 6 | end 7 | let(:custom_middleware_klass) { Class.new(Faraday::Middleware) } 8 | let(:dummy) { Class.new { extend Faraday::MiddlewareRegistry } } 9 | 10 | after { dummy.unregister_middleware(:custom) } 11 | 12 | it 'allows to register with constant' do 13 | dummy.register_middleware(custom: custom_middleware_klass) 14 | expect(dummy.lookup_middleware(:custom)).to eq(custom_middleware_klass) 15 | end 16 | 17 | it 'allows to register with symbol' do 18 | dummy.register_middleware(custom: :CustomMiddleware) 19 | expect(dummy.lookup_middleware(:custom)).to eq(custom_middleware_klass) 20 | end 21 | 22 | it 'allows to register with string' do 23 | dummy.register_middleware(custom: 'CustomMiddleware') 24 | expect(dummy.lookup_middleware(:custom)).to eq(custom_middleware_klass) 25 | end 26 | 27 | it 'allows to register with Proc' do 28 | dummy.register_middleware(custom: -> { custom_middleware_klass }) 29 | expect(dummy.lookup_middleware(:custom)).to eq(custom_middleware_klass) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/support/streaming_response_checker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | module StreamingResponseChecker 5 | def check_streaming_response(streamed, options = {}) 6 | opts = { 7 | prefix: '', 8 | streaming?: true 9 | }.merge(options) 10 | 11 | expected_response = opts[:prefix] + big_string 12 | 13 | chunks, sizes = streamed.transpose 14 | 15 | # Check that the total size of the chunks (via the last size returned) 16 | # is the same size as the expected_response 17 | expect(sizes.last).to eq(expected_response.bytesize) 18 | 19 | start_index = 0 20 | expected_chunks = [] 21 | chunks.each do |actual_chunk| 22 | expected_chunk = expected_response[start_index..((start_index + actual_chunk.bytesize) - 1)] 23 | expected_chunks << expected_chunk 24 | start_index += expected_chunk.bytesize 25 | end 26 | 27 | # it's easier to read a smaller portion, so we check that first 28 | expect(expected_chunks[0][0..255]).to eq(chunks[0][0..255]) 29 | 30 | [expected_chunks, chunks].transpose.each do |expected, actual| 31 | expect(actual).to eq(expected) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /docs/middleware/request/url_encoded.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: "UrlEncoded Middleware" 4 | permalink: /middleware/url-encoded 5 | hide: true 6 | prev_name: Authentication Middleware 7 | prev_link: ./authentication 8 | next_name: JSON Request Middleware 9 | next_link: ./json-request 10 | top_name: Back to Middleware 11 | top_link: ./list 12 | --- 13 | 14 | The `UrlEncoded` middleware converts a `Faraday::Request#body` hash of key/value pairs into a url-encoded request body. 15 | The middleware also automatically sets the `Content-Type` header to `application/x-www-form-urlencoded`. 16 | The way parameters are serialized can be [customized][customize]. 17 | 18 | 19 | ### Example Usage 20 | 21 | ```ruby 22 | conn = Faraday.new(...) do |f| 23 | f.request :url_encoded 24 | ... 25 | end 26 | 27 | conn.post('/', { a: 1, b: 2 }) 28 | # POST with 29 | # Content-Type: application/x-www-form-urlencoded 30 | # Body: a=1&b=2 31 | ``` 32 | 33 | Complex structures can also be passed 34 | 35 | ```ruby 36 | conn.post('/', { a: [1, 3], b: { c: 2, d: 4} }) 37 | # POST with 38 | # Content-Type: application/x-www-form-urlencoded 39 | # Body: a%5B%5D=1&a%5B%5D=3&b%5Bc%5D=2&b%5Bd%5D=4 40 | ``` 41 | 42 | [customize]: ../usage/customize#changing-how-parameters-are-serialized 43 | -------------------------------------------------------------------------------- /docs/_posts/2019-03-12-welcome-to-jekyll.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Welcome to Jekyll!" 4 | date: 2019-03-12 10:25:23 +0000 5 | categories: jekyll update 6 | --- 7 | You’ll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve`, which launches a web server and auto-regenerates your site when a file is updated. 8 | 9 | To add new posts, simply add a file in the `_posts` directory that follows the convention `YYYY-MM-DD-name-of-post.ext` and includes the necessary front matter. Take a look at the source for this post to get an idea about how it works. 10 | 11 | Jekyll also offers powerful support for code snippets: 12 | 13 | {% highlight ruby %} 14 | def print_hi(name) 15 | puts "Hi, #{name}" 16 | end 17 | print_hi('Tom') 18 | #=> prints 'Hi, Tom' to STDOUT. 19 | {% endhighlight %} 20 | 21 | Check out the [Jekyll docs][jekyll-docs] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekyll’s GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll Talk][jekyll-talk]. 22 | 23 | [jekyll-docs]: https://jekyllrb.com/docs/home 24 | [jekyll-gh]: https://github.com/jekyll/jekyll 25 | [jekyll-talk]: https://talk.jekyllrb.com/ 26 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Hello! This is where you manage which Jekyll version is used to run. 6 | # When you want to use a different version, change it below, save the 7 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so: 8 | # 9 | # bundle exec jekyll serve 10 | # 11 | # This will help ensure the proper Jekyll version is running. 12 | # Happy Jekylling! 13 | # gem "jekyll", "~> 3.7.4" 14 | 15 | # This is the default theme for new Jekyll sites. 16 | # You may change this to anything you like. 17 | # gem "minima", "~> 2.0" 18 | # gem "jekyll-theme-type" 19 | gem 'jekyll-remote-theme' 20 | 21 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and 22 | # uncomment the line below. To upgrade, run `bundle update github-pages`. 23 | gem 'github-pages', group: :jekyll_plugins 24 | 25 | # If you have any plugins, put them here! 26 | group :jekyll_plugins do 27 | gem 'jekyll-feed', '~> 0.6' 28 | end 29 | 30 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 31 | gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] 32 | 33 | # Performance-booster for watching directories on Windows 34 | gem 'wdm', '~> 0.1.0' if Gem.win_platform? 35 | 36 | # Ruby 3.X doesn't come with webrick by default anymore 37 | gem 'webrick', '~> 1.7' 38 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # You don't need to edit this file, it's empty on purpose. 3 | # Edit theme's home layout instead if you wanna make some changes 4 | # See: https://jekyllrb.com/docs/themes/#overriding-theme-defaults 5 | layout: page 6 | title: Homepage 7 | feature-title: 8 | feature-img: "assets/img/featured-bg.svg" 9 | hide: true 10 | --- 11 | 12 | Faraday is an HTTP client library that provides a common interface over many adapters (such as Net::HTTP) 13 | and embraces the concept of Rack middleware when processing the request/response cycle. 14 | 15 | {: .text-center} 16 | [ Fork on GitHub][github]{: .btn} 17 | [ Chat with us][gitter]{: .btn} 18 | 19 | {: .mt-60} 20 | ## Installation 21 | 22 | Add this line to your application's Gemfile: 23 | 24 | ```ruby 25 | gem 'faraday' 26 | ``` 27 | 28 | And then execute: 29 | 30 | ```bash 31 | $ bundle 32 | ``` 33 | 34 | Or install it yourself as: 35 | 36 | ```bash 37 | $ gem install faraday 38 | ``` 39 | 40 | {: .mt-60} 41 | 42 | {: .text-center} 43 | [ Read the docs][usage]{: .btn} 44 | 45 | [github]: https://github.com/lostisland/faraday 46 | [gitter]: https://gitter.im/lostisland/faraday 47 | [usage]: ./usage 48 | -------------------------------------------------------------------------------- /spec/faraday/params_encoders/flat_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rack/utils' 4 | 5 | RSpec.describe Faraday::FlatParamsEncoder do 6 | it_behaves_like 'a params encoder' 7 | 8 | it 'decodes arrays' do 9 | query = 'a=one&a=two&a=three' 10 | expected = { 'a' => %w[one two three] } 11 | expect(subject.decode(query)).to eq(expected) 12 | end 13 | 14 | it 'decodes boolean values' do 15 | query = 'a=true&b=false' 16 | expected = { 'a' => 'true', 'b' => 'false' } 17 | expect(subject.decode(query)).to eq(expected) 18 | end 19 | 20 | it 'encodes boolean values' do 21 | params = { a: true, b: false } 22 | expect(subject.encode(params)).to eq('a=true&b=false') 23 | end 24 | 25 | it 'encodes boolean values in array' do 26 | params = { a: [true, false] } 27 | expect(subject.encode(params)).to eq('a=true&a=false') 28 | end 29 | 30 | it 'encodes empty array in hash' do 31 | params = { a: [] } 32 | expect(subject.encode(params)).to eq('a=') 33 | end 34 | 35 | it 'encodes unsorted when asked' do 36 | params = { b: false, a: true } 37 | expect(subject.encode(params)).to eq('a=true&b=false') 38 | Faraday::FlatParamsEncoder.sort_params = false 39 | expect(subject.encode(params)).to eq('b=false&a=true') 40 | Faraday::FlatParamsEncoder.sort_params = true 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /docs/middleware/response/json.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: "JSON Response Middleware" 4 | permalink: /middleware/json-response 5 | hide: true 6 | prev_name: Instrumentation Middleware 7 | prev_link: ./instrumentation 8 | next_name: Logger Middleware 9 | next_link: ./logger 10 | top_name: Back to Middleware 11 | top_link: ./list 12 | --- 13 | 14 | The `JSON` response middleware parses response body into a hash of key/value pairs. 15 | The behaviour can be customized with the following options: 16 | * **parser_options:** options that will be sent to the JSON.parse method. Defaults to {}. 17 | * **content_type:** Single value or Array of response content-types that should be processed. Can be either strings or Regex. Defaults to `/\bjson$/`. 18 | * **preserve_raw:** If set to true, the original un-parsed response will be stored in the `response.env[:raw_body]` property. Defaults to `false`. 19 | 20 | ### Example Usage 21 | 22 | ```ruby 23 | conn = Faraday.new('http://httpbingo.org') do |f| 24 | f.response :json, **options 25 | end 26 | 27 | conn.get('json').body 28 | # => {"slideshow"=>{"author"=>"Yours Truly", "date"=>"date of publication", "slides"=>[{"title"=>"Wake up to WonderWidgets!", "type"=>"all"}, {"items"=>["Why WonderWidgets are great", "Who buys WonderWidgets"], "title"=>"Overview", "type"=>"all"}], "title"=>"Sample Slide Show"}} 29 | ``` 30 | -------------------------------------------------------------------------------- /spec/faraday/adapter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Faraday::Adapter do 4 | let(:adapter) { Faraday::Adapter.new } 5 | let(:request) { {} } 6 | 7 | context '#request_timeout' do 8 | it 'gets :read timeout' do 9 | expect(timeout(:read)).to eq(nil) 10 | 11 | request[:timeout] = 5 12 | request[:write_timeout] = 1 13 | 14 | expect(timeout(:read)).to eq(5) 15 | 16 | request[:read_timeout] = 2 17 | 18 | expect(timeout(:read)).to eq(2) 19 | end 20 | 21 | it 'gets :open timeout' do 22 | expect(timeout(:open)).to eq(nil) 23 | 24 | request[:timeout] = 5 25 | request[:write_timeout] = 1 26 | 27 | expect(timeout(:open)).to eq(5) 28 | 29 | request[:open_timeout] = 2 30 | 31 | expect(timeout(:open)).to eq(2) 32 | end 33 | 34 | it 'gets :write timeout' do 35 | expect(timeout(:write)).to eq(nil) 36 | 37 | request[:timeout] = 5 38 | request[:read_timeout] = 1 39 | 40 | expect(timeout(:write)).to eq(5) 41 | 42 | request[:write_timeout] = 2 43 | 44 | expect(timeout(:write)).to eq(2) 45 | end 46 | 47 | it 'attempts unknown timeout type' do 48 | expect { timeout(:unknown) }.to raise_error(ArgumentError) 49 | end 50 | 51 | def timeout(type) 52 | adapter.send(:request_timeout, type, request) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/faraday/utils/params_hash.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | module Utils 5 | # A hash with stringified keys. 6 | class ParamsHash < Hash 7 | def [](key) 8 | super(convert_key(key)) 9 | end 10 | 11 | def []=(key, value) 12 | super(convert_key(key), value) 13 | end 14 | 15 | def delete(key) 16 | super(convert_key(key)) 17 | end 18 | 19 | def include?(key) 20 | super(convert_key(key)) 21 | end 22 | 23 | alias has_key? include? 24 | alias member? include? 25 | alias key? include? 26 | 27 | def update(params) 28 | params.each do |key, value| 29 | self[key] = value 30 | end 31 | self 32 | end 33 | alias merge! update 34 | 35 | def merge(params) 36 | dup.update(params) 37 | end 38 | 39 | def replace(other) 40 | clear 41 | update(other) 42 | end 43 | 44 | def merge_query(query, encoder = nil) 45 | return self unless query && !query.empty? 46 | 47 | update((encoder || Utils.default_params_encoder).decode(query)) 48 | end 49 | 50 | def to_query(encoder = nil) 51 | (encoder || Utils.default_params_encoder).encode(self) 52 | end 53 | 54 | private 55 | 56 | def convert_key(key) 57 | key.to_s 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | # SITE CONFIGURATION 2 | url: 'https://lostisland.github.io' 3 | baseurl: '/faraday' 4 | repository: 'lostisland/faraday' 5 | 6 | # THEME-SPECIFIC CONFIGURATION 7 | theme_settings: 8 | title: Faraday 9 | avatar: assets/img/logo.png 10 | favicon: assets/img/favicon.png 11 | # email: your-email@example.com 12 | description: >- 13 | Simple, but flexible HTTP client library, with support for multiple backends 14 | footer_text: "© 2009 - 2023, the Faraday Team. Website and branding design by Elena Lo Piccolo." 15 | 16 | # Icons 17 | github: 'lostisland/faraday' 18 | gitter: 'lostisland/faraday' 19 | 20 | # Post navigation 21 | post_navigation: true 22 | site_navigation_sort: 'order' 23 | 24 | # BUILD SETTINGS 25 | markdown: kramdown 26 | remote_theme: rohanchandra/type-theme 27 | 28 | plugins: 29 | - jekyll-feed 30 | - jekyll-remote-theme 31 | 32 | # GitHub settings 33 | lsi: false 34 | safe: true 35 | #source: [your repo's top level directory] 36 | incremental: false 37 | highlighter: rouge 38 | gist: 39 | noscript: false 40 | kramdown: 41 | math_engine: mathjax 42 | syntax_highlighter: rouge 43 | 44 | # Exclude from processing. 45 | # The following items will not be processed, by default. Create a custom list 46 | # to override the default setting. 47 | exclude: 48 | - Gemfile 49 | - Gemfile.lock 50 | - README.md 51 | - node_modules 52 | - vendor/bundle/ 53 | - vendor/cache/ 54 | - vendor/gems/ 55 | - vendor/ruby/ 56 | -------------------------------------------------------------------------------- /docs/middleware/request/instrumentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: "Instrumentation Middleware" 4 | permalink: /middleware/instrumentation 5 | hide: true 6 | prev_name: JSON Request Middleware 7 | prev_link: ./json-request 8 | next_name: JSON Response Middleware 9 | next_link: ./json-response 10 | top_name: Back to Middleware 11 | top_link: ./list 12 | --- 13 | 14 | The `Instrumentation` middleware allows to instrument requests using different tools. 15 | Options for this middleware include the instrumentation `name` and the `instrumenter` you want to use. 16 | They default to `request.faraday` and `ActiveSupport::Notifications` respectively, but you can provide your own: 17 | 18 | ```ruby 19 | conn = Faraday.new(...) do |f| 20 | f.request :instrumentation, name: 'custom_name', instrumenter: MyInstrumenter 21 | ... 22 | end 23 | ``` 24 | 25 | ### Example Usage 26 | 27 | The `Instrumentation` middleware will use `ActiveSupport::Notifications` by default as instrumenter, 28 | allowing you to subscribe to the default event name and instrument requests: 29 | 30 | ```ruby 31 | conn = Faraday.new('http://example.com') do |f| 32 | f.request :instrumentation 33 | ... 34 | end 35 | 36 | ActiveSupport::Notifications.subscribe('request.faraday') do |name, starts, ends, _, env| 37 | url = env[:url] 38 | http_method = env[:method].to_s.upcase 39 | duration = ends - starts 40 | $stdout.puts '[%s] %s %s (%.3f s)' % [url.host, http_method, url.request_uri, duration] 41 | end 42 | 43 | conn.get('/search', { a: 1, b: 2 }) 44 | #=> [example.com] GET /search?a=1&b=2 (0.529 s) 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/usage/streaming.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: "Streaming Responses" 4 | permalink: /usage/streaming 5 | hide: true 6 | top_name: Usage 7 | top_link: ./ 8 | prev_name: Customizing the Request 9 | prev_link: ./customize 10 | --- 11 | 12 | Sometimes you might need to receive a streaming response. 13 | You can do this with the `on_data` request option. 14 | 15 | The `on_data` callback is a receives tuples of chunk Strings, and the total 16 | of received bytes so far. 17 | 18 | This example implements such a callback: 19 | 20 | ```ruby 21 | # A buffer to store the streamed data 22 | streamed = [] 23 | 24 | conn.get('/stream/10') do |req| 25 | # Set a callback which will receive tuples of chunk Strings, 26 | # the sum of characters received so far, and the response environment. 27 | # The latter will allow access to the response status, headers and reason, as well as the request info. 28 | req.options.on_data = Proc.new do |chunk, overall_received_bytes, env| 29 | puts "Received #{overall_received_bytes} characters" 30 | streamed << chunk 31 | end 32 | end 33 | 34 | # Joins all response chunks together 35 | streamed.join 36 | ``` 37 | 38 | The `on_data` streaming is currently only supported by some adapters. 39 | To see which ones, please refer to [Awesome Faraday][awesome] comparative table or check the adapter documentation. 40 | Moreover, the `env` parameter was only recently added, which means some adapters may only have partial support 41 | (i.e. only `chunk` and `overall_received_bytes` will be passed to your block). 42 | 43 | [awesome]: https://github.com/lostisland/awesome-faraday/#adapters 44 | 45 | -------------------------------------------------------------------------------- /spec/faraday/options/proxy_options_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Faraday::ProxyOptions do 4 | describe '#from' do 5 | it 'works with string' do 6 | options = Faraday::ProxyOptions.from 'http://user:pass@example.org' 7 | expect(options.user).to eq('user') 8 | expect(options.password).to eq('pass') 9 | expect(options.uri).to be_a_kind_of(URI) 10 | expect(options.path).to eq('') 11 | expect(options.port).to eq(80) 12 | expect(options.host).to eq('example.org') 13 | expect(options.scheme).to eq('http') 14 | expect(options.inspect).to match('#') 28 | end 29 | 30 | it 'works with no auth' do 31 | proxy = Faraday::ProxyOptions.from 'http://example.org' 32 | expect(proxy.user).to be_nil 33 | expect(proxy.password).to be_nil 34 | end 35 | end 36 | 37 | it 'allows hash access' do 38 | proxy = Faraday::ProxyOptions.from 'http://a%40b:pw%20d@example.org' 39 | expect(proxy.user).to eq('a@b') 40 | expect(proxy[:user]).to eq('a@b') 41 | expect(proxy.password).to eq('pw d') 42 | expect(proxy[:password]).to eq('pw d') 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /docs/assets/js/team.js: -------------------------------------------------------------------------------- 1 | function teamTile(member) { 2 | console.log(member); 3 | return ''; 11 | } 12 | 13 | function fetchTeam(json, team, div) { 14 | let el = document.querySelector(div); 15 | el.innerHTML = team.map(function (m) { 16 | let index = json.findIndex(function(e) { 17 | return e.author.login === m 18 | }); 19 | return teamTile(json.splice(index, 1)[0]); 20 | }).join(''); 21 | } 22 | 23 | function fetchContributors(json) { 24 | let el = document.querySelector('#contributors-list'); 25 | el.innerHTML = json.reverse().map(function (c) { 26 | return '' + c.author.login + ''; 27 | }).join(' · '); 28 | } 29 | 30 | function hideLoader() { 31 | let el = document.querySelector('#loader'); 32 | el.classList.add('hidden'); 33 | } 34 | 35 | function showTeam() { 36 | let el = document.querySelector('#team-content'); 37 | el.classList.remove('hidden'); 38 | } 39 | 40 | fetch('https://api.github.com/repos/lostisland/faraday/stats/contributors') 41 | .then(function (response) { 42 | response.json().then(function (json) { 43 | fetchTeam(json, ['technoweenie', 'iMacTia', 'olleolleolle'], '#active-maintainers-list'); 44 | fetchTeam(json, ['mislav', 'sferik'], '#historical-team-list'); 45 | fetchContributors(json); 46 | hideLoader(); 47 | showTeam(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /lib/faraday/request/json.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | 5 | module Faraday 6 | class Request 7 | # Request middleware that encodes the body as JSON. 8 | # 9 | # Processes only requests with matching Content-type or those without a type. 10 | # If a request doesn't have a type but has a body, it sets the Content-type 11 | # to JSON MIME-type. 12 | # 13 | # Doesn't try to encode bodies that already are in string form. 14 | class Json < Middleware 15 | MIME_TYPE = 'application/json' 16 | MIME_TYPE_REGEX = %r{^application/(vnd\..+\+)?json$}.freeze 17 | 18 | def on_request(env) 19 | match_content_type(env) do |data| 20 | env[:body] = encode(data) 21 | end 22 | end 23 | 24 | private 25 | 26 | def encode(data) 27 | ::JSON.generate(data) 28 | end 29 | 30 | def match_content_type(env) 31 | return unless process_request?(env) 32 | 33 | env[:request_headers][CONTENT_TYPE] ||= MIME_TYPE 34 | yield env[:body] unless env[:body].respond_to?(:to_str) 35 | end 36 | 37 | def process_request?(env) 38 | type = request_type(env) 39 | body?(env) && (type.empty? || type.match?(MIME_TYPE_REGEX)) 40 | end 41 | 42 | def body?(env) 43 | (body = env[:body]) && !(body.respond_to?(:to_str) && body.empty?) 44 | end 45 | 46 | def request_type(env) 47 | type = env[:request_headers][CONTENT_TYPE].to_s 48 | type = type.split(';', 2).first if type.index(';') 49 | type 50 | end 51 | end 52 | end 53 | end 54 | 55 | Faraday::Request.register_middleware(json: Faraday::Request::Json) 56 | -------------------------------------------------------------------------------- /lib/faraday/response/json.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | 5 | module Faraday 6 | class Response 7 | # Parse response bodies as JSON. 8 | class Json < Middleware 9 | def initialize(app = nil, parser_options: nil, content_type: /\bjson$/, preserve_raw: false) 10 | super(app) 11 | @parser_options = parser_options 12 | @content_types = Array(content_type) 13 | @preserve_raw = preserve_raw 14 | end 15 | 16 | def on_complete(env) 17 | process_response(env) if parse_response?(env) 18 | end 19 | 20 | private 21 | 22 | def process_response(env) 23 | env[:raw_body] = env[:body] if @preserve_raw 24 | env[:body] = parse(env[:body]) 25 | rescue StandardError, SyntaxError => e 26 | raise Faraday::ParsingError.new(e, env[:response]) 27 | end 28 | 29 | def parse(body) 30 | ::JSON.parse(body, @parser_options || {}) unless body.strip.empty? 31 | end 32 | 33 | def parse_response?(env) 34 | process_response_type?(env) && 35 | env[:body].respond_to?(:to_str) 36 | end 37 | 38 | def process_response_type?(env) 39 | type = response_type(env) 40 | @content_types.empty? || @content_types.any? do |pattern| 41 | pattern.is_a?(Regexp) ? type.match?(pattern) : type == pattern 42 | end 43 | end 44 | 45 | def response_type(env) 46 | type = env[:response_headers][CONTENT_TYPE].to_s 47 | type = type.split(';', 2).first if type.index(';') 48 | type 49 | end 50 | end 51 | end 52 | end 53 | 54 | Faraday::Response.register_middleware(json: Faraday::Response::Json) 55 | -------------------------------------------------------------------------------- /faraday.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/faraday/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'faraday' 7 | spec.version = Faraday::VERSION 8 | 9 | spec.summary = 'HTTP/REST API client library.' 10 | 11 | spec.authors = ['@technoweenie', '@iMacTia', '@olleolleolle'] 12 | spec.email = 'technoweenie@gmail.com' 13 | spec.homepage = 'https://lostisland.github.io/faraday' 14 | spec.licenses = ['MIT'] 15 | 16 | spec.required_ruby_version = '>= 2.6' 17 | 18 | # faraday-net_http is the "default adapter", but being a Faraday dependency it can't 19 | # control which version of faraday it will be pulled from. 20 | # To avoid releasing a major version every time there's a new Faraday API, we should 21 | # always fix its required version to the next MINOR version. 22 | # This way, we can release minor versions of the adapter with "breaking" changes for older versions of Faraday 23 | # and then bump the version requirement on the next compatible version of faraday. 24 | spec.add_dependency 'faraday-net_http', '>= 2.0', '< 3.1' 25 | spec.add_dependency 'ruby2_keywords', '>= 0.0.4' 26 | 27 | # Includes `examples` and `spec` to allow external adapter gems to run Faraday unit and integration tests 28 | spec.files = Dir['CHANGELOG.md', '{examples,lib,spec}/**/*', 'LICENSE.md', 'Rakefile', 'README.md'] 29 | spec.require_paths = %w[lib spec/external_adapters] 30 | spec.metadata = { 31 | 'homepage_uri' => 'https://lostisland.github.io/faraday', 32 | 'changelog_uri' => 33 | "https://github.com/lostisland/faraday/releases/tag/v#{spec.version}", 34 | 'source_code_uri' => 'https://github.com/lostisland/faraday', 35 | 'bug_tracker_uri' => 'https://github.com/lostisland/faraday/issues' 36 | } 37 | end 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | 6 | push: 7 | branches: [ main, 1.x, 0.1x ] 8 | 9 | env: 10 | GIT_COMMIT_SHA: ${{ github.sha }} 11 | GIT_BRANCH: ${{ github.ref }} 12 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 13 | 14 | permissions: 15 | contents: read # to fetch code (actions/checkout) 16 | 17 | jobs: 18 | linting: 19 | runs-on: ubuntu-latest 20 | env: 21 | BUNDLE_WITH: lint 22 | BUNDLE_WITHOUT: development:test 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | 27 | - name: Set up Ruby 2.7 28 | uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: 2.7 31 | bundler-cache: true 32 | 33 | - name: Rubocop 34 | run: bundle exec rubocop --format progress 35 | 36 | - name: Yard-Junk 37 | run: bundle exec yard-junk --path lib 38 | 39 | build: 40 | needs: [ linting ] 41 | runs-on: ubuntu-latest 42 | name: build ${{ matrix.ruby }} 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | ruby: [ '2.6', '2.7', '3.0', '3.1', '3.2' ] 47 | experimental: [false] 48 | include: 49 | - ruby: head 50 | experimental: true 51 | - ruby: truffleruby-head 52 | experimental: true 53 | 54 | steps: 55 | - uses: actions/checkout@v3 56 | - uses: ruby/setup-ruby@v1 57 | with: 58 | ruby-version: ${{ matrix.ruby }} 59 | bundler-cache: true 60 | 61 | - name: RSpec 62 | continue-on-error: ${{ matrix.experimental }} 63 | run: bundle exec rake 64 | 65 | - name: Test External Adapters 66 | if: ${{ matrix.ruby != '2.6' }} 67 | continue-on-error: ${{ matrix.experimental }} 68 | run: bundle exec bake test:external 69 | 70 | 71 | -------------------------------------------------------------------------------- /docs/middleware/response/raise_error.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: "Raise Error Middleware" 4 | permalink: /middleware/raise-error 5 | hide: true 6 | prev_name: Logger Middleware 7 | prev_link: ./logger 8 | top_name: Back to Middleware 9 | top_link: ./list 10 | --- 11 | 12 | The `RaiseError` middleware raises a `Faraday::Error` exception if an HTTP 13 | response returns with a 4xx or 5xx status code. All exceptions are initialized 14 | providing the response `status`, `headers`, and `body`. 15 | 16 | ```ruby 17 | conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| 18 | faraday.response :raise_error # raise Faraday::Error on status code 4xx or 5xx 19 | end 20 | 21 | begin 22 | conn.get('/wrong-url') # => Assume this raises a 404 response 23 | rescue Faraday::ResourceNotFound => e 24 | e.response[:status] #=> 404 25 | e.response[:headers] #=> { ... } 26 | e.response[:body] #=> "..." 27 | end 28 | ``` 29 | 30 | Specific exceptions are raised based on the HTTP Status code, according to the list below: 31 | 32 | An HTTP status in the 400-499 range typically represents an error 33 | by the client. They raise error classes inheriting from `Faraday::ClientError`. 34 | 35 | * 400 => `Faraday::BadRequestError` 36 | * 401 => `Faraday::UnauthorizedError` 37 | * 403 => `Faraday::ForbiddenError` 38 | * 404 => `Faraday::ResourceNotFound` 39 | * 407 => `Faraday::ProxyAuthError` 40 | * 409 => `Faraday::ConflictError` 41 | * 422 => `Faraday::UnprocessableEntityError` 42 | * 4xx => `Faraday::ClientError` 43 | 44 | An HTTP status in the 500-599 range represents a server error, and raises a 45 | `Faraday::ServerError` exception. 46 | 47 | * 5xx => `Faraday::ServerError` 48 | 49 | The HTTP response status may be nil due to a malformed HTTP response from the 50 | server, or a bug in the underlying HTTP library. It inherits from 51 | `Faraday::ServerError`. 52 | 53 | * nil => `Faraday::NilStatusError` 54 | -------------------------------------------------------------------------------- /lib/faraday/request/url_encoded.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | class Request 5 | # Middleware for supporting urlencoded requests. 6 | class UrlEncoded < Faraday::Middleware 7 | unless defined?(::Faraday::Request::UrlEncoded::CONTENT_TYPE) 8 | CONTENT_TYPE = 'Content-Type' 9 | end 10 | 11 | class << self 12 | attr_accessor :mime_type 13 | end 14 | self.mime_type = 'application/x-www-form-urlencoded' 15 | 16 | # Encodes as "application/x-www-form-urlencoded" if not already encoded or 17 | # of another type. 18 | # 19 | # @param env [Faraday::Env] 20 | def call(env) 21 | match_content_type(env) do |data| 22 | params = Faraday::Utils::ParamsHash[data] 23 | env.body = params.to_query(env.params_encoder) 24 | end 25 | @app.call env 26 | end 27 | 28 | # @param env [Faraday::Env] 29 | # @yield [request_body] Body of the request 30 | def match_content_type(env) 31 | return unless process_request?(env) 32 | 33 | env.request_headers[CONTENT_TYPE] ||= self.class.mime_type 34 | return if env.body.respond_to?(:to_str) || env.body.respond_to?(:read) 35 | 36 | yield(env.body) 37 | end 38 | 39 | # @param env [Faraday::Env] 40 | # 41 | # @return [Boolean] True if the request has a body and its Content-Type is 42 | # urlencoded. 43 | def process_request?(env) 44 | type = request_type(env) 45 | env.body && (type.empty? || (type == self.class.mime_type)) 46 | end 47 | 48 | # @param env [Faraday::Env] 49 | # 50 | # @return [String] 51 | def request_type(env) 52 | type = env.request_headers[CONTENT_TYPE].to_s 53 | type = type.split(';', 2).first if type.index(';') 54 | type 55 | end 56 | end 57 | end 58 | end 59 | 60 | Faraday::Request.register_middleware(url_encoded: Faraday::Request::UrlEncoded) 61 | -------------------------------------------------------------------------------- /spec/faraday/middleware_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Faraday::Middleware do 4 | subject { described_class.new(app) } 5 | let(:app) { double } 6 | 7 | describe 'options' do 8 | context 'when options are passed to the middleware' do 9 | subject { described_class.new(app, options) } 10 | let(:options) { { field: 'value' } } 11 | 12 | it 'accepts options when initialized' do 13 | expect(subject.options[:field]).to eq('value') 14 | end 15 | end 16 | end 17 | 18 | describe '#on_request' do 19 | subject do 20 | Class.new(described_class) do 21 | def on_request(env) 22 | # do nothing 23 | end 24 | end.new(app) 25 | end 26 | 27 | it 'is called by #call' do 28 | expect(app).to receive(:call).and_return(app) 29 | expect(app).to receive(:on_complete) 30 | is_expected.to receive(:call).and_call_original 31 | is_expected.to receive(:on_request) 32 | subject.call(double) 33 | end 34 | end 35 | 36 | describe '#on_error' do 37 | subject do 38 | Class.new(described_class) do 39 | def on_error(error) 40 | # do nothing 41 | end 42 | end.new(app) 43 | end 44 | 45 | it 'is called by #call' do 46 | expect(app).to receive(:call).and_raise(Faraday::ConnectionFailed) 47 | is_expected.to receive(:call).and_call_original 48 | is_expected.to receive(:on_error) 49 | 50 | expect { subject.call(double) }.to raise_error(Faraday::ConnectionFailed) 51 | end 52 | end 53 | 54 | describe '#close' do 55 | context "with app that doesn't support \#close" do 56 | it 'should issue warning' do 57 | is_expected.to receive(:warn) 58 | subject.close 59 | end 60 | end 61 | 62 | context "with app that supports \#close" do 63 | it 'should issue warning' do 64 | expect(app).to receive(:close) 65 | is_expected.to_not receive(:warn) 66 | subject.close 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2022-08-08 14:26:32 UTC using RuboCop version 1.33.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 6 10 | # Configuration parameters: AllowedMethods. 11 | # AllowedMethods: enums 12 | Lint/ConstantDefinitionInBlock: 13 | Exclude: 14 | - 'spec/faraday/options/options_spec.rb' 15 | - 'spec/faraday/rack_builder_spec.rb' 16 | - 'spec/faraday/request/instrumentation_spec.rb' 17 | 18 | # Offense count: 11 19 | # Configuration parameters: AllowComments, AllowEmptyLambdas. 20 | Lint/EmptyBlock: 21 | Exclude: 22 | - 'spec/faraday/connection_spec.rb' 23 | - 'spec/faraday/rack_builder_spec.rb' 24 | - 'spec/faraday/response_spec.rb' 25 | 26 | # Offense count: 12 27 | # Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods, CountRepeatedAttributes. 28 | Metrics/AbcSize: 29 | Max: 42 30 | 31 | # Offense count: 4 32 | # Configuration parameters: CountComments, CountAsOne. 33 | Metrics/ClassLength: 34 | Max: 230 35 | 36 | # Offense count: 9 37 | # Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. 38 | Metrics/CyclomaticComplexity: 39 | Max: 13 40 | 41 | # Offense count: 26 42 | # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods. 43 | Metrics/MethodLength: 44 | Max: 33 45 | 46 | # Offense count: 1 47 | # Configuration parameters: CountKeywordArgs, MaxOptionalParameters. 48 | Metrics/ParameterLists: 49 | Max: 6 50 | 51 | # Offense count: 6 52 | # Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. 53 | Metrics/PerceivedComplexity: 54 | Max: 14 55 | 56 | # Offense count: 3 57 | Style/DocumentDynamicEvalDefinition: 58 | Exclude: 59 | - 'lib/faraday/connection.rb' 60 | - 'lib/faraday/options.rb' 61 | -------------------------------------------------------------------------------- /lib/faraday/request/instrumentation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | class Request 5 | # Middleware for instrumenting Requests. 6 | class Instrumentation < Faraday::Middleware 7 | # Options class used in Request::Instrumentation class. 8 | Options = Faraday::Options.new(:name, :instrumenter) do 9 | # @return [String] 10 | def name 11 | self[:name] ||= 'request.faraday' 12 | end 13 | 14 | # @return [Class] 15 | def instrumenter 16 | self[:instrumenter] ||= ActiveSupport::Notifications 17 | end 18 | end 19 | 20 | # Instruments requests using Active Support. 21 | # 22 | # Measures time spent only for synchronous requests. 23 | # 24 | # @example Using ActiveSupport::Notifications to measure time spent 25 | # for Faraday requests. 26 | # ActiveSupport::Notifications 27 | # .subscribe('request.faraday') do |name, starts, ends, _, env| 28 | # url = env[:url] 29 | # http_method = env[:method].to_s.upcase 30 | # duration = ends - starts 31 | # $stderr.puts '[%s] %s %s (%.3f s)' % 32 | # [url.host, http_method, url.request_uri, duration] 33 | # end 34 | # @param app [#call] 35 | # @param options [nil, Hash] Options hash 36 | # @option options [String] :name ('request.faraday') 37 | # Name of the instrumenter 38 | # @option options [Class] :instrumenter (ActiveSupport::Notifications) 39 | # Active Support instrumenter class. 40 | def initialize(app, options = nil) 41 | super(app) 42 | @name, @instrumenter = Options.from(options) 43 | .values_at(:name, :instrumenter) 44 | end 45 | 46 | # @param env [Faraday::Env] 47 | def call(env) 48 | @instrumenter.instrument(@name, env) do 49 | @app.call(env) 50 | end 51 | end 52 | end 53 | end 54 | end 55 | 56 | Faraday::Request.register_middleware(instrumentation: Faraday::Request::Instrumentation) 57 | -------------------------------------------------------------------------------- /lib/faraday/request/authorization.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | class Request 5 | # Request middleware for the Authorization HTTP header 6 | class Authorization < Faraday::Middleware 7 | KEY = 'Authorization' 8 | 9 | # @param app [#call] 10 | # @param type [String, Symbol] Type of Authorization 11 | # @param params [Array] parameters to build the Authorization header. 12 | # If the type is `:basic`, then these can be a login and password pair. 13 | # Otherwise, a single value is expected that will be appended after the type. 14 | # This value can be a proc or an object responding to `.call`, in which case 15 | # it will be invoked on each request. 16 | def initialize(app, type, *params) 17 | @type = type 18 | @params = params 19 | super(app) 20 | end 21 | 22 | # @param env [Faraday::Env] 23 | def on_request(env) 24 | return if env.request_headers[KEY] 25 | 26 | env.request_headers[KEY] = header_from(@type, env, *@params) 27 | end 28 | 29 | private 30 | 31 | # @param type [String, Symbol] 32 | # @param env [Faraday::Env] 33 | # @param params [Array] 34 | # @return [String] a header value 35 | def header_from(type, env, *params) 36 | if type.to_s.casecmp('basic').zero? && params.size == 2 37 | Utils.basic_header_from(*params) 38 | elsif params.size != 1 39 | raise ArgumentError, "Unexpected params received (got #{params.size} instead of 1)" 40 | else 41 | value = params.first 42 | if (value.is_a?(Proc) && value.arity == 1) || (value.respond_to?(:call) && value.method(:call).arity == 1) 43 | value = value.call(env) 44 | elsif value.is_a?(Proc) || value.respond_to?(:call) 45 | value = value.call 46 | end 47 | "#{type} #{value}" 48 | end 49 | end 50 | end 51 | end 52 | end 53 | 54 | Faraday::Request.register_middleware(authorization: Faraday::Request::Authorization) 55 | -------------------------------------------------------------------------------- /spec/faraday/options/env_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Faraday::Env do 4 | subject(:env) { described_class.new } 5 | 6 | it 'allows to access members' do 7 | expect(env.method).to be_nil 8 | env.method = :get 9 | expect(env.method).to eq(:get) 10 | end 11 | 12 | it 'allows to access symbol non members' do 13 | expect(env[:custom]).to be_nil 14 | env[:custom] = :boom 15 | expect(env[:custom]).to eq(:boom) 16 | end 17 | 18 | it 'allows to access string non members' do 19 | expect(env['custom']).to be_nil 20 | env['custom'] = :boom 21 | expect(env['custom']).to eq(:boom) 22 | end 23 | 24 | it 'ignores false when fetching' do 25 | ssl = Faraday::SSLOptions.new 26 | ssl.verify = false 27 | expect(ssl.fetch(:verify, true)).to be_falsey 28 | end 29 | 30 | it 'handle verify_hostname when fetching' do 31 | ssl = Faraday::SSLOptions.new 32 | ssl.verify_hostname = true 33 | expect(ssl.fetch(:verify_hostname, false)).to be_truthy 34 | end 35 | 36 | it 'retains custom members' do 37 | env[:foo] = 'custom 1' 38 | env[:bar] = :custom2 39 | env2 = Faraday::Env.from(env) 40 | env2[:baz] = 'custom 3' 41 | 42 | expect(env2[:foo]).to eq('custom 1') 43 | expect(env2[:bar]).to eq(:custom2) 44 | expect(env[:baz]).to be_nil 45 | end 46 | 47 | describe '#body' do 48 | subject(:env) { described_class.from(body: { foo: 'bar' }) } 49 | 50 | context 'when response is not finished yet' do 51 | it 'returns the request body' do 52 | expect(env.body).to eq(foo: 'bar') 53 | end 54 | end 55 | 56 | context 'when response is finished' do 57 | before do 58 | env.status = 200 59 | env.body = { bar: 'foo' } 60 | env.response = Faraday::Response.new(env) 61 | end 62 | 63 | it 'returns the response body' do 64 | expect(env.body).to eq(bar: 'foo') 65 | end 66 | 67 | it 'allows to access request_body' do 68 | expect(env.request_body).to eq(foo: 'bar') 69 | end 70 | 71 | it 'allows to access response_body' do 72 | expect(env.response_body).to eq(bar: 'foo') 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /docs/middleware/request/authentication.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: "Authentication Middleware" 4 | permalink: /middleware/authentication 5 | hide: true 6 | next_name: UrlEncoded Middleware 7 | next_link: ./url-encoded 8 | top_name: Back to Middleware 9 | top_link: ./list 10 | --- 11 | 12 | The `Faraday::Request::Authorization` middleware allows you to automatically add an `Authorization` header 13 | to your requests. It also features a handy helper to manage Basic authentication. 14 | **Please note the way you use this middleware in Faraday 1.x is different**, 15 | examples are available at the bottom of this page. 16 | 17 | ```ruby 18 | Faraday.new(...) do |conn| 19 | conn.request :authorization, 'Bearer', 'authentication-token' 20 | end 21 | ``` 22 | 23 | ### With a proc 24 | 25 | You can also provide a proc, which will be evaluated on each request: 26 | 27 | ```ruby 28 | Faraday.new(...) do |conn| 29 | conn.request :authorization, 'Bearer', -> { MyAuthStorage.get_auth_token } 30 | end 31 | ``` 32 | 33 | If the proc takes an argument, it will receive the forwarded `env` 34 | 35 | ```ruby 36 | Faraday.new(...) do |conn| 37 | conn.request :authorization, 'Bearer', ->(env) { MyAuthStorage.get_auth_token(env) } 38 | end 39 | ``` 40 | 41 | ### Basic Authentication 42 | 43 | The middleware will automatically Base64 encode your Basic username and password: 44 | 45 | ```ruby 46 | Faraday.new(...) do |conn| 47 | conn.request :authorization, :basic, 'username', 'password' 48 | end 49 | ``` 50 | 51 | ### Faraday 1.x usage 52 | 53 | In Faraday 1.x, the way you use this middleware is slightly different: 54 | 55 | ```ruby 56 | # Basic Auth request 57 | # Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= 58 | Faraday.new(...) do |conn| 59 | conn.request :basic_auth, 'username', 'password' 60 | end 61 | 62 | # Token Auth request 63 | # `options` are automatically converted into `key=value` format 64 | # Authorization: Token authentication-token 65 | Faraday.new(...) do |conn| 66 | conn.request :token_auth, 'authentication-token', **options 67 | end 68 | 69 | # Generic Auth Request 70 | # Authorization: Bearer authentication-token 71 | Faraday.new(...) do |conn| 72 | conn.request :authorization, 'Bearer', 'authentication-token' 73 | end 74 | ``` 75 | -------------------------------------------------------------------------------- /lib/faraday/response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'forwardable' 4 | 5 | module Faraday 6 | # Response represents an HTTP response from making an HTTP request. 7 | class Response 8 | extend Forwardable 9 | extend MiddlewareRegistry 10 | 11 | def initialize(env = nil) 12 | @env = Env.from(env) if env 13 | @on_complete_callbacks = [] 14 | end 15 | 16 | attr_reader :env 17 | 18 | def status 19 | finished? ? env.status : nil 20 | end 21 | 22 | def reason_phrase 23 | finished? ? env.reason_phrase : nil 24 | end 25 | 26 | def headers 27 | finished? ? env.response_headers : {} 28 | end 29 | 30 | def_delegator :headers, :[] 31 | 32 | def body 33 | finished? ? env.body : nil 34 | end 35 | 36 | def finished? 37 | !!env 38 | end 39 | 40 | def on_complete(&block) 41 | if finished? 42 | yield(env) 43 | else 44 | @on_complete_callbacks << block 45 | end 46 | self 47 | end 48 | 49 | def finish(env) 50 | raise 'response already finished' if finished? 51 | 52 | @env = env.is_a?(Env) ? env : Env.from(env) 53 | @on_complete_callbacks.each { |callback| callback.call(@env) } 54 | self 55 | end 56 | 57 | def success? 58 | finished? && env.success? 59 | end 60 | 61 | def to_hash 62 | { 63 | status: env.status, body: env.body, 64 | response_headers: env.response_headers, 65 | url: env.url 66 | } 67 | end 68 | 69 | # because @on_complete_callbacks cannot be marshalled 70 | def marshal_dump 71 | finished? ? to_hash : nil 72 | end 73 | 74 | def marshal_load(env) 75 | @env = Env.from(env) 76 | end 77 | 78 | # Expand the env with more properties, without overriding existing ones. 79 | # Useful for applying request params after restoring a marshalled Response. 80 | def apply_request(request_env) 81 | raise "response didn't finish yet" unless finished? 82 | 83 | @env = Env.from(request_env).update(@env) 84 | self 85 | end 86 | end 87 | end 88 | 89 | require 'faraday/response/json' 90 | require 'faraday/response/logger' 91 | require 'faraday/response/raise_error' 92 | -------------------------------------------------------------------------------- /docs/_includes/header.html: -------------------------------------------------------------------------------- 1 | 3 | 60 | -------------------------------------------------------------------------------- /spec/faraday/request/instrumentation_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Faraday::Request::Instrumentation do 4 | class FakeInstrumenter 5 | attr_reader :instrumentations 6 | 7 | def initialize 8 | @instrumentations = [] 9 | end 10 | 11 | def instrument(name, env) 12 | @instrumentations << [name, env] 13 | yield 14 | end 15 | end 16 | 17 | let(:config) { {} } 18 | let(:options) { Faraday::Request::Instrumentation::Options.from config } 19 | let(:instrumenter) { FakeInstrumenter.new } 20 | let(:conn) do 21 | Faraday.new do |f| 22 | f.request :instrumentation, config.merge(instrumenter: instrumenter) 23 | f.adapter :test do |stub| 24 | stub.get '/' do 25 | [200, {}, 'ok'] 26 | end 27 | end 28 | end 29 | end 30 | 31 | it { expect(options.name).to eq('request.faraday') } 32 | it 'defaults to ActiveSupport::Notifications' do 33 | res = options.instrumenter 34 | rescue NameError => e 35 | expect(e.to_s).to match('ActiveSupport') 36 | else 37 | expect(res).to eq(ActiveSupport::Notifications) 38 | end 39 | 40 | it 'instruments with default name' do 41 | expect(instrumenter.instrumentations.size).to eq(0) 42 | 43 | res = conn.get '/' 44 | expect(res.body).to eq('ok') 45 | expect(instrumenter.instrumentations.size).to eq(1) 46 | 47 | name, env = instrumenter.instrumentations.first 48 | expect(name).to eq('request.faraday') 49 | expect(env[:url].path).to eq('/') 50 | end 51 | 52 | context 'with custom name' do 53 | let(:config) { { name: 'custom' } } 54 | 55 | it { expect(options.name).to eq('custom') } 56 | it 'instruments with custom name' do 57 | expect(instrumenter.instrumentations.size).to eq(0) 58 | 59 | res = conn.get '/' 60 | expect(res.body).to eq('ok') 61 | expect(instrumenter.instrumentations.size).to eq(1) 62 | 63 | name, env = instrumenter.instrumentations.first 64 | expect(name).to eq('custom') 65 | expect(env[:url].path).to eq('/') 66 | end 67 | end 68 | 69 | context 'with custom instrumenter' do 70 | let(:config) { { instrumenter: :custom } } 71 | 72 | it { expect(options.instrumenter).to eq(:custom) } 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /docs/middleware/list.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: "Available Middleware" 4 | permalink: /middleware/list 5 | hide: true 6 | top_name: Middleware 7 | top_link: ./ 8 | next_name: Writing Middleware 9 | next_link: ./custom 10 | --- 11 | 12 | Faraday ships with some useful middleware that you can use to customize your request/response lifecycle. 13 | Middleware are separated into two macro-categories: **Request Middleware** and **Response Middleware**. 14 | The former usually deal with the request, encoding the parameters or setting headers. 15 | The latter instead activate after the request is completed and a response has been received, like 16 | parsing the response body, logging useful info or checking the response status. 17 | 18 | ### Request Middleware 19 | 20 | **Request middleware** can modify Request details before the Adapter runs. Most 21 | middleware set Header values or transform the request body based on the 22 | content type. 23 | 24 | * [`BasicAuthentication`][authentication] sets the `Authorization` header to the `user:password` 25 | base64 representation. 26 | * [`TokenAuthentication`][authentication] sets the `Authorization` header to the specified token. 27 | * [`UrlEncoded`][url_encoded] converts a `Faraday::Request#body` hash of key/value pairs into a url-encoded request body. 28 | * [`Json Request`][json-request] converts a `Faraday::Request#body` hash of key/value pairs into a JSON request body. 29 | * [`Instrumentation`][instrumentation] allows to instrument requests using different tools. 30 | 31 | 32 | ### Response Middleware 33 | 34 | **Response middleware** receives the response from the adapter and can modify its details 35 | before returning it. 36 | 37 | * [`Json Response`][json-response] parses response body into a hash of key/value pairs. 38 | * [`Logger`][logger] logs both the request and the response body and headers. 39 | * [`RaiseError`][raise_error] checks the response HTTP code and raises an exception if it is a 4xx or 5xx code. 40 | 41 | 42 | [authentication]: ./authentication 43 | [url_encoded]: ./url-encoded 44 | [json-request]: ./json-request 45 | [instrumentation]: ./instrumentation 46 | [json-response]: ./json-response 47 | [logger]: ./logger 48 | [raise_error]: ./raise-error 49 | -------------------------------------------------------------------------------- /spec/support/helper_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | module HelperMethods 5 | def self.included(base) 6 | base.extend ClassMethods 7 | end 8 | 9 | module ClassMethods 10 | def features(*features) 11 | @features = features 12 | end 13 | 14 | def on_feature(name) 15 | yield if block_given? && feature?(name) 16 | end 17 | 18 | def feature?(name) 19 | if @features.nil? 20 | superclass.feature?(name) if superclass.respond_to?(:feature?) 21 | elsif @features.include?(name) 22 | true 23 | end 24 | end 25 | 26 | def method_with_body?(method) 27 | METHODS_WITH_BODY.include?(method.to_s) 28 | end 29 | end 30 | 31 | def ssl_mode? 32 | ENV['SSL'] == 'yes' 33 | end 34 | 35 | def normalize(url) 36 | Faraday::Utils::URI(url) 37 | end 38 | 39 | def with_default_uri_parser(parser) 40 | old_parser = Faraday::Utils.default_uri_parser 41 | begin 42 | Faraday::Utils.default_uri_parser = parser 43 | yield 44 | ensure 45 | Faraday::Utils.default_uri_parser = old_parser 46 | end 47 | end 48 | 49 | def with_env(new_env) 50 | old_env = {} 51 | 52 | new_env.each do |key, value| 53 | old_env[key] = ENV.fetch(key, false) 54 | ENV[key] = value 55 | end 56 | 57 | begin 58 | yield 59 | ensure 60 | old_env.each do |key, value| 61 | value == false ? ENV.delete(key) : ENV[key] = value 62 | end 63 | end 64 | end 65 | 66 | def with_env_proxy_disabled 67 | Faraday.ignore_env_proxy = true 68 | 69 | begin 70 | yield 71 | ensure 72 | Faraday.ignore_env_proxy = false 73 | end 74 | end 75 | 76 | def capture_warnings 77 | old = $stderr 78 | $stderr = StringIO.new 79 | begin 80 | yield 81 | $stderr.string 82 | ensure 83 | $stderr = old 84 | end 85 | end 86 | 87 | def method_with_body?(method) 88 | self.class.method_with_body?(method) 89 | end 90 | 91 | def big_string 92 | kb = 1024 93 | (32..126).map(&:chr).cycle.take(50 * kb).join 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /docs/adapters/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: "Adapters" 4 | permalink: /adapters/ 5 | order: 2 6 | --- 7 | 8 | The Faraday Adapter interface determines how a Faraday request is turned into 9 | a Faraday response object. Adapters are typically implemented with common Ruby 10 | HTTP clients, but can have custom implementations. Adapters can be configured 11 | either globally or per Faraday Connection through the configuration block. 12 | 13 | For example, consider using `httpclient` as an adapter. Note that [faraday-httpclient](https://github.com/lostisland/faraday-httpclient) must be installed beforehand. 14 | 15 | If you want to configure it globally, do the following: 16 | 17 | ```ruby 18 | require 'faraday' 19 | require 'faraday/httpclient' 20 | 21 | Faraday.default_adapter = :httpclient 22 | ``` 23 | 24 | If you want to configure it per Faraday Connection, do the following: 25 | 26 | ```ruby 27 | require 'faraday' 28 | require 'faraday/httpclient' 29 | 30 | conn = Faraday.new do |f| 31 | f.adapter :httpclient 32 | end 33 | ``` 34 | 35 | {: .mt-60} 36 | ## Fantastic adapters and where to find them 37 | 38 | With the only exception being the [Test Adapter][testing], which is for _test purposes only_, 39 | adapters are distributed separately from Faraday. 40 | They are usually available as gems, or bundled with HTTP clients. 41 | 42 | While most adapters use a common Ruby HTTP client library, adapters can also 43 | have completely custom implementations. 44 | 45 | If you're just getting started you can find a list of featured adapters in [Awesome Faraday][awesome]. 46 | Anyone can create a Faraday adapter and distribute it. If you're interested learning more, check how to [build your own][build_adapters]! 47 | 48 | ## Ad-hoc adapters customization 49 | 50 | Faraday is intended to be a generic interface between your code and the adapter. 51 | However, sometimes you need to access a feature specific to one of the adapters that is not covered in Faraday's interface. 52 | When that happens, you can pass a block when specifying the adapter to customize it. 53 | The block parameter will change based on the adapter you're using. See each adapter page for more details. 54 | 55 | [testing]: ./testing 56 | [awesome]: https://github.com/lostisland/awesome-faraday/#adapters 57 | [build_adapters]: ./write_your_adapter.md 58 | -------------------------------------------------------------------------------- /lib/faraday/response/raise_error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | class Response 5 | # RaiseError is a Faraday middleware that raises exceptions on common HTTP 6 | # client or server error responses. 7 | class RaiseError < Middleware 8 | # rubocop:disable Naming/ConstantName 9 | ClientErrorStatuses = (400...500).freeze 10 | ServerErrorStatuses = (500...600).freeze 11 | # rubocop:enable Naming/ConstantName 12 | 13 | def on_complete(env) 14 | case env[:status] 15 | when 400 16 | raise Faraday::BadRequestError, response_values(env) 17 | when 401 18 | raise Faraday::UnauthorizedError, response_values(env) 19 | when 403 20 | raise Faraday::ForbiddenError, response_values(env) 21 | when 404 22 | raise Faraday::ResourceNotFound, response_values(env) 23 | when 407 24 | # mimic the behavior that we get with proxy requests with HTTPS 25 | msg = %(407 "Proxy Authentication Required") 26 | raise Faraday::ProxyAuthError.new(msg, response_values(env)) 27 | when 409 28 | raise Faraday::ConflictError, response_values(env) 29 | when 422 30 | raise Faraday::UnprocessableEntityError, response_values(env) 31 | when ClientErrorStatuses 32 | raise Faraday::ClientError, response_values(env) 33 | when ServerErrorStatuses 34 | raise Faraday::ServerError, response_values(env) 35 | when nil 36 | raise Faraday::NilStatusError, response_values(env) 37 | end 38 | end 39 | 40 | def response_values(env) 41 | { 42 | status: env.status, 43 | headers: env.response_headers, 44 | body: env.body, 45 | request: { 46 | method: env.method, 47 | url: env.url, 48 | url_path: env.url.path, 49 | params: query_params(env), 50 | headers: env.request_headers, 51 | body: env.request_body 52 | } 53 | } 54 | end 55 | 56 | def query_params(env) 57 | env.request.params_encoder ||= Faraday::Utils.default_params_encoder 58 | env.params_encoder.decode(env.url.query) 59 | end 60 | end 61 | end 62 | end 63 | 64 | Faraday::Response.register_middleware(raise_error: Faraday::Response::RaiseError) 65 | -------------------------------------------------------------------------------- /lib/faraday/middleware_registry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'monitor' 4 | 5 | module Faraday 6 | # Adds the ability for other modules to register and lookup 7 | # middleware classes. 8 | module MiddlewareRegistry 9 | def registered_middleware 10 | @registered_middleware ||= {} 11 | end 12 | 13 | # Register middleware class(es) on the current module. 14 | # 15 | # @param mappings [Hash] Middleware mappings from a lookup symbol to a middleware class. 16 | # @return [void] 17 | # 18 | # @example Lookup by a constant 19 | # 20 | # module Faraday 21 | # class Whatever < Middleware 22 | # # Middleware looked up by :foo returns Faraday::Whatever::Foo. 23 | # register_middleware(foo: Whatever) 24 | # end 25 | # end 26 | def register_middleware(**mappings) 27 | middleware_mutex do 28 | registered_middleware.update(mappings) 29 | end 30 | end 31 | 32 | # Unregister a previously registered middleware class. 33 | # 34 | # @param key [Symbol] key for the registered middleware. 35 | def unregister_middleware(key) 36 | registered_middleware.delete(key) 37 | end 38 | 39 | # Lookup middleware class with a registered Symbol shortcut. 40 | # 41 | # @param key [Symbol] key for the registered middleware. 42 | # @return [Class] a middleware Class. 43 | # @raise [Faraday::Error] if given key is not registered 44 | # 45 | # @example 46 | # 47 | # module Faraday 48 | # class Whatever < Middleware 49 | # register_middleware(foo: Whatever) 50 | # end 51 | # end 52 | # 53 | # Faraday::Middleware.lookup_middleware(:foo) 54 | # # => Faraday::Whatever 55 | def lookup_middleware(key) 56 | load_middleware(key) || 57 | raise(Faraday::Error, "#{key.inspect} is not registered on #{self}") 58 | end 59 | 60 | private 61 | 62 | def middleware_mutex(&block) 63 | @middleware_mutex ||= Monitor.new 64 | @middleware_mutex.synchronize(&block) 65 | end 66 | 67 | def load_middleware(key) 68 | value = registered_middleware[key] 69 | case value 70 | when Module 71 | value 72 | when Symbol, String 73 | middleware_mutex do 74 | @registered_middleware[key] = const_get(value) 75 | end 76 | when Proc 77 | middleware_mutex do 78 | @registered_middleware[key] = value.call 79 | end 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /docs/middleware/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: "Writing Middleware" 4 | permalink: /middleware/custom 5 | hide: true 6 | top_name: Middleware 7 | top_link: ./ 8 | prev_name: Available Middleware 9 | prev_link: ./list 10 | --- 11 | 12 | Middleware are classes that implement a `#call` instance method. They hook into the request/response cycle. 13 | 14 | ```ruby 15 | def call(request_env) 16 | # do something with the request 17 | # request_env[:request_headers].merge!(...) 18 | 19 | @app.call(request_env).on_complete do |response_env| 20 | # do something with the response 21 | # response_env[:response_headers].merge!(...) 22 | end 23 | end 24 | ``` 25 | 26 | It's important to do all processing of the response only in the `#on_complete` 27 | block. This enables middleware to work in parallel mode where requests are 28 | asynchronous. 29 | 30 | The `env` is a hash with symbol keys that contains info about the request and, 31 | later, response. Some keys are: 32 | 33 | ``` 34 | # request phase 35 | :method - :get, :post, ... 36 | :url - URI for the current request; also contains GET parameters 37 | :body - POST parameters for :post/:put requests 38 | :request_headers 39 | 40 | # response phase 41 | :status - HTTP response status code, such as 200 42 | :body - the response body 43 | :response_headers 44 | ``` 45 | 46 | ### Faraday::Middleware 47 | 48 | There's an easier way to write middleware, and it's also the recommended one: make your middleware subclass `Faraday::Middleware`. 49 | `Faraday::Middleware` already implements the `#call` method for you and looks for two methods in your subclass: `#on_request(env)` and `#on_complete(env)`. 50 | `#on_request` is called when the request is being built and is given the `env` representing the request. 51 | 52 | `#on_complete` is called after the response has been received (that's right, it already supports parallel mode!) and receives the `env` of the response. 53 | 54 | ### Do I need to override `#call`? 55 | 56 | For the majority of middleware, it's not necessary to override the `#call` method. You can instead use `#on_request` and `#on_complete`. 57 | 58 | However, in some cases you may need to wrap the call in a block, or work around it somehow (think of a begin-rescue, for example). 59 | When that happens, then you can override `#call`. When you do so, remember to call either `app.call(env)` or `super` to avoid breaking the middleware stack call! 60 | 61 | ### Can I find a middleware template somewhere? 62 | 63 | Yes, you can! Look at the [`faraday-middleware-template`](https://github.com/lostisland/faraday-middleware-template) repository. 64 | -------------------------------------------------------------------------------- /spec/faraday/error_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Faraday::ClientError do 4 | describe '.initialize' do 5 | subject { described_class.new(exception, response) } 6 | let(:response) { nil } 7 | 8 | context 'with exception only' do 9 | let(:exception) { RuntimeError.new('test') } 10 | 11 | it { expect(subject.wrapped_exception).to eq(exception) } 12 | it { expect(subject.response).to be_nil } 13 | it { expect(subject.message).to eq(exception.message) } 14 | it { expect(subject.backtrace).to eq(exception.backtrace) } 15 | it { expect(subject.inspect).to eq('#>') } 16 | it { expect(subject.response_status).to be_nil } 17 | end 18 | 19 | context 'with response hash' do 20 | let(:exception) { { status: 400 } } 21 | 22 | it { expect(subject.wrapped_exception).to be_nil } 23 | it { expect(subject.response).to eq(exception) } 24 | it { expect(subject.message).to eq('the server responded with status 400') } 25 | it { expect(subject.inspect).to eq('#400}>') } 26 | it { expect(subject.response_status).to eq(400) } 27 | end 28 | 29 | context 'with string' do 30 | let(:exception) { 'custom message' } 31 | 32 | it { expect(subject.wrapped_exception).to be_nil } 33 | it { expect(subject.response).to be_nil } 34 | it { expect(subject.message).to eq('custom message') } 35 | it { expect(subject.inspect).to eq('#>') } 36 | it { expect(subject.response_status).to be_nil } 37 | end 38 | 39 | context 'with anything else #to_s' do 40 | let(:exception) { %w[error1 error2] } 41 | 42 | it { expect(subject.wrapped_exception).to be_nil } 43 | it { expect(subject.response).to be_nil } 44 | it { expect(subject.message).to eq('["error1", "error2"]') } 45 | it { expect(subject.inspect).to eq('#>') } 46 | it { expect(subject.response_status).to be_nil } 47 | end 48 | 49 | context 'with exception string and response hash' do 50 | let(:exception) { 'custom message' } 51 | let(:response) { { status: 400 } } 52 | 53 | it { expect(subject.wrapped_exception).to be_nil } 54 | it { expect(subject.response).to eq(response) } 55 | it { expect(subject.message).to eq('custom message') } 56 | it { expect(subject.inspect).to eq('#400}>') } 57 | it { expect(subject.response_status).to eq(400) } 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/faraday/response_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Faraday::Response do 4 | subject { Faraday::Response.new(env) } 5 | 6 | let(:env) do 7 | Faraday::Env.from(status: 404, body: 'yikes', url: Faraday::Utils.URI('https://lostisland.github.io/faraday'), 8 | response_headers: { 'Content-Type' => 'text/plain' }) 9 | end 10 | 11 | it { expect(subject.finished?).to be_truthy } 12 | it { expect { subject.finish({}) }.to raise_error(RuntimeError) } 13 | it { expect(subject.success?).to be_falsey } 14 | it { expect(subject.status).to eq(404) } 15 | it { expect(subject.body).to eq('yikes') } 16 | it { expect(subject.headers['Content-Type']).to eq('text/plain') } 17 | it { expect(subject['content-type']).to eq('text/plain') } 18 | 19 | describe '#apply_request' do 20 | before { subject.apply_request(body: 'a=b', method: :post) } 21 | 22 | it { expect(subject.body).to eq('yikes') } 23 | it { expect(subject.env[:method]).to eq(:post) } 24 | end 25 | 26 | describe '#to_hash' do 27 | let(:hash) { subject.to_hash } 28 | 29 | it { expect(hash).to be_a(Hash) } 30 | it { expect(hash[:status]).to eq(subject.status) } 31 | it { expect(hash[:response_headers]).to eq(subject.headers) } 32 | it { expect(hash[:body]).to eq(subject.body) } 33 | it { expect(hash[:url]).to eq(subject.env.url) } 34 | end 35 | 36 | describe 'marshal serialization support' do 37 | subject { Faraday::Response.new } 38 | let(:loaded) { Marshal.load(Marshal.dump(subject)) } 39 | 40 | before do 41 | subject.on_complete {} 42 | subject.finish(env.merge(params: 'moo')) 43 | end 44 | 45 | it { expect(loaded.env[:params]).to be_nil } 46 | it { expect(loaded.env[:body]).to eq(env[:body]) } 47 | it { expect(loaded.env[:response_headers]).to eq(env[:response_headers]) } 48 | it { expect(loaded.env[:status]).to eq(env[:status]) } 49 | it { expect(loaded.env[:url]).to eq(env[:url]) } 50 | end 51 | 52 | describe '#on_complete' do 53 | subject { Faraday::Response.new } 54 | 55 | it 'parse body on finish' do 56 | subject.on_complete { |env| env[:body] = env[:body].upcase } 57 | subject.finish(env) 58 | 59 | expect(subject.body).to eq('YIKES') 60 | end 61 | 62 | it 'can access response body in on_complete callback' do 63 | subject.on_complete { |env| env[:body] = subject.body.upcase } 64 | subject.finish(env) 65 | 66 | expect(subject.body).to eq('YIKES') 67 | end 68 | 69 | it 'can access response body in on_complete callback' do 70 | callback_env = nil 71 | subject.on_complete { |env| callback_env = env } 72 | subject.finish({}) 73 | 74 | expect(subject.env).to eq(callback_env) 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /docs/_sass/faraday.sass: -------------------------------------------------------------------------------- 1 | // Custom Styles added on top of the theme. 2 | 3 | .btn 4 | display: inline-block 5 | background-color: $link-color 6 | padding: 5px 10px 7 | box-shadow: 0 4px 10px 5px rgba(238, 66, 102, 0.30) 8 | border-radius: 20px 9 | width: 200px 10 | color: #FFFFFF 11 | letter-spacing: -0.41px 12 | text-align: center 13 | margin: 0 10px 14 | 15 | &:hover 16 | background-color: darken($link-color, 10%) 17 | color: white 18 | text-decoration: none 19 | 20 | .text-center 21 | text-align: center 22 | 23 | .mt-60 24 | margin-top: 60px 25 | 26 | .hidden 27 | display: none 28 | 29 | .docs-nav 30 | display: flex 31 | margin-top: 40px 32 | 33 | .docs-nav-item 34 | flex: 1 1 0 35 | text-align: center 36 | 37 | pre.highlight 38 | padding: 20px 39 | background-color: #F6F6F6 40 | border-radius: 4px 41 | 42 | code 43 | word-wrap: normal 44 | overflow: scroll 45 | 46 | code.highlighter-rouge 47 | background-color: #EEE 48 | padding: 0 5px 49 | border-radius: 3px 50 | 51 | .site-header .site-nav li 52 | margin-right: 1.2em 53 | 54 | h1, h2, h3, h4, h5, h6 55 | font-weight: bold 56 | 57 | .feature-image header 58 | @media (max-width: 1000px) 59 | padding: 7% 12.5% 60 | @media (max-width: 576px) 61 | padding: 4% 5% 1% 5% 62 | 63 | #team-content 64 | h3 65 | margin: 30px 0 66 | 67 | #contributors-list 68 | text-align: justify 69 | 70 | .team-tile 71 | width: 200px 72 | display: inline-block 73 | margin: 0 20px 74 | 75 | img 76 | width: 100% 77 | border-radius: 50% 78 | 79 | footer 80 | background-color: #f1f3f4 81 | 82 | #active-maintainers-list, #historical-team-list 83 | text-align: center 84 | 85 | #loader 86 | margin-top: 20% 87 | margin-bottom: 20% 88 | text-align: center 89 | 90 | .lds-ring 91 | display: inline-block 92 | position: relative 93 | width: 200px 94 | height: 200px 95 | 96 | .lds-ring div 97 | box-sizing: border-box 98 | display: block 99 | position: absolute 100 | width: 187px 101 | height: 187px 102 | margin: 6px 103 | border: 12px solid $link-color 104 | border-radius: 50% 105 | animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite 106 | border-color: $link-color transparent transparent transparent 107 | 108 | .lds-ring div:nth-child(1) 109 | animation-delay: -0.45s 110 | 111 | .lds-ring div:nth-child(2) 112 | animation-delay: -0.3s 113 | 114 | .lds-ring div:nth-child(3) 115 | animation-delay: -0.15s 116 | 117 | @keyframes lds-ring 118 | 0% 119 | transform: rotate(0deg) 120 | 121 | 100% 122 | transform: rotate(360deg) 123 | 124 | #search-form 125 | display: inline-block 126 | // You need a minimal window size for the search to display well, should be ok, people are on desktop 127 | // when needing search usually 128 | @media screen and (max-width: 800px) 129 | display: none 130 | -------------------------------------------------------------------------------- /lib/faraday/options/ssl_options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | # @!parse 5 | # # SSL-related options. 6 | # # 7 | # # @!attribute verify 8 | # # @return [Boolean] whether to verify SSL certificates or not 9 | # # 10 | # # @!attribute verify_hostname 11 | # # @return [Boolean] whether to enable hostname verification on server certificates 12 | # # during the handshake or not (see https://github.com/ruby/openssl/pull/60) 13 | # # 14 | # # @!attribute ca_file 15 | # # @return [String] CA file 16 | # # 17 | # # @!attribute ca_path 18 | # # @return [String] CA path 19 | # # 20 | # # @!attribute verify_mode 21 | # # @return [Integer] Any `OpenSSL::SSL::` constant (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL.html) 22 | # # 23 | # # @!attribute cert_store 24 | # # @return [OpenSSL::X509::Store] certificate store 25 | # # 26 | # # @!attribute client_cert 27 | # # @return [String, OpenSSL::X509::Certificate] client certificate 28 | # # 29 | # # @!attribute client_key 30 | # # @return [String, OpenSSL::PKey::RSA, OpenSSL::PKey::DSA] client key 31 | # # 32 | # # @!attribute certificate 33 | # # @return [OpenSSL::X509::Certificate] certificate (Excon only) 34 | # # 35 | # # @!attribute private_key 36 | # # @return [OpenSSL::PKey::RSA, OpenSSL::PKey::DSA] private key (Excon only) 37 | # # 38 | # # @!attribute verify_depth 39 | # # @return [Integer] maximum depth for the certificate chain verification 40 | # # 41 | # # @!attribute version 42 | # # @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) 43 | # # 44 | # # @!attribute min_version 45 | # # @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) 46 | # # 47 | # # @!attribute max_version 48 | # # @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) 49 | # class SSLOptions < Options; end 50 | SSLOptions = Options.new(:verify, :verify_hostname, 51 | :ca_file, :ca_path, :verify_mode, 52 | :cert_store, :client_cert, :client_key, 53 | :certificate, :private_key, :verify_depth, 54 | :version, :min_version, :max_version) do 55 | # @return [Boolean] true if should verify 56 | def verify? 57 | verify != false 58 | end 59 | 60 | # @return [Boolean] true if should not verify 61 | def disable? 62 | !verify? 63 | end 64 | 65 | # @return [Boolean] true if should verify_hostname 66 | def verify_hostname? 67 | verify_hostname != false 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/support/shared_examples/adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | shared_examples 'an adapter' do |**options| 4 | before { skip } if options[:skip] 5 | 6 | context 'with SSL enabled' do 7 | before { ENV['SSL'] = 'yes' } 8 | include_examples 'adapter examples', options 9 | end 10 | 11 | context 'with SSL disabled' do 12 | before { ENV['SSL'] = 'no' } 13 | include_examples 'adapter examples', options 14 | end 15 | end 16 | 17 | shared_examples 'adapter examples' do |**options| 18 | include Faraday::StreamingResponseChecker 19 | 20 | let(:adapter) { described_class.name.split('::').last } 21 | 22 | let(:conn_options) { { headers: { 'X-Faraday-Adapter' => adapter } }.merge(options[:conn_options] || {}) } 23 | 24 | let(:adapter_options) do 25 | return [] unless options[:adapter_options] 26 | 27 | if options[:adapter_options].is_a?(Array) 28 | options[:adapter_options] 29 | else 30 | [options[:adapter_options]] 31 | end 32 | end 33 | 34 | let(:protocol) { ssl_mode? ? 'https' : 'http' } 35 | let(:remote) { "#{protocol}://example.com" } 36 | let(:stub_remote) { remote } 37 | 38 | let(:conn) do 39 | conn_options[:ssl] ||= {} 40 | conn_options[:ssl][:ca_file] ||= ENV.fetch('SSL_FILE', nil) 41 | conn_options[:ssl][:verify_hostname] ||= ENV['SSL_VERIFY_HOSTNAME'] == 'yes' 42 | 43 | Faraday.new(remote, conn_options) do |conn| 44 | conn.request :url_encoded 45 | conn.response :raise_error 46 | conn.adapter described_class, *adapter_options 47 | end 48 | end 49 | 50 | let!(:request_stub) { stub_request(http_method, stub_remote) } 51 | 52 | after do 53 | expect(request_stub).to have_been_requested unless request_stub.disabled? 54 | end 55 | 56 | describe '#delete' do 57 | let(:http_method) { :delete } 58 | 59 | it_behaves_like 'a request method', :delete 60 | end 61 | 62 | describe '#get' do 63 | let(:http_method) { :get } 64 | 65 | it_behaves_like 'a request method', :get 66 | end 67 | 68 | describe '#head' do 69 | let(:http_method) { :head } 70 | 71 | it_behaves_like 'a request method', :head 72 | end 73 | 74 | describe '#options' do 75 | let(:http_method) { :options } 76 | 77 | it_behaves_like 'a request method', :options 78 | end 79 | 80 | describe '#patch' do 81 | let(:http_method) { :patch } 82 | 83 | it_behaves_like 'a request method', :patch 84 | end 85 | 86 | describe '#post' do 87 | let(:http_method) { :post } 88 | 89 | it_behaves_like 'a request method', :post 90 | end 91 | 92 | describe '#put' do 93 | let(:http_method) { :put } 94 | 95 | it_behaves_like 'a request method', :put 96 | end 97 | 98 | on_feature :trace_method do 99 | describe '#trace' do 100 | let(:http_method) { :trace } 101 | 102 | it_behaves_like 'a request method', :trace 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [![Faraday](./docs/assets/img/repo-card-slim.png)][website] 2 | 3 | [![Gem Version](https://badge.fury.io/rb/faraday.svg)](https://rubygems.org/gems/faraday) 4 | [![GitHub Actions CI](https://github.com/lostisland/faraday/workflows/CI/badge.svg)](https://github.com/lostisland/faraday/actions?query=workflow%3ACI) 5 | [![GitHub Discussions](https://img.shields.io/github/discussions/lostisland/faraday?logo=github)](https://github.com/lostisland/faraday/discussions) 6 | 7 | Faraday is an HTTP client library abstraction layer that provides a common interface over many 8 | adapters (such as Net::HTTP) and embraces the concept of Rack middleware when processing the request/response cycle. 9 | You probably don't want to use Faraday directly in your project, as it will lack an actual client library to perform 10 | requests. Instead, you probably want to have a look at [Awesome Faraday][awesome] for a list of available adapters. 11 | 12 | ## Getting Started 13 | 14 | The best starting point is the [Faraday Website][website], with its introduction and explanation. 15 | Need more details? See the [Faraday API Documentation][apidoc] to see how it works internally. 16 | 17 | ## Supported Ruby versions 18 | 19 | This library aims to support and is [tested against][actions] the currently officially supported Ruby 20 | implementations. This means that, even without a major release, we could add or drop support for Ruby versions, 21 | following their [EOL](https://endoflife.date/ruby). 22 | Currently that means we support Ruby 2.6+ 23 | 24 | If something doesn't work on one of these Ruby versions, it's a bug. 25 | 26 | This library may inadvertently work (or seem to work) on other Ruby 27 | implementations and versions, however support will only be provided for the versions listed 28 | above. 29 | 30 | If you would like this library to support another Ruby version, you may 31 | volunteer to be a maintainer. Being a maintainer entails making sure all tests 32 | run and pass on that implementation. When something breaks on your 33 | implementation, you will be responsible for providing patches in a timely 34 | fashion. If critical issues for a particular implementation exist at the time 35 | of a major release, support for that Ruby version may be dropped. 36 | 37 | ## Contribute 38 | 39 | Do you want to contribute to Faraday? 40 | Open the issues page and check for the `help wanted` label! 41 | But before you start coding, please read our [Contributing Guide][contributing] 42 | 43 | ## Copyright 44 | 45 | © 2009 - 2023, the [Faraday Team][faraday_team]. Website and branding design by [Elena Lo Piccolo](https://elelopic.design). 46 | 47 | [awesome]: https://github.com/lostisland/awesome-faraday/#adapters 48 | [website]: https://lostisland.github.io/faraday 49 | [faraday_team]: https://lostisland.github.io/faraday/team 50 | [contributing]: https://github.com/lostisland/faraday/blob/master/.github/CONTRIBUTING.md 51 | [apidoc]: https://www.rubydoc.info/github/lostisland/faraday 52 | [actions]: https://github.com/lostisland/faraday/actions 53 | [jruby]: http://jruby.org/ 54 | [rubinius]: http://rubini.us/ 55 | [license]: LICENSE.md 56 | -------------------------------------------------------------------------------- /spec/faraday/request/json_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Faraday::Request::Json do 4 | let(:middleware) { described_class.new(->(env) { Faraday::Response.new(env) }) } 5 | 6 | def process(body, content_type = nil) 7 | env = { body: body, request_headers: Faraday::Utils::Headers.new } 8 | env[:request_headers]['content-type'] = content_type if content_type 9 | middleware.call(Faraday::Env.from(env)).env 10 | end 11 | 12 | def result_body 13 | result[:body] 14 | end 15 | 16 | def result_type 17 | result[:request_headers]['content-type'] 18 | end 19 | 20 | context 'no body' do 21 | let(:result) { process(nil) } 22 | 23 | it "doesn't change body" do 24 | expect(result_body).to be_nil 25 | end 26 | 27 | it "doesn't add content type" do 28 | expect(result_type).to be_nil 29 | end 30 | end 31 | 32 | context 'empty body' do 33 | let(:result) { process('') } 34 | 35 | it "doesn't change body" do 36 | expect(result_body).to be_empty 37 | end 38 | 39 | it "doesn't add content type" do 40 | expect(result_type).to be_nil 41 | end 42 | end 43 | 44 | context 'string body' do 45 | let(:result) { process('{"a":1}') } 46 | 47 | it "doesn't change body" do 48 | expect(result_body).to eq('{"a":1}') 49 | end 50 | 51 | it 'adds content type' do 52 | expect(result_type).to eq('application/json') 53 | end 54 | end 55 | 56 | context 'object body' do 57 | let(:result) { process(a: 1) } 58 | 59 | it 'encodes body' do 60 | expect(result_body).to eq('{"a":1}') 61 | end 62 | 63 | it 'adds content type' do 64 | expect(result_type).to eq('application/json') 65 | end 66 | end 67 | 68 | context 'empty object body' do 69 | let(:result) { process({}) } 70 | 71 | it 'encodes body' do 72 | expect(result_body).to eq('{}') 73 | end 74 | end 75 | 76 | context 'object body with json type' do 77 | let(:result) { process({ a: 1 }, 'application/json; charset=utf-8') } 78 | 79 | it 'encodes body' do 80 | expect(result_body).to eq('{"a":1}') 81 | end 82 | 83 | it "doesn't change content type" do 84 | expect(result_type).to eq('application/json; charset=utf-8') 85 | end 86 | end 87 | 88 | context 'object body with vendor json type' do 89 | let(:result) { process({ a: 1 }, 'application/vnd.myapp.v1+json; charset=utf-8') } 90 | 91 | it 'encodes body' do 92 | expect(result_body).to eq('{"a":1}') 93 | end 94 | 95 | it "doesn't change content type" do 96 | expect(result_type).to eq('application/vnd.myapp.v1+json; charset=utf-8') 97 | end 98 | end 99 | 100 | context 'object body with incompatible type' do 101 | let(:result) { process({ a: 1 }, 'application/xml; charset=utf-8') } 102 | 103 | it "doesn't change body" do 104 | expect(result_body).to eq(a: 1) 105 | end 106 | 107 | it "doesn't change content type" do 108 | expect(result_type).to eq('application/xml; charset=utf-8') 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainer at giuffrida.mattia AT gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /lib/faraday/adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | # Base class for all Faraday adapters. Adapters are 5 | # responsible for fulfilling a Faraday request. 6 | class Adapter 7 | extend MiddlewareRegistry 8 | 9 | CONTENT_LENGTH = 'Content-Length' 10 | 11 | # This module marks an Adapter as supporting parallel requests. 12 | module Parallelism 13 | attr_writer :supports_parallel 14 | 15 | def supports_parallel? 16 | @supports_parallel 17 | end 18 | 19 | def inherited(subclass) 20 | super 21 | subclass.supports_parallel = supports_parallel? 22 | end 23 | end 24 | 25 | extend Parallelism 26 | self.supports_parallel = false 27 | 28 | def initialize(_app = nil, opts = {}, &block) 29 | @app = ->(env) { env.response } 30 | @connection_options = opts 31 | @config_block = block 32 | end 33 | 34 | # Yields or returns an adapter's configured connection. Depends on 35 | # #build_connection being defined on this adapter. 36 | # 37 | # @param env [Faraday::Env, Hash] The env object for a faraday request. 38 | # 39 | # @return The return value of the given block, or the HTTP connection object 40 | # if no block is given. 41 | def connection(env) 42 | conn = build_connection(env) 43 | return conn unless block_given? 44 | 45 | yield conn 46 | end 47 | 48 | # Close any persistent connections. The adapter should still be usable 49 | # after calling close. 50 | def close 51 | # Possible implementation: 52 | # @app.close if @app.respond_to?(:close) 53 | end 54 | 55 | def call(env) 56 | env.clear_body if env.needs_body? 57 | env.response = Response.new 58 | end 59 | 60 | private 61 | 62 | def save_response(env, status, body, headers = nil, reason_phrase = nil, finished: true) 63 | env.status = status 64 | env.body = body 65 | env.reason_phrase = reason_phrase&.to_s&.strip 66 | env.response_headers = Utils::Headers.new.tap do |response_headers| 67 | response_headers.update headers unless headers.nil? 68 | yield(response_headers) if block_given? 69 | end 70 | 71 | env.response.finish(env) unless env.parallel? || !finished 72 | env.response 73 | end 74 | 75 | # Fetches either a read, write, or open timeout setting. Defaults to the 76 | # :timeout value if a more specific one is not given. 77 | # 78 | # @param type [Symbol] Describes which timeout setting to get: :read, 79 | # :write, or :open. 80 | # @param options [Hash] Hash containing Symbol keys like :timeout, 81 | # :read_timeout, :write_timeout, or :open_timeout 82 | # 83 | # @return [Integer, nil] Timeout duration in seconds, or nil if no timeout 84 | # has been set. 85 | def request_timeout(type, options) 86 | key = TIMEOUT_KEYS.fetch(type) do 87 | msg = "Expected :read, :write, :open. Got #{type.inspect} :(" 88 | raise ArgumentError, msg 89 | end 90 | options[key] || options[:timeout] 91 | end 92 | 93 | TIMEOUT_KEYS = { 94 | read: :read_timeout, 95 | open: :open_timeout, 96 | write: :write_timeout 97 | }.freeze 98 | end 99 | end 100 | 101 | require 'faraday/adapter/test' 102 | -------------------------------------------------------------------------------- /lib/faraday/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'base64' 4 | require 'uri' 5 | require 'faraday/utils/headers' 6 | require 'faraday/utils/params_hash' 7 | 8 | module Faraday 9 | # Utils contains various static helper methods. 10 | module Utils 11 | module_function 12 | 13 | def build_query(params) 14 | FlatParamsEncoder.encode(params) 15 | end 16 | 17 | def build_nested_query(params) 18 | NestedParamsEncoder.encode(params) 19 | end 20 | 21 | def default_space_encoding 22 | @default_space_encoding ||= '+' 23 | end 24 | 25 | class << self 26 | attr_writer :default_space_encoding 27 | end 28 | 29 | ESCAPE_RE = /[^a-zA-Z0-9 .~_-]/.freeze 30 | 31 | def escape(str) 32 | str.to_s.gsub(ESCAPE_RE) do |match| 33 | "%#{match.unpack('H2' * match.bytesize).join('%').upcase}" 34 | end.gsub(' ', default_space_encoding) 35 | end 36 | 37 | def unescape(str) 38 | CGI.unescape str.to_s 39 | end 40 | 41 | DEFAULT_SEP = /[&;] */n.freeze 42 | 43 | # Adapted from Rack 44 | def parse_query(query) 45 | FlatParamsEncoder.decode(query) 46 | end 47 | 48 | def parse_nested_query(query) 49 | NestedParamsEncoder.decode(query) 50 | end 51 | 52 | def default_params_encoder 53 | @default_params_encoder ||= NestedParamsEncoder 54 | end 55 | 56 | def basic_header_from(login, pass) 57 | value = Base64.encode64("#{login}:#{pass}") 58 | value.delete!("\n") 59 | "Basic #{value}" 60 | end 61 | 62 | class << self 63 | attr_writer :default_params_encoder 64 | end 65 | 66 | # Normalize URI() behavior across Ruby versions 67 | # 68 | # url - A String or URI. 69 | # 70 | # Returns a parsed URI. 71 | def URI(url) # rubocop:disable Naming/MethodName 72 | if url.respond_to?(:host) 73 | url 74 | elsif url.respond_to?(:to_str) 75 | default_uri_parser.call(url) 76 | else 77 | raise ArgumentError, 'bad argument (expected URI object or URI string)' 78 | end 79 | end 80 | 81 | def default_uri_parser 82 | @default_uri_parser ||= Kernel.method(:URI) 83 | end 84 | 85 | def default_uri_parser=(parser) 86 | @default_uri_parser = if parser.respond_to?(:call) || parser.nil? 87 | parser 88 | else 89 | parser.method(:parse) 90 | end 91 | end 92 | 93 | # Receives a String or URI and returns just 94 | # the path with the query string sorted. 95 | def normalize_path(url) 96 | url = URI(url) 97 | (url.path.start_with?('/') ? url.path : "/#{url.path}") + 98 | (url.query ? "?#{sort_query_params(url.query)}" : '') 99 | end 100 | 101 | # Recursive hash update 102 | def deep_merge!(target, hash) 103 | hash.each do |key, value| 104 | target[key] = if value.is_a?(Hash) && (target[key].is_a?(Hash) || target[key].is_a?(Options)) 105 | deep_merge(target[key], value) 106 | else 107 | value 108 | end 109 | end 110 | target 111 | end 112 | 113 | # Recursive hash merge 114 | def deep_merge(source, hash) 115 | deep_merge!(source.dup, hash) 116 | end 117 | 118 | def sort_query_params(query) 119 | query.split('&').sort.join('&') 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /docs/usage/customize.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: "Customizing the Request" 4 | permalink: /usage/customize 5 | hide: true 6 | top_name: Usage 7 | top_link: ./ 8 | next_name: Streaming Responses 9 | next_link: ./streaming 10 | --- 11 | 12 | Configuration can be set up with the connection and/or adjusted per request. 13 | 14 | As connection options: 15 | 16 | ```ruby 17 | conn = Faraday.new('http://httpbingo.org', request: { timeout: 5 }) 18 | conn.get('/ip') 19 | ``` 20 | 21 | Or as per-request options: 22 | 23 | ```ruby 24 | conn.get do |req| 25 | req.url '/ip' 26 | req.options.timeout = 5 27 | end 28 | ``` 29 | 30 | You can also inject arbitrary data into the request using the `context` option. 31 | This will be available in the `env` on all middleware. 32 | 33 | ```ruby 34 | conn.get do |req| 35 | req.url '/get' 36 | req.options.context = { 37 | foo: 'foo', 38 | bar: 'bar' 39 | } 40 | end 41 | ``` 42 | 43 | ## Changing how parameters are serialized 44 | 45 | Sometimes you need to send the same URL parameter multiple times with different values. 46 | This requires manually setting the parameter encoder and can be done on 47 | either per-connection or per-request basis. 48 | This applies to all HTTP verbs. 49 | 50 | Per-connection setting: 51 | 52 | ```ruby 53 | conn = Faraday.new request: { params_encoder: Faraday::FlatParamsEncoder } 54 | conn.get('', { roll: ['california', 'philadelphia'] }) 55 | ``` 56 | 57 | Per-request setting: 58 | 59 | ```ruby 60 | conn.get do |req| 61 | req.options.params_encoder = Faraday::FlatParamsEncoder 62 | req.params = { roll: ['california', 'philadelphia'] } 63 | end 64 | ``` 65 | 66 | ### Custom serializers 67 | 68 | You can build your custom encoder, if you like. 69 | 70 | The value of Faraday `params_encoder` can be any object that responds to: 71 | 72 | * `#encode(hash) #=> String` 73 | * `#decode(string) #=> Hash` 74 | 75 | The encoder will affect both how Faraday processes query strings and how it 76 | serializes POST bodies. 77 | 78 | The default encoder is `Faraday::NestedParamsEncoder`. 79 | 80 | ### Order of parameters 81 | 82 | By default, parameters are sorted by name while being serialized. 83 | Since this is really useful to provide better cache management and most servers don't really care about parameters order, this is the default behaviour. 84 | However you might find yourself dealing with a server that requires parameters to be in a specific order. 85 | When that happens, you can configure the encoder to skip sorting them. 86 | This configuration is supported by both the default `Faraday::NestedParamsEncoder` and `Faraday::FlatParamsEncoder`: 87 | 88 | ```ruby 89 | Faraday::NestedParamsEncoder.sort_params = false 90 | # or 91 | Faraday::FlatParamsEncoder.sort_params = false 92 | ``` 93 | 94 | ## Proxy 95 | 96 | Faraday will try to automatically infer the proxy settings from your system using [`URI#find_proxy`][ruby-find-proxy]. 97 | This will retrieve them from environment variables such as http_proxy, ftp_proxy, no_proxy, etc. 98 | If for any reason you want to disable this behaviour, you can do so by setting the global variable `ignore_env_proxy`: 99 | 100 | ```ruby 101 | Faraday.ignore_env_proxy = true 102 | ``` 103 | 104 | You can also specify a custom proxy when initializing the connection: 105 | 106 | ```ruby 107 | conn = Faraday.new('http://www.example.com', proxy: 'http://proxy.com') 108 | ``` 109 | 110 | [ruby-find-proxy]: https://ruby-doc.org/stdlib-2.6.3/libdoc/uri/rdoc/URI/Generic.html#method-i-find_proxy 111 | -------------------------------------------------------------------------------- /spec/faraday/request/url_encoded_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'stringio' 4 | 5 | RSpec.describe Faraday::Request::UrlEncoded do 6 | let(:conn) do 7 | Faraday.new do |b| 8 | b.request :url_encoded 9 | b.adapter :test do |stub| 10 | stub.post('/echo') do |env| 11 | posted_as = env[:request_headers]['Content-Type'] 12 | body = env[:body] 13 | if body.respond_to?(:read) 14 | body = body.read 15 | end 16 | [200, { 'Content-Type' => posted_as }, body] 17 | end 18 | end 19 | end 20 | end 21 | 22 | it 'does nothing without payload' do 23 | response = conn.post('/echo') 24 | expect(response.headers['Content-Type']).to be_nil 25 | expect(response.body.empty?).to be_truthy 26 | end 27 | 28 | it 'ignores custom content type' do 29 | response = conn.post('/echo', { some: 'data' }, 'content-type' => 'application/x-foo') 30 | expect(response.headers['Content-Type']).to eq('application/x-foo') 31 | expect(response.body).to eq(some: 'data') 32 | end 33 | 34 | it 'works with no headers' do 35 | response = conn.post('/echo', fruit: %w[apples oranges]) 36 | expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded') 37 | expect(response.body).to eq('fruit%5B%5D=apples&fruit%5B%5D=oranges') 38 | end 39 | 40 | it 'works with with headers' do 41 | response = conn.post('/echo', { 'a' => 123 }, 'content-type' => 'application/x-www-form-urlencoded') 42 | expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded') 43 | expect(response.body).to eq('a=123') 44 | end 45 | 46 | it 'works with nested params' do 47 | response = conn.post('/echo', user: { name: 'Mislav', web: 'mislav.net' }) 48 | expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded') 49 | expected = { 'user' => { 'name' => 'Mislav', 'web' => 'mislav.net' } } 50 | expect(Faraday::Utils.parse_nested_query(response.body)).to eq(expected) 51 | end 52 | 53 | it 'works with non nested params' do 54 | response = conn.post('/echo', dimensions: %w[date location]) do |req| 55 | req.options.params_encoder = Faraday::FlatParamsEncoder 56 | end 57 | expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded') 58 | expected = { 'dimensions' => %w[date location] } 59 | expect(Faraday::Utils.parse_query(response.body)).to eq(expected) 60 | expect(response.body).to eq('dimensions=date&dimensions=location') 61 | end 62 | 63 | it 'works with unicode' do 64 | err = capture_warnings do 65 | response = conn.post('/echo', str: 'eé cç aã aâ') 66 | expect(response.body).to eq('str=e%C3%A9+c%C3%A7+a%C3%A3+a%C3%A2') 67 | end 68 | expect(err.empty?).to be_truthy 69 | end 70 | 71 | it 'works with nested keys' do 72 | response = conn.post('/echo', 'a' => { 'b' => { 'c' => ['d'] } }) 73 | expect(response.body).to eq('a%5Bb%5D%5Bc%5D%5B%5D=d') 74 | end 75 | 76 | it 'works with files' do 77 | response = conn.post('/echo', StringIO.new('str=apple')) 78 | expect(response.body).to eq('str=apple') 79 | end 80 | 81 | context 'customising default_space_encoding' do 82 | around do |example| 83 | Faraday::Utils.default_space_encoding = '%20' 84 | example.run 85 | Faraday::Utils.default_space_encoding = nil 86 | end 87 | 88 | it 'uses the custom character to encode spaces' do 89 | response = conn.post('/echo', str: 'apple banana') 90 | expect(response.body).to eq('str=apple%20banana') 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/faraday/encoders/flat_params_encoder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | # FlatParamsEncoder manages URI params as a flat hash. Any Array values repeat 5 | # the parameter multiple times. 6 | module FlatParamsEncoder 7 | class << self 8 | extend Forwardable 9 | def_delegators :'Faraday::Utils', :escape, :unescape 10 | end 11 | 12 | # Encode converts the given param into a URI querystring. Keys and values 13 | # will converted to strings and appropriately escaped for the URI. 14 | # 15 | # @param params [Hash] query arguments to convert. 16 | # 17 | # @example 18 | # 19 | # encode({a: %w[one two three], b: true, c: "C"}) 20 | # # => 'a=one&a=two&a=three&b=true&c=C' 21 | # 22 | # @return [String] the URI querystring (without the leading '?') 23 | def self.encode(params) 24 | return nil if params.nil? 25 | 26 | unless params.is_a?(Array) 27 | unless params.respond_to?(:to_hash) 28 | raise TypeError, 29 | "Can't convert #{params.class} into Hash." 30 | end 31 | params = params.to_hash 32 | params = params.map do |key, value| 33 | key = key.to_s if key.is_a?(Symbol) 34 | [key, value] 35 | end 36 | 37 | # Only to be used for non-Array inputs. Arrays should preserve order. 38 | params.sort! if @sort_params 39 | end 40 | 41 | # The params have form [['key1', 'value1'], ['key2', 'value2']]. 42 | buffer = +'' 43 | params.each do |key, value| 44 | encoded_key = escape(key) 45 | if value.nil? 46 | buffer << "#{encoded_key}&" 47 | elsif value.is_a?(Array) 48 | if value.empty? 49 | buffer << "#{encoded_key}=&" 50 | else 51 | value.each do |sub_value| 52 | encoded_value = escape(sub_value) 53 | buffer << "#{encoded_key}=#{encoded_value}&" 54 | end 55 | end 56 | else 57 | encoded_value = escape(value) 58 | buffer << "#{encoded_key}=#{encoded_value}&" 59 | end 60 | end 61 | buffer.chop 62 | end 63 | 64 | # Decode converts the given URI querystring into a hash. 65 | # 66 | # @param query [String] query arguments to parse. 67 | # 68 | # @example 69 | # 70 | # decode('a=one&a=two&a=three&b=true&c=C') 71 | # # => {"a"=>["one", "two", "three"], "b"=>"true", "c"=>"C"} 72 | # 73 | # @return [Hash] parsed keys and value strings from the querystring. 74 | def self.decode(query) 75 | return nil if query.nil? 76 | 77 | empty_accumulator = {} 78 | 79 | split_query = (query.split('&').map do |pair| 80 | pair.split('=', 2) if pair && !pair.empty? 81 | end).compact 82 | split_query.each_with_object(empty_accumulator.dup) do |pair, accu| 83 | pair[0] = unescape(pair[0]) 84 | pair[1] = true if pair[1].nil? 85 | if pair[1].respond_to?(:to_str) 86 | pair[1] = unescape(pair[1].to_str.tr('+', ' ')) 87 | end 88 | if accu[pair[0]].is_a?(Array) 89 | accu[pair[0]] << pair[1] 90 | elsif accu[pair[0]] 91 | accu[pair[0]] = [accu[pair[0]], pair[1]] 92 | else 93 | accu[pair[0]] = pair[1] 94 | end 95 | end 96 | end 97 | 98 | class << self 99 | attr_accessor :sort_params 100 | end 101 | 102 | # Useful default for OAuth and caching. 103 | @sort_params = true 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/faraday/response/json_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Faraday::Response::Json, type: :response do 4 | let(:options) { {} } 5 | let(:headers) { {} } 6 | let(:middleware) do 7 | described_class.new(lambda { |env| 8 | Faraday::Response.new(env) 9 | }, **options) 10 | end 11 | 12 | def process(body, content_type = 'application/json', options = {}) 13 | env = { 14 | body: body, request: options, 15 | request_headers: Faraday::Utils::Headers.new, 16 | response_headers: Faraday::Utils::Headers.new(headers) 17 | } 18 | env[:response_headers]['content-type'] = content_type if content_type 19 | yield(env) if block_given? 20 | middleware.call(Faraday::Env.from(env)) 21 | end 22 | 23 | context 'no type matching' do 24 | it "doesn't change nil body" do 25 | expect(process(nil).body).to be_nil 26 | end 27 | 28 | it 'nullifies empty body' do 29 | expect(process('').body).to be_nil 30 | end 31 | 32 | it 'parses json body' do 33 | response = process('{"a":1}') 34 | expect(response.body).to eq('a' => 1) 35 | expect(response.env[:raw_body]).to be_nil 36 | end 37 | end 38 | 39 | context 'with preserving raw' do 40 | let(:options) { { preserve_raw: true } } 41 | 42 | it 'parses json body' do 43 | response = process('{"a":1}') 44 | expect(response.body).to eq('a' => 1) 45 | expect(response.env[:raw_body]).to eq('{"a":1}') 46 | end 47 | end 48 | 49 | context 'with default regexp type matching' do 50 | it 'parses json body of correct type' do 51 | response = process('{"a":1}', 'application/x-json') 52 | expect(response.body).to eq('a' => 1) 53 | end 54 | 55 | it 'ignores json body of incorrect type' do 56 | response = process('{"a":1}', 'text/json-xml') 57 | expect(response.body).to eq('{"a":1}') 58 | end 59 | end 60 | 61 | context 'with array type matching' do 62 | let(:options) { { content_type: %w[a/b c/d] } } 63 | 64 | it 'parses json body of correct type' do 65 | expect(process('{"a":1}', 'a/b').body).to be_a(Hash) 66 | expect(process('{"a":1}', 'c/d').body).to be_a(Hash) 67 | end 68 | 69 | it 'ignores json body of incorrect type' do 70 | expect(process('{"a":1}', 'a/d').body).not_to be_a(Hash) 71 | end 72 | end 73 | 74 | it 'chokes on invalid json' do 75 | expect { process('{!') }.to raise_error(Faraday::ParsingError) 76 | end 77 | 78 | it 'includes the response on the ParsingError instance' do 79 | process('{') { |env| env[:response] = Faraday::Response.new } 80 | raise 'Parsing should have failed.' 81 | rescue Faraday::ParsingError => e 82 | expect(e.response).to be_a(Faraday::Response) 83 | end 84 | 85 | context 'HEAD responses' do 86 | it "nullifies the body if it's only one space" do 87 | response = process(' ') 88 | expect(response.body).to be_nil 89 | end 90 | 91 | it "nullifies the body if it's two spaces" do 92 | response = process(' ') 93 | expect(response.body).to be_nil 94 | end 95 | end 96 | 97 | context 'JSON options' do 98 | let(:body) { '{"a": 1}' } 99 | let(:result) { { a: 1 } } 100 | let(:options) do 101 | { 102 | parser_options: { 103 | symbolize_names: true 104 | } 105 | } 106 | end 107 | 108 | it 'passes relevant options to JSON parse' do 109 | expect(::JSON).to receive(:parse) 110 | .with(body, options[:parser_options]) 111 | .and_return(result) 112 | 113 | response = process(body) 114 | expect(response.body).to eq(result) 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/faraday/logging/formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pp' 4 | 5 | module Faraday 6 | module Logging 7 | # Serves as an integration point to customize logging 8 | class Formatter 9 | extend Forwardable 10 | 11 | DEFAULT_OPTIONS = { headers: true, bodies: false, errors: false, 12 | log_level: :info }.freeze 13 | 14 | def initialize(logger:, options:) 15 | @logger = logger 16 | @filter = [] 17 | @options = DEFAULT_OPTIONS.merge(options) 18 | end 19 | 20 | def_delegators :@logger, :debug, :info, :warn, :error, :fatal 21 | 22 | def request(env) 23 | request_log = proc do 24 | "#{env.method.upcase} #{apply_filters(env.url.to_s)}" 25 | end 26 | public_send(log_level, 'request', &request_log) 27 | 28 | log_headers('request', env.request_headers) if log_headers?(:request) 29 | log_body('request', env[:body]) if env[:body] && log_body?(:request) 30 | end 31 | 32 | def response(env) 33 | status = proc { "Status #{env.status}" } 34 | public_send(log_level, 'response', &status) 35 | 36 | log_headers('response', env.response_headers) if log_headers?(:response) 37 | log_body('response', env[:body]) if env[:body] && log_body?(:response) 38 | end 39 | 40 | def exception(exc) 41 | return unless log_errors? 42 | 43 | error_log = proc { exc.full_message } 44 | public_send(log_level, 'error', &error_log) 45 | 46 | log_headers('error', exc.response_headers) if exc.respond_to?(:response_headers) && log_headers?(:error) 47 | return unless exc.respond_to?(:response_body) && exc.response_body && log_body?(:error) 48 | 49 | log_body('error', exc.response_body) 50 | end 51 | 52 | def filter(filter_word, filter_replacement) 53 | @filter.push([filter_word, filter_replacement]) 54 | end 55 | 56 | private 57 | 58 | def dump_headers(headers) 59 | headers.map { |k, v| "#{k}: #{v.inspect}" }.join("\n") 60 | end 61 | 62 | def dump_body(body) 63 | if body.respond_to?(:to_str) 64 | body.to_str 65 | else 66 | pretty_inspect(body) 67 | end 68 | end 69 | 70 | def pretty_inspect(body) 71 | body.pretty_inspect 72 | end 73 | 74 | def log_headers?(type) 75 | case @options[:headers] 76 | when Hash 77 | @options[:headers][type] 78 | else 79 | @options[:headers] 80 | end 81 | end 82 | 83 | def log_body?(type) 84 | case @options[:bodies] 85 | when Hash 86 | @options[:bodies][type] 87 | else 88 | @options[:bodies] 89 | end 90 | end 91 | 92 | def log_errors? 93 | @options[:errors] 94 | end 95 | 96 | def apply_filters(output) 97 | @filter.each do |pattern, replacement| 98 | output = output.to_s.gsub(pattern, replacement) 99 | end 100 | output 101 | end 102 | 103 | def log_level 104 | unless %i[debug info warn error fatal].include?(@options[:log_level]) 105 | return :info 106 | end 107 | 108 | @options[:log_level] 109 | end 110 | 111 | def log_headers(type, headers) 112 | headers_log = proc { apply_filters(dump_headers(headers)) } 113 | public_send(log_level, type, &headers_log) 114 | end 115 | 116 | def log_body(type, body) 117 | body_log = proc { apply_filters(dump_body(body)) } 118 | public_send(log_level, type, &body_log) 119 | end 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /spec/faraday/utils_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Faraday::Utils do 4 | describe 'headers parsing' do 5 | let(:multi_response_headers) do 6 | "HTTP/1.x 500 OK\r\nContent-Type: text/html; charset=UTF-8\r\n" \ 7 | "HTTP/1.x 200 OK\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n" 8 | end 9 | 10 | it 'parse headers for aggregated responses' do 11 | headers = Faraday::Utils::Headers.new 12 | headers.parse(multi_response_headers) 13 | 14 | result = headers.to_hash 15 | 16 | expect(result['Content-Type']).to eq('application/json; charset=UTF-8') 17 | end 18 | end 19 | 20 | describe 'URI parsing' do 21 | let(:url) { 'http://example.com/abc' } 22 | 23 | it 'escapes safe buffer' do 24 | str = FakeSafeBuffer.new('$32,000.00') 25 | expect(Faraday::Utils.escape(str)).to eq('%2432%2C000.00') 26 | end 27 | 28 | it 'parses with default parser' do 29 | with_default_uri_parser(nil) do 30 | uri = normalize(url) 31 | expect(uri.host).to eq('example.com') 32 | end 33 | end 34 | 35 | it 'parses with URI' do 36 | with_default_uri_parser(::URI) do 37 | uri = normalize(url) 38 | expect(uri.host).to eq('example.com') 39 | end 40 | end 41 | 42 | it 'parses with block' do 43 | with_default_uri_parser(->(u) { "booya#{'!' * u.size}" }) do 44 | expect(normalize(url)).to eq('booya!!!!!!!!!!!!!!!!!!!!!!') 45 | end 46 | end 47 | 48 | it 'replaces headers hash' do 49 | headers = Faraday::Utils::Headers.new('authorization' => 't0ps3cr3t!') 50 | expect(headers).to have_key('authorization') 51 | 52 | headers.replace('content-type' => 'text/plain') 53 | expect(headers).not_to have_key('authorization') 54 | end 55 | end 56 | 57 | describe '.deep_merge!' do 58 | let(:connection_options) { Faraday::ConnectionOptions.new } 59 | let(:url) do 60 | { 61 | url: 'http://example.com/abc', 62 | headers: { 'Mime-Version' => '1.0' }, 63 | request: { oauth: { consumer_key: 'anonymous' } }, 64 | ssl: { version: '2' } 65 | } 66 | end 67 | 68 | it 'recursively merges the headers' do 69 | connection_options.headers = { user_agent: 'My Agent 1.0' } 70 | deep_merge = Faraday::Utils.deep_merge!(connection_options, url) 71 | 72 | expect(deep_merge.headers).to eq('Mime-Version' => '1.0', user_agent: 'My Agent 1.0') 73 | end 74 | 75 | context 'when a target hash has an Options Struct value' do 76 | let(:request) do 77 | { 78 | params_encoder: nil, 79 | proxy: nil, 80 | bind: nil, 81 | timeout: nil, 82 | open_timeout: nil, 83 | read_timeout: nil, 84 | write_timeout: nil, 85 | boundary: nil, 86 | oauth: { consumer_key: 'anonymous' }, 87 | context: nil, 88 | on_data: nil 89 | } 90 | end 91 | let(:ssl) do 92 | { 93 | verify: nil, 94 | ca_file: nil, 95 | ca_path: nil, 96 | verify_mode: nil, 97 | cert_store: nil, 98 | client_cert: nil, 99 | client_key: nil, 100 | certificate: nil, 101 | private_key: nil, 102 | verify_depth: nil, 103 | version: '2', 104 | min_version: nil, 105 | max_version: nil, 106 | verify_hostname: nil 107 | } 108 | end 109 | 110 | it 'does not overwrite an Options Struct value' do 111 | deep_merge = Faraday::Utils.deep_merge!(connection_options, url) 112 | 113 | expect(deep_merge.request.to_h).to eq(request) 114 | expect(deep_merge.ssl.to_h).to eq(ssl) 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /examples/client_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Requires Ruby with rspec and faraday gems. 4 | # rspec client_spec.rb 5 | 6 | require 'faraday' 7 | require 'json' 8 | 9 | # Example API client 10 | class Client 11 | def initialize(conn) 12 | @conn = conn 13 | end 14 | 15 | def httpbingo(jname, params: {}) 16 | res = @conn.get("/#{jname}", params) 17 | data = JSON.parse(res.body) 18 | data['origin'] 19 | end 20 | 21 | def foo(params) 22 | res = @conn.post('/foo', JSON.dump(params)) 23 | res.status 24 | end 25 | end 26 | 27 | RSpec.describe Client do 28 | let(:stubs) { Faraday::Adapter::Test::Stubs.new } 29 | let(:conn) { Faraday.new { |b| b.adapter(:test, stubs) } } 30 | let(:client) { Client.new(conn) } 31 | 32 | it 'parses origin' do 33 | stubs.get('/ip') do |env| 34 | # optional: you can inspect the Faraday::Env 35 | expect(env.url.path).to eq('/ip') 36 | [ 37 | 200, 38 | { 'Content-Type': 'application/javascript' }, 39 | '{"origin": "127.0.0.1"}' 40 | ] 41 | end 42 | 43 | # uncomment to trigger stubs.verify_stubbed_calls failure 44 | # stubs.get('/unused') { [404, {}, ''] } 45 | 46 | expect(client.httpbingo('ip')).to eq('127.0.0.1') 47 | stubs.verify_stubbed_calls 48 | end 49 | 50 | it 'handles 404' do 51 | stubs.get('/api') do 52 | [ 53 | 404, 54 | { 'Content-Type': 'application/javascript' }, 55 | '{}' 56 | ] 57 | end 58 | expect(client.httpbingo('api')).to be_nil 59 | stubs.verify_stubbed_calls 60 | end 61 | 62 | it 'handles exception' do 63 | stubs.get('/api') do 64 | raise Faraday::ConnectionFailed 65 | end 66 | 67 | expect { client.httpbingo('api') }.to raise_error(Faraday::ConnectionFailed) 68 | stubs.verify_stubbed_calls 69 | end 70 | 71 | context 'When the test stub is run in strict_mode' do 72 | let(:stubs) { Faraday::Adapter::Test::Stubs.new(strict_mode: true) } 73 | 74 | it 'verifies the all parameter values are identical' do 75 | stubs.get('/api?abc=123') do 76 | [ 77 | 200, 78 | { 'Content-Type': 'application/javascript' }, 79 | '{"origin": "127.0.0.1"}' 80 | ] 81 | end 82 | 83 | # uncomment to raise Stubs::NotFound 84 | # expect(client.httpbingo('api', params: { abc: 123, foo: 'Kappa' })).to eq('127.0.0.1') 85 | expect(client.httpbingo('api', params: { abc: 123 })).to eq('127.0.0.1') 86 | stubs.verify_stubbed_calls 87 | end 88 | end 89 | 90 | context 'When the Faraday connection is configured with FlatParamsEncoder' do 91 | let(:conn) { Faraday.new(request: { params_encoder: Faraday::FlatParamsEncoder }) { |b| b.adapter(:test, stubs) } } 92 | 93 | it 'handles the same multiple URL parameters' do 94 | stubs.get('/api?a=x&a=y&a=z') { [200, { 'Content-Type' => 'application/json' }, '{"origin": "127.0.0.1"}'] } 95 | 96 | # uncomment to raise Stubs::NotFound 97 | # expect(client.httpbingo('api', params: { a: %w[x y] })).to eq('127.0.0.1') 98 | expect(client.httpbingo('api', params: { a: %w[x y z] })).to eq('127.0.0.1') 99 | stubs.verify_stubbed_calls 100 | end 101 | end 102 | 103 | context 'When you want to test the body, you can use a proc as well as string' do 104 | it 'tests with a string' do 105 | stubs.post('/foo', '{"name":"YK"}') { [200, {}, ''] } 106 | 107 | expect(client.foo(name: 'YK')).to eq 200 108 | stubs.verify_stubbed_calls 109 | end 110 | 111 | it 'tests with a proc' do 112 | check = ->(request_body) { JSON.parse(request_body).slice('name') == { 'name' => 'YK' } } 113 | stubs.post('/foo', check) { [200, {}, ''] } 114 | 115 | expect(client.foo(name: 'YK', created_at: Time.now)).to eq 200 116 | stubs.verify_stubbed_calls 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/faraday/utils/headers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | module Utils 5 | # A case-insensitive Hash that preserves the original case of a header 6 | # when set. 7 | # 8 | # Adapted from Rack::Utils::HeaderHash 9 | class Headers < ::Hash 10 | def self.from(value) 11 | new(value) 12 | end 13 | 14 | def self.allocate 15 | new_self = super 16 | new_self.initialize_names 17 | new_self 18 | end 19 | 20 | def initialize(hash = nil) 21 | super() 22 | @names = {} 23 | update(hash || {}) 24 | end 25 | 26 | def initialize_names 27 | @names = {} 28 | end 29 | 30 | # on dup/clone, we need to duplicate @names hash 31 | def initialize_copy(other) 32 | super 33 | @names = other.names.dup 34 | end 35 | 36 | # need to synchronize concurrent writes to the shared KeyMap 37 | keymap_mutex = Mutex.new 38 | 39 | # symbol -> string mapper + cache 40 | KeyMap = Hash.new do |map, key| 41 | value = if key.respond_to?(:to_str) 42 | key 43 | else 44 | key.to_s.split('_') # user_agent: %w(user agent) 45 | .each(&:capitalize!) # => %w(User Agent) 46 | .join('-') # => "User-Agent" 47 | end 48 | keymap_mutex.synchronize { map[key] = value } 49 | end 50 | KeyMap[:etag] = 'ETag' 51 | 52 | def [](key) 53 | key = KeyMap[key] 54 | super(key) || super(@names[key.downcase]) 55 | end 56 | 57 | def []=(key, val) 58 | key = KeyMap[key] 59 | key = (@names[key.downcase] ||= key) 60 | # join multiple values with a comma 61 | val = val.to_ary.join(', ') if val.respond_to?(:to_ary) 62 | super(key, val) 63 | end 64 | 65 | def fetch(key, *args, &block) 66 | key = KeyMap[key] 67 | key = @names.fetch(key.downcase, key) 68 | super(key, *args, &block) 69 | end 70 | 71 | def delete(key) 72 | key = KeyMap[key] 73 | key = @names[key.downcase] 74 | return unless key 75 | 76 | @names.delete key.downcase 77 | super(key) 78 | end 79 | 80 | def include?(key) 81 | @names.include? key.downcase 82 | end 83 | 84 | alias has_key? include? 85 | alias member? include? 86 | alias key? include? 87 | 88 | def merge!(other) 89 | other.each { |k, v| self[k] = v } 90 | self 91 | end 92 | 93 | alias update merge! 94 | 95 | def merge(other) 96 | hash = dup 97 | hash.merge! other 98 | end 99 | 100 | def replace(other) 101 | clear 102 | @names.clear 103 | update other 104 | self 105 | end 106 | 107 | def to_hash 108 | {}.update(self) 109 | end 110 | 111 | def parse(header_string) 112 | return unless header_string && !header_string.empty? 113 | 114 | headers = header_string.split("\r\n") 115 | 116 | # Find the last set of response headers. 117 | start_index = headers.rindex { |x| x.start_with?('HTTP/') } || 0 118 | last_response = headers.slice(start_index, headers.size) 119 | 120 | last_response 121 | .tap { |a| a.shift if a.first.start_with?('HTTP/') } 122 | .map { |h| h.split(/:\s*/, 2) } # split key and value 123 | .reject { |p| p[0].nil? } # ignore blank lines 124 | .each { |key, value| add_parsed(key, value) } 125 | end 126 | 127 | protected 128 | 129 | attr_reader :names 130 | 131 | private 132 | 133 | # Join multiple values with a comma. 134 | def add_parsed(key, value) 135 | if key?(key) 136 | self[key] = self[key].to_s 137 | self[key] << ', ' << value 138 | else 139 | self[key] = value 140 | end 141 | end 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /spec/faraday/utils/headers_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Faraday::Utils::Headers do 4 | subject { Faraday::Utils::Headers.new } 5 | 6 | context 'when Content-Type is set to application/json' do 7 | before { subject['Content-Type'] = 'application/json' } 8 | 9 | it { expect(subject.keys).to eq(['Content-Type']) } 10 | it { expect(subject['Content-Type']).to eq('application/json') } 11 | it { expect(subject['CONTENT-TYPE']).to eq('application/json') } 12 | it { expect(subject['content-type']).to eq('application/json') } 13 | it { is_expected.to include('content-type') } 14 | end 15 | 16 | context 'when Content-Type is set to application/xml' do 17 | before { subject['Content-Type'] = 'application/xml' } 18 | 19 | it { expect(subject.keys).to eq(['Content-Type']) } 20 | it { expect(subject['Content-Type']).to eq('application/xml') } 21 | it { expect(subject['CONTENT-TYPE']).to eq('application/xml') } 22 | it { expect(subject['content-type']).to eq('application/xml') } 23 | it { is_expected.to include('content-type') } 24 | end 25 | 26 | describe '#fetch' do 27 | before { subject['Content-Type'] = 'application/json' } 28 | 29 | it { expect(subject.fetch('Content-Type')).to eq('application/json') } 30 | it { expect(subject.fetch('CONTENT-TYPE')).to eq('application/json') } 31 | it { expect(subject.fetch(:content_type)).to eq('application/json') } 32 | it { expect(subject.fetch('invalid', 'default')).to eq('default') } 33 | it { expect(subject.fetch('invalid', false)).to eq(false) } 34 | it { expect(subject.fetch('invalid', nil)).to be_nil } 35 | it { expect(subject.fetch('Invalid') { |key| "#{key} key" }).to eq('Invalid key') } 36 | it 'calls a block when provided' do 37 | block_called = false 38 | expect(subject.fetch('content-type') { block_called = true }).to eq('application/json') 39 | expect(block_called).to be_falsey 40 | end 41 | it 'raises an error if key not found' do 42 | expected_error = defined?(KeyError) ? KeyError : IndexError 43 | expect { subject.fetch('invalid') }.to raise_error(expected_error) 44 | end 45 | end 46 | 47 | describe '#delete' do 48 | before do 49 | subject['Content-Type'] = 'application/json' 50 | @deleted = subject.delete('content-type') 51 | end 52 | 53 | it { expect(@deleted).to eq('application/json') } 54 | it { expect(subject.size).to eq(0) } 55 | it { is_expected.not_to include('content-type') } 56 | it { expect(subject.delete('content-type')).to be_nil } 57 | end 58 | 59 | describe '#parse' do 60 | context 'when response headers leave http status line out' do 61 | let(:headers) { "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n" } 62 | 63 | before { subject.parse(headers) } 64 | 65 | it { expect(subject.keys).to eq(%w[Content-Type]) } 66 | it { expect(subject['Content-Type']).to eq('text/html') } 67 | it { expect(subject['content-type']).to eq('text/html') } 68 | end 69 | 70 | context 'when response headers values include a colon' do 71 | let(:headers) { "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nLocation: http://httpbingo.org/\r\n\r\n" } 72 | 73 | before { subject.parse(headers) } 74 | 75 | it { expect(subject['location']).to eq('http://httpbingo.org/') } 76 | end 77 | 78 | context 'when response headers include a blank line' do 79 | let(:headers) { "HTTP/1.1 200 OK\r\n\r\nContent-Type: text/html\r\n\r\n" } 80 | 81 | before { subject.parse(headers) } 82 | 83 | it { expect(subject['content-type']).to eq('text/html') } 84 | end 85 | 86 | context 'when response headers include already stored keys' do 87 | let(:headers) { "HTTP/1.1 200 OK\r\nX-Numbers: 123\r\n\r\n" } 88 | 89 | before do 90 | h = subject 91 | h[:x_numbers] = 8 92 | h.parse(headers) 93 | end 94 | 95 | it do 96 | expect(subject[:x_numbers]).to eq('8, 123') 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /spec/faraday/request/authorization_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Faraday::Request::Authorization do 4 | let(:conn) do 5 | Faraday.new do |b| 6 | b.request :authorization, auth_type, *auth_config 7 | b.adapter :test do |stub| 8 | stub.get('/auth-echo') do |env| 9 | [200, {}, env[:request_headers]['Authorization']] 10 | end 11 | end 12 | end 13 | end 14 | 15 | shared_examples 'does not interfere with existing authentication' do 16 | context 'and request already has an authentication header' do 17 | let(:response) { conn.get('/auth-echo', nil, authorization: 'OAuth oauth_token') } 18 | 19 | it 'does not interfere with existing authorization' do 20 | expect(response.body).to eq('OAuth oauth_token') 21 | end 22 | end 23 | end 24 | 25 | let(:response) { conn.get('/auth-echo') } 26 | 27 | describe 'basic_auth' do 28 | let(:auth_type) { :basic } 29 | 30 | context 'when passed correct params' do 31 | let(:auth_config) { %w[aladdin opensesame] } 32 | 33 | it { expect(response.body).to eq('Basic YWxhZGRpbjpvcGVuc2VzYW1l') } 34 | 35 | include_examples 'does not interfere with existing authentication' 36 | end 37 | 38 | context 'when passed very long values' do 39 | let(:auth_config) { ['A' * 255, ''] } 40 | 41 | it { expect(response.body).to eq("Basic #{'QUFB' * 85}Og==") } 42 | 43 | include_examples 'does not interfere with existing authentication' 44 | end 45 | end 46 | 47 | describe 'authorization' do 48 | let(:auth_type) { :Bearer } 49 | 50 | context 'when passed a string' do 51 | let(:auth_config) { ['custom'] } 52 | 53 | it { expect(response.body).to eq('Bearer custom') } 54 | 55 | include_examples 'does not interfere with existing authentication' 56 | end 57 | 58 | context 'when passed a proc' do 59 | let(:auth_config) { [-> { 'custom_from_proc' }] } 60 | 61 | it { expect(response.body).to eq('Bearer custom_from_proc') } 62 | 63 | include_examples 'does not interfere with existing authentication' 64 | end 65 | 66 | context 'when passed a callable' do 67 | let(:callable) { double('Callable Authorizer', call: 'custom_from_callable') } 68 | let(:auth_config) { [callable] } 69 | 70 | it { expect(response.body).to eq('Bearer custom_from_callable') } 71 | 72 | include_examples 'does not interfere with existing authentication' 73 | end 74 | 75 | context 'with an argument' do 76 | let(:response) { conn.get('/auth-echo', nil, 'middle' => 'crunchy surprise') } 77 | 78 | context 'when passed a proc' do 79 | let(:auth_config) { [proc { |env| "proc #{env.request_headers['middle']}" }] } 80 | 81 | it { expect(response.body).to eq('Bearer proc crunchy surprise') } 82 | 83 | include_examples 'does not interfere with existing authentication' 84 | end 85 | 86 | context 'when passed a lambda' do 87 | let(:auth_config) { [->(env) { "lambda #{env.request_headers['middle']}" }] } 88 | 89 | it { expect(response.body).to eq('Bearer lambda crunchy surprise') } 90 | 91 | include_examples 'does not interfere with existing authentication' 92 | end 93 | 94 | context 'when passed a callable with an argument' do 95 | let(:callable) do 96 | Class.new do 97 | def call(env) 98 | "callable #{env.request_headers['middle']}" 99 | end 100 | end.new 101 | end 102 | let(:auth_config) { [callable] } 103 | 104 | it { expect(response.body).to eq('Bearer callable crunchy surprise') } 105 | 106 | include_examples 'does not interfere with existing authentication' 107 | end 108 | end 109 | 110 | context 'when passed too many arguments' do 111 | let(:auth_config) { %w[baz foo] } 112 | 113 | it { expect { response }.to raise_error(ArgumentError) } 114 | 115 | include_examples 'does not interfere with existing authentication' 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /docs/assets/img/featured-bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Background and Stripes 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/adapters/testing.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: "Testing" 4 | permalink: /adapters/testing 5 | hide: true 6 | top_name: Adapters 7 | top_link: ./ 8 | --- 9 | 10 | The built-in Faraday Test adapter lets you define stubbed HTTP requests. This can 11 | be used to mock out network services in an application's unit tests. 12 | 13 | The easiest way to do this is to create the stubbed requests when initializing 14 | a `Faraday::Connection`. Stubbing a request by path yields a block with a 15 | `Faraday::Env` object. The stub block expects an Array return value with three 16 | values: an Integer HTTP status code, a Hash of key/value headers, and a 17 | response body. 18 | 19 | ```ruby 20 | conn = Faraday.new do |builder| 21 | builder.adapter :test do |stub| 22 | # block returns an array with 3 items: 23 | # - Integer response status 24 | # - Hash HTTP headers 25 | # - String response body 26 | stub.get('/ebi') do |env| 27 | [ 28 | 200, 29 | { 'Content-Type': 'text/plain', }, 30 | 'shrimp' 31 | ] 32 | end 33 | 34 | # test exceptions too 35 | stub.get('/boom') do 36 | raise Faraday::ConnectionFailed 37 | end 38 | end 39 | end 40 | ``` 41 | 42 | You can define the stubbed requests outside of the test adapter block: 43 | 44 | ```ruby 45 | stubs = Faraday::Adapter::Test::Stubs.new do |stub| 46 | stub.get('/tamago') { |env| [200, {}, 'egg'] } 47 | end 48 | ``` 49 | 50 | This Stubs instance can be passed to a new Connection: 51 | 52 | ```ruby 53 | conn = Faraday.new do |builder| 54 | builder.adapter :test, stubs do |stub| 55 | stub.get('/ebi') { |env| [ 200, {}, 'shrimp' ]} 56 | end 57 | end 58 | ``` 59 | 60 | It's also possible to stub additional requests after the connection has been 61 | initialized. This is useful for testing. 62 | 63 | ```ruby 64 | stubs.get('/uni') { |env| [ 200, {}, 'urchin' ]} 65 | ``` 66 | 67 | You can also stub the request body with a string or a proc. 68 | It would be useful to pass a proc if it's OK only to check the parts of the request body are passed. 69 | 70 | ```ruby 71 | stubs.post('/kohada', 'where=sea&temperature=24') { |env| [ 200, {}, 'spotted gizzard shad' ]} 72 | stubs.post('/anago', -> (request_body) { JSON.parse(request_body).slice('name') == { 'name' => 'Wakamoto' } }) { |env| [200, {}, 'conger eel'] } 73 | ``` 74 | 75 | If you want to stub requests that exactly match a path, parameters, and headers, 76 | `strict_mode` would be useful. 77 | 78 | ```ruby 79 | stubs = Faraday::Adapter::Test::Stubs.new(strict_mode: true) do |stub| 80 | stub.get('/ikura?nori=true', 'X-Soy-Sauce' => '5ml' ) { |env| [200, {}, 'ikura gunkan maki'] } 81 | end 82 | ``` 83 | 84 | This stub expects the connection will be called like this: 85 | 86 | ```ruby 87 | conn.get('/ikura', { nori: 'true' }, { 'X-Soy-Sauce' => '5ml' } ) 88 | ``` 89 | 90 | If there are other parameters or headers included, the Faraday Test adapter 91 | will raise `Faraday::Test::Stubs::NotFound`. It also raises the error 92 | if the specified parameters (`nori`) or headers (`X-Soy-Sauce`) are omitted. 93 | 94 | You can also enable `strict_mode` after initializing the connection. 95 | In this case, all requests, including ones that have been already stubbed, 96 | will be handled in a strict way. 97 | 98 | ```ruby 99 | stubs.strict_mode = true 100 | ``` 101 | 102 | Finally, you can treat your stubs as mocks by verifying that all of the stubbed 103 | calls were made. NOTE: this feature is still fairly experimental. It will not 104 | verify the order or count of any stub. 105 | 106 | ```ruby 107 | stubs.verify_stubbed_calls 108 | ``` 109 | 110 | After the test case is completed (possibly in an `after` hook), you should clear 111 | the default connection to prevent it from being cached between different tests. 112 | This allows for each test to have its own set of stubs 113 | 114 | ```ruby 115 | Faraday.default_connection = nil 116 | ``` 117 | 118 | ## Examples 119 | 120 | Working [RSpec] and [test/unit] examples for a fictional JSON API client are 121 | available. 122 | 123 | [RSpec]: https://github.com/lostisland/faraday/blob/master/examples/client_spec.rb 124 | [test/unit]: https://github.com/lostisland/faraday/blob/master/examples/client_test.rb 125 | -------------------------------------------------------------------------------- /examples/client_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Requires Ruby with test-unit and faraday gems. 4 | # ruby client_test.rb 5 | 6 | require 'faraday' 7 | require 'json' 8 | require 'test/unit' 9 | 10 | # Example API client 11 | class Client 12 | def initialize(conn) 13 | @conn = conn 14 | end 15 | 16 | def httpbingo(jname, params: {}) 17 | res = @conn.get("/#{jname}", params) 18 | data = JSON.parse(res.body) 19 | data['origin'] 20 | end 21 | 22 | def foo(params) 23 | res = @conn.post('/foo', JSON.dump(params)) 24 | res.status 25 | end 26 | end 27 | 28 | # Example API client test 29 | class ClientTest < Test::Unit::TestCase 30 | def test_httpbingo_name 31 | stubs = Faraday::Adapter::Test::Stubs.new 32 | stubs.get('/api') do |env| 33 | # optional: you can inspect the Faraday::Env 34 | assert_equal '/api', env.url.path 35 | [ 36 | 200, 37 | { 'Content-Type': 'application/javascript' }, 38 | '{"origin": "127.0.0.1"}' 39 | ] 40 | end 41 | 42 | # uncomment to trigger stubs.verify_stubbed_calls failure 43 | # stubs.get('/unused') { [404, {}, ''] } 44 | 45 | cli = client(stubs) 46 | assert_equal '127.0.0.1', cli.httpbingo('api') 47 | stubs.verify_stubbed_calls 48 | end 49 | 50 | def test_httpbingo_not_found 51 | stubs = Faraday::Adapter::Test::Stubs.new 52 | stubs.get('/api') do 53 | [ 54 | 404, 55 | { 'Content-Type': 'application/javascript' }, 56 | '{}' 57 | ] 58 | end 59 | 60 | cli = client(stubs) 61 | assert_nil cli.httpbingo('api') 62 | stubs.verify_stubbed_calls 63 | end 64 | 65 | def test_httpbingo_exception 66 | stubs = Faraday::Adapter::Test::Stubs.new 67 | stubs.get('/api') do 68 | raise Faraday::ConnectionFailed 69 | end 70 | 71 | cli = client(stubs) 72 | assert_raise Faraday::ConnectionFailed do 73 | cli.httpbingo('api') 74 | end 75 | stubs.verify_stubbed_calls 76 | end 77 | 78 | def test_strict_mode 79 | stubs = Faraday::Adapter::Test::Stubs.new(strict_mode: true) 80 | stubs.get('/api?abc=123') do 81 | [ 82 | 200, 83 | { 'Content-Type': 'application/javascript' }, 84 | '{"origin": "127.0.0.1"}' 85 | ] 86 | end 87 | 88 | cli = client(stubs) 89 | assert_equal '127.0.0.1', cli.httpbingo('api', params: { abc: 123 }) 90 | 91 | # uncomment to raise Stubs::NotFound 92 | # assert_equal '127.0.0.1', cli.httpbingo('api', params: { abc: 123, foo: 'Kappa' }) 93 | stubs.verify_stubbed_calls 94 | end 95 | 96 | def test_non_default_params_encoder 97 | stubs = Faraday::Adapter::Test::Stubs.new(strict_mode: true) 98 | stubs.get('/api?a=x&a=y&a=z') do 99 | [ 100 | 200, 101 | { 'Content-Type': 'application/javascript' }, 102 | '{"origin": "127.0.0.1"}' 103 | ] 104 | end 105 | conn = Faraday.new(request: { params_encoder: Faraday::FlatParamsEncoder }) do |builder| 106 | builder.adapter :test, stubs 107 | end 108 | 109 | cli = Client.new(conn) 110 | assert_equal '127.0.0.1', cli.httpbingo('api', params: { a: %w[x y z] }) 111 | 112 | # uncomment to raise Stubs::NotFound 113 | # assert_equal '127.0.0.1', cli.httpbingo('api', params: { a: %w[x y] }) 114 | stubs.verify_stubbed_calls 115 | end 116 | 117 | def test_with_string_body 118 | stubs = Faraday::Adapter::Test::Stubs.new do |stub| 119 | stub.post('/foo', '{"name":"YK"}') { [200, {}, ''] } 120 | end 121 | cli = client(stubs) 122 | assert_equal 200, cli.foo(name: 'YK') 123 | 124 | stubs.verify_stubbed_calls 125 | end 126 | 127 | def test_with_proc_body 128 | stubs = Faraday::Adapter::Test::Stubs.new do |stub| 129 | check = ->(request_body) { JSON.parse(request_body).slice('name') == { 'name' => 'YK' } } 130 | stub.post('/foo', check) { [200, {}, ''] } 131 | end 132 | cli = client(stubs) 133 | assert_equal 200, cli.foo(name: 'YK', created_at: Time.now) 134 | 135 | stubs.verify_stubbed_calls 136 | end 137 | 138 | def client(stubs) 139 | conn = Faraday.new do |builder| 140 | builder.adapter :test, stubs 141 | end 142 | Client.new(conn) 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /spec/faraday/request_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Faraday::Request do 4 | let(:conn) do 5 | Faraday.new(url: 'http://httpbingo.org/api', 6 | headers: { 'Mime-Version' => '1.0' }, 7 | request: { oauth: { consumer_key: 'anonymous' } }) 8 | end 9 | let(:http_method) { :get } 10 | let(:block) { nil } 11 | 12 | subject { conn.build_request(http_method, &block) } 13 | 14 | context 'when nothing particular is configured' do 15 | it { expect(subject.http_method).to eq(:get) } 16 | it { expect(subject.to_env(conn).ssl.verify).to be_falsey } 17 | it { expect(subject.to_env(conn).ssl.verify_hostname).to be_falsey } 18 | end 19 | 20 | context 'when HTTP method is post' do 21 | let(:http_method) { :post } 22 | 23 | it { expect(subject.http_method).to eq(:post) } 24 | end 25 | 26 | context 'when setting the url on setup with a URI' do 27 | let(:block) { proc { |req| req.url URI.parse('foo.json?a=1') } } 28 | 29 | it { expect(subject.path).to eq(URI.parse('foo.json')) } 30 | it { expect(subject.params).to eq('a' => '1') } 31 | it { expect(subject.to_env(conn).url.to_s).to eq('http://httpbingo.org/api/foo.json?a=1') } 32 | end 33 | 34 | context 'when setting the url on setup with a string path and params' do 35 | let(:block) { proc { |req| req.url 'foo.json', 'a' => 1 } } 36 | 37 | it { expect(subject.path).to eq('foo.json') } 38 | it { expect(subject.params).to eq('a' => 1) } 39 | it { expect(subject.to_env(conn).url.to_s).to eq('http://httpbingo.org/api/foo.json?a=1') } 40 | end 41 | 42 | context 'when setting the url on setup with a path including params' do 43 | let(:block) { proc { |req| req.url 'foo.json?b=2&a=1#qqq' } } 44 | 45 | it { expect(subject.path).to eq('foo.json') } 46 | it { expect(subject.params).to eq('a' => '1', 'b' => '2') } 47 | it { expect(subject.to_env(conn).url.to_s).to eq('http://httpbingo.org/api/foo.json?a=1&b=2') } 48 | end 49 | 50 | context 'when setting a header on setup with []= syntax' do 51 | let(:block) { proc { |req| req['Server'] = 'Faraday' } } 52 | let(:headers) { subject.to_env(conn).request_headers } 53 | 54 | it { expect(subject.headers['Server']).to eq('Faraday') } 55 | it { expect(headers['mime-version']).to eq('1.0') } 56 | it { expect(headers['server']).to eq('Faraday') } 57 | end 58 | 59 | context 'when setting the body on setup' do 60 | let(:block) { proc { |req| req.body = 'hi' } } 61 | 62 | it { expect(subject.body).to eq('hi') } 63 | it { expect(subject.to_env(conn).body).to eq('hi') } 64 | end 65 | 66 | context 'with global request options set' do 67 | let(:env_request) { subject.to_env(conn).request } 68 | 69 | before do 70 | conn.options.timeout = 3 71 | conn.options.open_timeout = 5 72 | conn.ssl.verify = false 73 | conn.proxy = 'http://proxy.com' 74 | end 75 | 76 | it { expect(subject.options.timeout).to eq(3) } 77 | it { expect(subject.options.open_timeout).to eq(5) } 78 | it { expect(env_request.timeout).to eq(3) } 79 | it { expect(env_request.open_timeout).to eq(5) } 80 | 81 | context 'and per-request options set' do 82 | let(:block) do 83 | proc do |req| 84 | req.options.timeout = 10 85 | req.options.boundary = 'boo' 86 | req.options.oauth[:consumer_secret] = 'xyz' 87 | req.options.context = { 88 | foo: 'foo', 89 | bar: 'bar' 90 | } 91 | end 92 | end 93 | 94 | it { expect(subject.options.timeout).to eq(10) } 95 | it { expect(subject.options.open_timeout).to eq(5) } 96 | it { expect(env_request.timeout).to eq(10) } 97 | it { expect(env_request.open_timeout).to eq(5) } 98 | it { expect(env_request.boundary).to eq('boo') } 99 | it { expect(env_request.context).to eq(foo: 'foo', bar: 'bar') } 100 | it do 101 | oauth_expected = { consumer_secret: 'xyz', consumer_key: 'anonymous' } 102 | expect(env_request.oauth).to eq(oauth_expected) 103 | end 104 | end 105 | end 106 | 107 | it 'supports marshal serialization' do 108 | expect(Marshal.load(Marshal.dump(subject))).to eq(subject) 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/faraday/request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | # Used to setup URLs, params, headers, and the request body in a sane manner. 5 | # 6 | # @example 7 | # @connection.post do |req| 8 | # req.url 'http://localhost', 'a' => '1' # 'http://localhost?a=1' 9 | # req.headers['b'] = '2' # Header 10 | # req.params['c'] = '3' # GET Param 11 | # req['b'] = '2' # also Header 12 | # req.body = 'abc' 13 | # end 14 | # 15 | # @!attribute http_method 16 | # @return [Symbol] the HTTP method of the Request 17 | # @!attribute path 18 | # @return [URI, String] the path 19 | # @!attribute params 20 | # @return [Hash] query parameters 21 | # @!attribute headers 22 | # @return [Faraday::Utils::Headers] headers 23 | # @!attribute body 24 | # @return [String] body 25 | # @!attribute options 26 | # @return [RequestOptions] options 27 | Request = Struct.new(:http_method, :path, :params, :headers, :body, :options) do 28 | extend MiddlewareRegistry 29 | 30 | alias_method :member_get, :[] 31 | private :member_get 32 | alias_method :member_set, :[]= 33 | private :member_set 34 | 35 | # @param request_method [String] 36 | # @yield [request] for block customization, if block given 37 | # @yieldparam request [Request] 38 | # @return [Request] 39 | def self.create(request_method) 40 | new(request_method).tap do |request| 41 | yield(request) if block_given? 42 | end 43 | end 44 | 45 | # Replace params, preserving the existing hash type. 46 | # 47 | # @param hash [Hash] new params 48 | def params=(hash) 49 | if params 50 | params.replace hash 51 | else 52 | member_set(:params, hash) 53 | end 54 | end 55 | 56 | # Replace request headers, preserving the existing hash type. 57 | # 58 | # @param hash [Hash] new headers 59 | def headers=(hash) 60 | if headers 61 | headers.replace hash 62 | else 63 | member_set(:headers, hash) 64 | end 65 | end 66 | 67 | # Update path and params. 68 | # 69 | # @param path [URI, String] 70 | # @param params [Hash, nil] 71 | # @return [void] 72 | def url(path, params = nil) 73 | if path.respond_to? :query 74 | if (query = path.query) 75 | path = path.dup 76 | path.query = nil 77 | end 78 | else 79 | anchor_index = path.index('#') 80 | path = path.slice(0, anchor_index) unless anchor_index.nil? 81 | path, query = path.split('?', 2) 82 | end 83 | self.path = path 84 | self.params.merge_query query, options.params_encoder 85 | self.params.update(params) if params 86 | end 87 | 88 | # @param key [Object] key to look up in headers 89 | # @return [Object] value of the given header name 90 | def [](key) 91 | headers[key] 92 | end 93 | 94 | # @param key [Object] key of header to write 95 | # @param value [Object] value of header 96 | def []=(key, value) 97 | headers[key] = value 98 | end 99 | 100 | # Marshal serialization support. 101 | # 102 | # @return [Hash] the hash ready to be serialized in Marshal. 103 | def marshal_dump 104 | { 105 | http_method: http_method, 106 | body: body, 107 | headers: headers, 108 | path: path, 109 | params: params, 110 | options: options 111 | } 112 | end 113 | 114 | # Marshal serialization support. 115 | # Restores the instance variables according to the +serialised+. 116 | # @param serialised [Hash] the serialised object. 117 | def marshal_load(serialised) 118 | self.http_method = serialised[:http_method] 119 | self.body = serialised[:body] 120 | self.headers = serialised[:headers] 121 | self.path = serialised[:path] 122 | self.params = serialised[:params] 123 | self.options = serialised[:options] 124 | end 125 | 126 | # @return [Env] the Env for this Request 127 | def to_env(connection) 128 | Env.new(http_method, body, connection.build_exclusive_url(path, params), 129 | options, headers, connection.ssl, connection.parallel_manager) 130 | end 131 | end 132 | end 133 | 134 | require 'faraday/request/authorization' 135 | require 'faraday/request/instrumentation' 136 | require 'faraday/request/json' 137 | require 'faraday/request/url_encoded' 138 | -------------------------------------------------------------------------------- /docs/middleware/response/logger.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: "Logger Middleware" 4 | permalink: /middleware/logger 5 | hide: true 6 | prev_name: JSON Response Middleware 7 | prev_link: ./json-response 8 | next_name: RaiseError Middleware 9 | next_link: ./raise-error 10 | top_name: Back to Middleware 11 | top_link: ./list 12 | --- 13 | 14 | The `Logger` middleware logs both the request and the response body and headers. 15 | It is highly customizable and allows to mask confidential information if necessary. 16 | 17 | ### Basic Usage 18 | 19 | ```ruby 20 | conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| 21 | faraday.response :logger # log requests and responses to $stdout 22 | end 23 | 24 | conn.get 25 | # => INFO -- request: GET http://httpbingo.org/ 26 | # => DEBUG -- request: User-Agent: "Faraday v1.0.0" 27 | # => INFO -- response: Status 301 28 | # => DEBUG -- response: date: "Sun, 19 May 2019 16:05:40 GMT" 29 | ``` 30 | 31 | ### Customize the logger 32 | 33 | By default, the `Logger` middleware uses the Ruby `Logger.new($stdout)`. 34 | You can customize it to use any logger you want by providing it when you add the middleware to the stack: 35 | 36 | ```ruby 37 | conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| 38 | faraday.response :logger, MyLogger.new($stdout) 39 | end 40 | ``` 41 | 42 | ### Include and exclude headers/bodies 43 | 44 | By default, the `logger` middleware logs only headers for security reasons, however, you can configure it 45 | to log bodies and errors as well, or disable headers logging if you need to. 46 | To do so, simply provide a configuration hash when you add the middleware to the stack: 47 | 48 | ```ruby 49 | conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| 50 | faraday.response :logger, nil, { headers: true, bodies: true, errors: true } 51 | end 52 | ``` 53 | 54 | You can also configure the `logger` middleware with a little more complex settings 55 | like "do not log the request bodies, but log the response bodies". 56 | 57 | ```ruby 58 | conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| 59 | faraday.response :logger, nil, { bodies: { request: false, response: true } } 60 | end 61 | ``` 62 | 63 | Please note this only works with the default formatter. 64 | 65 | ### Filter sensitive information 66 | 67 | You can filter sensitive information from Faraday logs using a regex matcher: 68 | 69 | ```ruby 70 | conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| 71 | faraday.response :logger do | logger | 72 | logger.filter(/(api_key=)([^&]+)/, '\1[REMOVED]') 73 | end 74 | end 75 | 76 | conn.get('/', api_key: 'secret') 77 | # => INFO -- request: GET http://httpbingo.org/?api_key=[REMOVED] 78 | # => DEBUG -- request: User-Agent: "Faraday v1.0.0" 79 | # => INFO -- response: Status 301 80 | # => DEBUG -- response: date: "Sun, 19 May 2019 16:12:36 GMT" 81 | ``` 82 | 83 | ### Change log level 84 | 85 | By default, the `logger` middleware logs on the `info` log level. It is possible to configure 86 | the severity by providing the `log_level` option: 87 | 88 | ```ruby 89 | conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| 90 | faraday.response :logger, nil, { bodies: true, log_level: :debug } 91 | end 92 | ``` 93 | 94 | ### Customize the formatter 95 | 96 | You can also provide a custom formatter to control how requests, responses and errors are logged. 97 | Any custom formatter MUST implement the `request` and `response` method, with one argument which 98 | will be passed being the Faraday environment. 99 | Any custom formatter CAN implement the `exception` method, 100 | with one argument which will be passed being the exception (StandardError). 101 | If you make your formatter inheriting from `Faraday::Logging::Formatter`, 102 | then the methods `debug`, `info`, `warn`, `error` and `fatal` are automatically delegated to the logger. 103 | 104 | ```ruby 105 | class MyFormatter < Faraday::Logging::Formatter 106 | def request(env) 107 | # Build a custom message using `env` 108 | info('Request') { 'Sending Request' } 109 | end 110 | 111 | def response(env) 112 | # Build a custom message using `env` 113 | info('Response') { 'Response Received' } 114 | end 115 | 116 | def exception(exc) 117 | # Build a custom message using `exc` 118 | info('Error') { 'Error Raised' } 119 | end 120 | end 121 | 122 | conn = Faraday.new(url: 'http://httpbingo.org/api_key=s3cr3t') do |faraday| 123 | faraday.response :logger, nil, formatter: MyFormatter 124 | end 125 | ``` 126 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | In Faraday we always welcome new ideas and features, however we also have to ensure 4 | that the overall code quality stays on reasonable levels. 5 | For this reason, before adding any contribution to Faraday, we highly recommend reading this 6 | quick guide to ensure your PR can be reviewed and approved as quickly as possible. 7 | 8 | We are past our 1.0 release, and follow [Semantic Versioning][semver]. If your 9 | patch includes changes that break compatibility, note that in the Pull Request, so we can add it to 10 | the [Changelog][]. 11 | 12 | 13 | ### Policy on inclusive language 14 | 15 | You have read our [Code of Conduct][], which includes a note about **inclusive language**. This section tries to make that actionable. 16 | 17 | Faraday has a large and diverse userbase. To make Faraday a pleasant and effective experience for everyone, we use inclusive language. 18 | 19 | These resources can help: 20 | 21 | - Google's tutorial [Writing inclusive documentation](https://developers.google.com/style/inclusive-documentation) teaches by example, how to reword non-inclusive things. 22 | - Linux kernel mailing list's [Coding Style: Inclusive Terminology](https://lkml.org/lkml/2020/7/4/229) said "Add no new instances of non-inclusive words, here is a list of words not include new ones of." 23 | - Linguistic Society of America published [Guidelines for Inclusive Language](https://www.linguisticsociety.org/resource/guidelines-inclusive-language) which concluded: "We encourage all linguists to consider the possible reactions of their potential audience to their writing and, in so doing, to choose expository practices and content that is positive, inclusive, and respectful." 24 | 25 | This project attempts to improve in these areas. Join us in doing that important work. 26 | 27 | If you want to privately raise any breach to this policy with the Faraday team, feel free to reach out to [@iMacTia](https://twitter.com/iMacTia) and [@olleolleolle](https://twitter.com/olleolleolle) on Twitter. 28 | 29 | 30 | ### Required Checks 31 | 32 | Before pushing your code and opening a PR, we recommend you run the following checks to avoid 33 | our GitHub Actions Workflow to block your contribution. 34 | 35 | ```bash 36 | # Run unit tests and check code coverage 37 | $ bundle exec rspec 38 | 39 | # Check code style 40 | $ bundle exec rubocop 41 | ``` 42 | 43 | 44 | ### New Features 45 | 46 | When adding a feature in Faraday: 47 | 48 | 1. also add tests to cover your new feature. 49 | 2. if the feature is for an adapter, the **attempt** must be made to add the same feature to all other adapters as well. 50 | 3. start opening an issue describing how the new feature will work, and only after receiving 51 | the green light by the core team start working on the PR. 52 | 53 | 54 | ### New Middleware & Adapters 55 | 56 | We prefer new adapters and middlewares to be added **as separate gems**. We can link to such gems from this project. 57 | 58 | This goes for the [faraday_middleware][] project as well. 59 | 60 | We encourage adapters that: 61 | 62 | 1. support SSL & streaming; 63 | 1. are proven and may have better performance than existing ones; or 64 | 1. have features not present in included adapters. 65 | 66 | 67 | ### Changes to the Faraday Website 68 | 69 | The [Faraday Website][website] is included in the Faraday repository, under the `/docs` folder. 70 | If you want to apply changes to it, please test it locally before opening your PR. 71 | 72 | 73 | #### Test website changes using Docker 74 | 75 | Start by cloning the repository and navigate to the newly-cloned directory on your computer. Then run the following: 76 | 77 | ```bash 78 | docker container run -p 80:4000 -v $(pwd)/docs:/site bretfisher/jekyll-serve 79 | ``` 80 | 81 | And that's it! Open your browser and navigate to `http://localhost` to see the website running. 82 | Any change done to files in the `/docs` folder will be automatically picked up (with the exception of config changes). 83 | 84 | 85 | #### Test website changes using Jekyll 86 | 87 | You can test website changes locally, on your machine, too. Here's how: 88 | 89 | Navigate into the /docs folder: 90 | 91 | ```bash 92 | $ cd docs 93 | ``` 94 | 95 | Install Jekyll dependencies, this bundle is different from Faraday's one. 96 | 97 | ```bash 98 | $ bundle install 99 | ``` 100 | 101 | Run the Jekyll server with the Faraday website 102 | 103 | ```bash 104 | $ bundle exec jekyll serve 105 | ``` 106 | 107 | Now, navigate to http://127.0.0.1:4000/faraday/ to see the website running. 108 | 109 | [semver]: https://semver.org/ 110 | [changelog]: https://github.com/lostisland/faraday/releases 111 | [faraday_middleware]: https://github.com/lostisland/faraday_middleware 112 | [website]: https://lostisland.github.io/faraday 113 | [Code of Conduct]: ./CODE_OF_CONDUCT.md 114 | -------------------------------------------------------------------------------- /lib/faraday/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Faraday namespace. 4 | module Faraday 5 | # Faraday error base class. 6 | class Error < StandardError 7 | attr_reader :response, :wrapped_exception 8 | 9 | def initialize(exc = nil, response = nil) 10 | @wrapped_exception = nil unless defined?(@wrapped_exception) 11 | @response = nil unless defined?(@response) 12 | super(exc_msg_and_response!(exc, response)) 13 | end 14 | 15 | def backtrace 16 | if @wrapped_exception 17 | @wrapped_exception.backtrace 18 | else 19 | super 20 | end 21 | end 22 | 23 | def inspect 24 | inner = +'' 25 | inner << " wrapped=#{@wrapped_exception.inspect}" if @wrapped_exception 26 | inner << " response=#{@response.inspect}" if @response 27 | inner << " #{super}" if inner.empty? 28 | %(#<#{self.class}#{inner}>) 29 | end 30 | 31 | def response_status 32 | @response[:status] if @response 33 | end 34 | 35 | def response_headers 36 | @response[:headers] if @response 37 | end 38 | 39 | def response_body 40 | @response[:body] if @response 41 | end 42 | 43 | protected 44 | 45 | # Pulls out potential parent exception and response hash, storing them in 46 | # instance variables. 47 | # exc - Either an Exception, a string message, or a response hash. 48 | # response - Hash 49 | # :status - Optional integer HTTP response status 50 | # :headers - String key/value hash of HTTP response header 51 | # values. 52 | # :body - Optional string HTTP response body. 53 | # :request - Hash 54 | # :method - Symbol with the request HTTP method. 55 | # :url - URI object with the url requested. 56 | # :url_path - String with the url path requested. 57 | # :params - String key/value hash of query params 58 | # present in the request. 59 | # :headers - String key/value hash of HTTP request 60 | # header values. 61 | # :body - String HTTP request body. 62 | # 63 | # If a subclass has to call this, then it should pass a string message 64 | # to `super`. See NilStatusError. 65 | def exc_msg_and_response!(exc, response = nil) 66 | if @response.nil? && @wrapped_exception.nil? 67 | @wrapped_exception, msg, @response = exc_msg_and_response(exc, response) 68 | return msg 69 | end 70 | 71 | exc.to_s 72 | end 73 | 74 | # Pulls out potential parent exception and response hash. 75 | def exc_msg_and_response(exc, response = nil) 76 | return [exc, exc.message, response] if exc.respond_to?(:backtrace) 77 | 78 | return [nil, "the server responded with status #{exc[:status]}", exc] \ 79 | if exc.respond_to?(:each_key) 80 | 81 | [nil, exc.to_s, response] 82 | end 83 | end 84 | 85 | # Faraday client error class. Represents 4xx status responses. 86 | class ClientError < Error 87 | end 88 | 89 | # Raised by Faraday::Response::RaiseError in case of a 400 response. 90 | class BadRequestError < ClientError 91 | end 92 | 93 | # Raised by Faraday::Response::RaiseError in case of a 401 response. 94 | class UnauthorizedError < ClientError 95 | end 96 | 97 | # Raised by Faraday::Response::RaiseError in case of a 403 response. 98 | class ForbiddenError < ClientError 99 | end 100 | 101 | # Raised by Faraday::Response::RaiseError in case of a 404 response. 102 | class ResourceNotFound < ClientError 103 | end 104 | 105 | # Raised by Faraday::Response::RaiseError in case of a 407 response. 106 | class ProxyAuthError < ClientError 107 | end 108 | 109 | # Raised by Faraday::Response::RaiseError in case of a 409 response. 110 | class ConflictError < ClientError 111 | end 112 | 113 | # Raised by Faraday::Response::RaiseError in case of a 422 response. 114 | class UnprocessableEntityError < ClientError 115 | end 116 | 117 | # Faraday server error class. Represents 5xx status responses. 118 | class ServerError < Error 119 | end 120 | 121 | # A unified client error for timeouts. 122 | class TimeoutError < ServerError 123 | def initialize(exc = 'timeout', response = nil) 124 | super(exc, response) 125 | end 126 | end 127 | 128 | # Raised by Faraday::Response::RaiseError in case of a nil status in response. 129 | class NilStatusError < ServerError 130 | def initialize(exc, response = nil) 131 | exc_msg_and_response!(exc, response) 132 | super('http status could not be derived from the server response') 133 | end 134 | end 135 | 136 | # A unified error for failed connections. 137 | class ConnectionFailed < Error 138 | end 139 | 140 | # A unified client error for SSL errors. 141 | class SSLError < Error 142 | end 143 | 144 | # Raised by middlewares that parse the response, like the JSON response middleware. 145 | class ParsingError < Error 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /docs/middleware/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: "Middleware" 4 | permalink: /middleware/ 5 | next_name: Available Middleware 6 | next_link: ./list 7 | order: 3 8 | --- 9 | 10 | Under the hood, Faraday uses a Rack-inspired middleware stack for making 11 | requests. Much of Faraday's power is unlocked with custom middleware. Some 12 | middleware is included with Faraday, and others are in external gems. 13 | 14 | Here are some of the features that middleware can provide: 15 | 16 | - authentication 17 | - caching responses on disk or in memory 18 | - cookies 19 | - following redirects 20 | - JSON encoding/decoding 21 | - logging 22 | 23 | To use these great features, create a `Faraday::Connection` with `Faraday.new` 24 | and add the correct middleware in a block. For example: 25 | 26 | ```ruby 27 | require 'faraday' 28 | 29 | conn = Faraday.new do |f| 30 | f.request :json # encode req bodies as JSON 31 | f.request :logger # logs request and responses 32 | f.response :json # decode response bodies as JSON 33 | f.adapter :net_http # Use the Net::HTTP adapter 34 | end 35 | response = conn.get("http://httpbingo.org/get") 36 | ``` 37 | 38 | ### How it Works 39 | 40 | A `Faraday::Connection` uses a `Faraday::RackBuilder` to assemble a 41 | Rack-inspired middleware stack for making HTTP requests. Each middleware runs 42 | and passes an Env object around to the next one. After the final middleware has 43 | run, Faraday will return a `Faraday::Response` to the end user. 44 | 45 | The order in which middleware is stacked is important. Like with Rack, the first 46 | middleware on the list wraps all others, while the last middleware is the 47 | innermost one. If you want to use a custom [adapter](../adapters), it must 48 | therefore be last. 49 | 50 | ![Middleware](../assets/img/middleware.png) 51 | 52 | This is what makes things like the "retry middleware" possible. 53 | It doesn't really matter if the middleware was registered as a request or a response one, the only thing that matter is how they're added to the stack. 54 | 55 | Say you have the following: 56 | 57 | ```ruby 58 | Faraday.new(...) do |conn| 59 | conn.request :authorization 60 | conn.response :json 61 | conn.response :parse_dates 62 | end 63 | ``` 64 | 65 | This will result into a middleware stack like this: 66 | 67 | ```ruby 68 | authorization do 69 | # authorization request hook 70 | json do 71 | # json request hook 72 | parse_dates do 73 | # parse_dates request hook 74 | response = adapter.perform(request) 75 | # parse_dates response hook 76 | end 77 | # json response hook 78 | end 79 | # authorization response hook 80 | end 81 | ``` 82 | 83 | In this example, you can see that `parse_dates` is the LAST middleware processing the request, and the FIRST middleware processing the response. 84 | This is why it's important for the adapter to always be at the end of the middleware list. 85 | 86 | ### Using Middleware 87 | 88 | Calling `use` is the most basic way to add middleware to your stack, but most 89 | middleware is conveniently registered in the `request`, `response` or `adapter` 90 | namespaces. All four methods are equivalent apart from the namespacing. 91 | 92 | For example, the `Faraday::Request::UrlEncoded` middleware registers itself in 93 | `Faraday::Request` so it can be added with `request`. These two are equivalent: 94 | 95 | ```ruby 96 | # add by symbol, lookup from Faraday::Request, 97 | # Faraday::Response and Faraday::Adapter registries 98 | conn = Faraday.new do |f| 99 | f.request :url_encoded 100 | f.response :logger 101 | f.adapter :net_http 102 | end 103 | ``` 104 | 105 | or: 106 | 107 | ```ruby 108 | # identical, but add the class directly instead of using lookups 109 | conn = Faraday.new do |f| 110 | f.use Faraday::Request::UrlEncoded 111 | f.use Faraday::Response::Logger 112 | f.use Faraday::Adapter::NetHttp 113 | end 114 | ``` 115 | 116 | This is also the place to pass options. For example: 117 | 118 | ```ruby 119 | conn = Faraday.new do |f| 120 | f.request :logger, bodies: true 121 | end 122 | ``` 123 | 124 | ### Available Middleware 125 | 126 | The [Awesome Faraday](https://github.com/lostisland/awesome-faraday/) project 127 | has a complete list of useful, well-maintained Faraday middleware. Middleware is 128 | often provided by external gems, like the 129 | [faraday-retry](https://github.com/lostisland/faraday-retry) gem. 130 | 131 | We also have [great documentation](list) for the middleware that ships with 132 | Faraday. 133 | 134 | ### Detailed Example 135 | 136 | Here's a more realistic example: 137 | 138 | ```ruby 139 | Faraday.new(...) do |conn| 140 | # POST/PUT params encoder 141 | conn.request :url_encoded 142 | 143 | # Logging of requests/responses 144 | conn.response :logger 145 | 146 | # Last middleware must be the adapter 147 | conn.adapter :net_http 148 | end 149 | ``` 150 | 151 | This request middleware setup affects POST/PUT requests in the following way: 152 | 153 | 1. `Request::UrlEncoded` encodes as "application/x-www-form-urlencoded" if not 154 | already encoded or of another type. 155 | 2. `Response::Logger` logs request and response headers, can be configured to log bodies as well. 156 | 157 | Swapping middleware means giving the other priority. Specifying the 158 | "Content-Type" for the request is explicitly stating which middleware should 159 | process it. 160 | -------------------------------------------------------------------------------- /spec/faraday/params_encoders/nested_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rack/utils' 4 | 5 | RSpec.describe Faraday::NestedParamsEncoder do 6 | it_behaves_like 'a params encoder' 7 | 8 | it 'decodes arrays' do 9 | query = 'a[1]=one&a[2]=two&a[3]=three' 10 | expected = { 'a' => %w[one two three] } 11 | expect(subject.decode(query)).to eq(expected) 12 | end 13 | 14 | it 'decodes hashes' do 15 | query = 'a[b1]=one&a[b2]=two&a[b][c]=foo' 16 | expected = { 'a' => { 'b1' => 'one', 'b2' => 'two', 'b' => { 'c' => 'foo' } } } 17 | expect(subject.decode(query)).to eq(expected) 18 | end 19 | 20 | it 'decodes nested arrays rack compat' do 21 | query = 'a[][one]=1&a[][two]=2&a[][one]=3&a[][two]=4' 22 | expected = Rack::Utils.parse_nested_query(query) 23 | expect(subject.decode(query)).to eq(expected) 24 | end 25 | 26 | it 'decodes nested array mixed types' do 27 | query = 'a[][one]=1&a[]=2&a[]=&a[]' 28 | expected = Rack::Utils.parse_nested_query(query) 29 | expect(subject.decode(query)).to eq(expected) 30 | end 31 | 32 | it 'decodes nested ignores invalid array' do 33 | query = '[][a]=1&b=2' 34 | expected = { 'a' => '1', 'b' => '2' } 35 | expect(subject.decode(query)).to eq(expected) 36 | end 37 | 38 | it 'decodes nested ignores repeated array notation' do 39 | query = 'a[][][]=1' 40 | expected = { 'a' => ['1'] } 41 | expect(subject.decode(query)).to eq(expected) 42 | end 43 | 44 | it 'decodes nested ignores malformed keys' do 45 | query = '=1&[]=2' 46 | expected = {} 47 | expect(subject.decode(query)).to eq(expected) 48 | end 49 | 50 | it 'decodes nested subkeys dont have to be in brackets' do 51 | query = 'a[b]c[d]e=1' 52 | expected = { 'a' => { 'b' => { 'c' => { 'd' => { 'e' => '1' } } } } } 53 | expect(subject.decode(query)).to eq(expected) 54 | end 55 | 56 | it 'decodes nested final value overrides any type' do 57 | query = 'a[b][c]=1&a[b]=2' 58 | expected = { 'a' => { 'b' => '2' } } 59 | expect(subject.decode(query)).to eq(expected) 60 | end 61 | 62 | it 'encodes rack compat' do 63 | params = { a: [{ one: '1', two: '2' }, '3', ''] } 64 | result = Faraday::Utils.unescape(Faraday::NestedParamsEncoder.encode(params)).split('&') 65 | expected = Rack::Utils.build_nested_query(params).split('&') 66 | expect(result).to match_array(expected) 67 | end 68 | 69 | it 'encodes empty string array value' do 70 | expected = 'baz=&foo%5Bbar%5D=' 71 | result = Faraday::NestedParamsEncoder.encode(foo: { bar: '' }, baz: '') 72 | expect(result).to eq(expected) 73 | end 74 | 75 | it 'encodes nil array value' do 76 | expected = 'baz&foo%5Bbar%5D' 77 | result = Faraday::NestedParamsEncoder.encode(foo: { bar: nil }, baz: nil) 78 | expect(result).to eq(expected) 79 | end 80 | 81 | it 'encodes empty array value' do 82 | expected = 'baz%5B%5D&foo%5Bbar%5D%5B%5D' 83 | result = Faraday::NestedParamsEncoder.encode(foo: { bar: [] }, baz: []) 84 | expect(result).to eq(expected) 85 | end 86 | 87 | it 'encodes boolean values' do 88 | params = { a: true, b: false } 89 | expect(subject.encode(params)).to eq('a=true&b=false') 90 | end 91 | 92 | it 'encodes boolean values in array' do 93 | params = { a: [true, false] } 94 | expect(subject.encode(params)).to eq('a%5B%5D=true&a%5B%5D=false') 95 | end 96 | 97 | it 'encodes unsorted when asked' do 98 | params = { b: false, a: true } 99 | expect(subject.encode(params)).to eq('a=true&b=false') 100 | Faraday::NestedParamsEncoder.sort_params = false 101 | expect(subject.encode(params)).to eq('b=false&a=true') 102 | Faraday::NestedParamsEncoder.sort_params = true 103 | end 104 | 105 | it 'encodes arrays indices when asked' do 106 | params = { a: [0, 1, 2] } 107 | expect(subject.encode(params)).to eq('a%5B%5D=0&a%5B%5D=1&a%5B%5D=2') 108 | Faraday::NestedParamsEncoder.array_indices = true 109 | expect(subject.encode(params)).to eq('a%5B0%5D=0&a%5B1%5D=1&a%5B2%5D=2') 110 | Faraday::NestedParamsEncoder.array_indices = false 111 | end 112 | 113 | shared_examples 'a wrong decoding' do 114 | it do 115 | expect { subject.decode(query) }.to raise_error(TypeError) do |e| 116 | expect(e.message).to eq(error_message) 117 | end 118 | end 119 | end 120 | 121 | context 'when expecting hash but getting string' do 122 | let(:query) { 'a=1&a[b]=2' } 123 | let(:error_message) { "expected Hash (got String) for param `a'" } 124 | it_behaves_like 'a wrong decoding' 125 | end 126 | 127 | context 'when expecting hash but getting array' do 128 | let(:query) { 'a[]=1&a[b]=2' } 129 | let(:error_message) { "expected Hash (got Array) for param `a'" } 130 | it_behaves_like 'a wrong decoding' 131 | end 132 | 133 | context 'when expecting nested hash but getting non nested' do 134 | let(:query) { 'a[b]=1&a[b][c]=2' } 135 | let(:error_message) { "expected Hash (got String) for param `b'" } 136 | it_behaves_like 'a wrong decoding' 137 | end 138 | 139 | context 'when expecting array but getting hash' do 140 | let(:query) { 'a[b]=1&a[]=2' } 141 | let(:error_message) { "expected Array (got Hash) for param `a'" } 142 | it_behaves_like 'a wrong decoding' 143 | end 144 | 145 | context 'when expecting array but getting string' do 146 | let(:query) { 'a=1&a[]=2' } 147 | let(:error_message) { "expected Array (got String) for param `a'" } 148 | it_behaves_like 'a wrong decoding' 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /lib/faraday/options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | # Subclasses Struct with some special helpers for converting from a Hash to 5 | # a Struct. 6 | class Options < Struct 7 | # Public 8 | def self.from(value) 9 | value ? new.update(value) : new 10 | end 11 | 12 | # Public 13 | def each 14 | return to_enum(:each) unless block_given? 15 | 16 | members.each do |key| 17 | yield(key.to_sym, send(key)) 18 | end 19 | end 20 | 21 | # Public 22 | def update(obj) 23 | obj.each do |key, value| 24 | sub_options = self.class.options_for(key) 25 | if sub_options 26 | new_value = sub_options.from(value) if value 27 | elsif value.is_a?(Hash) 28 | new_value = value.dup 29 | else 30 | new_value = value 31 | end 32 | 33 | send("#{key}=", new_value) unless new_value.nil? 34 | end 35 | self 36 | end 37 | 38 | # Public 39 | def delete(key) 40 | value = send(key) 41 | send("#{key}=", nil) 42 | value 43 | end 44 | 45 | # Public 46 | def clear 47 | members.each { |member| delete(member) } 48 | end 49 | 50 | # Public 51 | def merge!(other) 52 | other.each do |key, other_value| 53 | self_value = send(key) 54 | sub_options = self.class.options_for(key) 55 | new_value = if self_value && sub_options && other_value 56 | self_value.merge(other_value) 57 | else 58 | other_value 59 | end 60 | send("#{key}=", new_value) unless new_value.nil? 61 | end 62 | self 63 | end 64 | 65 | # Public 66 | def merge(other) 67 | dup.merge!(other) 68 | end 69 | 70 | # Public 71 | def deep_dup 72 | self.class.from(self) 73 | end 74 | 75 | # Public 76 | def fetch(key, *args) 77 | unless symbolized_key_set.include?(key.to_sym) 78 | key_setter = "#{key}=" 79 | if !args.empty? 80 | send(key_setter, args.first) 81 | elsif block_given? 82 | send(key_setter, yield(key)) 83 | else 84 | raise self.class.fetch_error_class, "key not found: #{key.inspect}" 85 | end 86 | end 87 | send(key) 88 | end 89 | 90 | # Public 91 | def values_at(*keys) 92 | keys.map { |key| send(key) } 93 | end 94 | 95 | # Public 96 | def keys 97 | members.reject { |member| send(member).nil? } 98 | end 99 | 100 | # Public 101 | def empty? 102 | keys.empty? 103 | end 104 | 105 | # Public 106 | def each_key(&block) 107 | return to_enum(:each_key) unless block 108 | 109 | keys.each(&block) 110 | end 111 | 112 | # Public 113 | def key?(key) 114 | keys.include?(key) 115 | end 116 | 117 | alias has_key? key? 118 | 119 | # Public 120 | def each_value(&block) 121 | return to_enum(:each_value) unless block 122 | 123 | values.each(&block) 124 | end 125 | 126 | # Public 127 | def value?(value) 128 | values.include?(value) 129 | end 130 | 131 | alias has_value? value? 132 | 133 | # Public 134 | def to_hash 135 | hash = {} 136 | members.each do |key| 137 | value = send(key) 138 | hash[key.to_sym] = value unless value.nil? 139 | end 140 | hash 141 | end 142 | 143 | # Internal 144 | def inspect 145 | values = [] 146 | members.each do |member| 147 | value = send(member) 148 | values << "#{member}=#{value.inspect}" if value 149 | end 150 | values = values.empty? ? '(empty)' : values.join(', ') 151 | 152 | %(#<#{self.class} #{values}>) 153 | end 154 | 155 | # Internal 156 | def self.options(mapping) 157 | attribute_options.update(mapping) 158 | end 159 | 160 | # Internal 161 | def self.options_for(key) 162 | attribute_options[key] 163 | end 164 | 165 | # Internal 166 | def self.attribute_options 167 | @attribute_options ||= {} 168 | end 169 | 170 | def self.memoized(key, &block) 171 | unless block 172 | raise ArgumentError, '#memoized must be called with a block' 173 | end 174 | 175 | memoized_attributes[key.to_sym] = block 176 | class_eval <<-RUBY, __FILE__, __LINE__ + 1 177 | def #{key}() self[:#{key}]; end 178 | RUBY 179 | end 180 | 181 | def self.memoized_attributes 182 | @memoized_attributes ||= {} 183 | end 184 | 185 | def [](key) 186 | key = key.to_sym 187 | if (method = self.class.memoized_attributes[key]) 188 | super(key) || (self[key] = instance_eval(&method)) 189 | else 190 | super 191 | end 192 | end 193 | 194 | def symbolized_key_set 195 | @symbolized_key_set ||= Set.new(keys.map(&:to_sym)) 196 | end 197 | 198 | def self.inherited(subclass) 199 | super 200 | subclass.attribute_options.update(attribute_options) 201 | subclass.memoized_attributes.update(memoized_attributes) 202 | end 203 | 204 | def self.fetch_error_class 205 | @fetch_error_class ||= if Object.const_defined?(:KeyError) 206 | ::KeyError 207 | else 208 | ::IndexError 209 | end 210 | end 211 | end 212 | end 213 | 214 | require 'faraday/options/request_options' 215 | require 'faraday/options/ssl_options' 216 | require 'faraday/options/proxy_options' 217 | require 'faraday/options/connection_options' 218 | require 'faraday/options/env' 219 | -------------------------------------------------------------------------------- /docs/adapters/write_your_adapter.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: "Write your own adapter" 4 | permalink: /adapters/write_your_adapter 5 | hide: true 6 | order: 2 7 | --- 8 | 9 | Adapters have methods that can help you implement support for a new backend. 10 | 11 | This example will use a fictional HTTP backend gem called `FlorpHttp`. It doesn't 12 | exist. Its only function is to make this example more concrete. 13 | 14 | ### An Adapter _is_ a Middleware 15 | 16 | When you subclass `::Faraday::Adapter`, you get helpful methods defined: 17 | 18 | ```ruby 19 | class FlorpHttp < ::Faraday::Adapter 20 | end 21 | ``` 22 | 23 | Now, there are only two things which are actually mandatory for an adapter middleware to function: 24 | 25 | - a `#call` implementation 26 | - a call to `#save_response` inside `#call`, which will keep the Response around. 27 | 28 | These are the only two things. 29 | The rest of this text is about methods which make the authoring easier. 30 | 31 | ### Helpful method: `#build_connection` 32 | 33 | Faraday abstracts all your backend's concrete stuff behind its user-facing API. 34 | You take care of setting up the connection from the supplied parameters. 35 | 36 | Example from the excon adapter: it gets an `Env` and reads its information 37 | to instantiate an `Excon` object: 38 | 39 | ```ruby 40 | class FlorpHttp < ::Faraday::Adapter 41 | def build_connection(env) 42 | opts = opts_from_env(env) 43 | ::Excon.new(env[:url].to_s, opts.merge(@connection_options)) 44 | end 45 | end 46 | ``` 47 | 48 | The `env` contains stuff like: 49 | 50 | - `env[:ssl]` 51 | - `env[:request]` 52 | 53 | There are helper methods to fetch timeouts: `#request_timeout(type, options)` knows 54 | about supported timeout types, and falls back to `:timeout` if they are not set. 55 | You can use those when building the options you need for your backend's instantiation. 56 | 57 | So, use the information provided in `env` to instantiate your backend's connection class. 58 | Return that instance. Now, Faraday knows how to create and reuse that connection. 59 | 60 | ### Connection options and configuration block 61 | 62 | Users of your adapter have two main ways of configuring it: 63 | * connection options: these can be passed to your adapter initializer and are automatically stored into an instance variable `@connection_options`. 64 | * configuration block: this can also be provided to your adapter initializer and it's stored into an instance variable `@config_block`. 65 | 66 | Both of these are automatically managed by `Faraday::Adapter#initialize`, so remember to call it with `super` if you create an `initialize` method in your adapter. 67 | You can then use them in your adapter code as you wish, since they're pretty flexible. 68 | 69 | Below is an example of how they can be used: 70 | 71 | ```ruby 72 | # You can use @connection_options and @config_block in your adapter code 73 | class FlorpHttp < ::Faraday::Adapter 74 | def call(env) 75 | # `connection` internally calls `build_connection` and yields the result 76 | connection do |conn| 77 | # perform the request using configured `conn` 78 | end 79 | end 80 | 81 | def build_connection(env) 82 | conn = FlorpHttp::Client.new(pool_size: @connection_options[:pool_size] || 10) 83 | @config_block&.call(conn) 84 | conn 85 | end 86 | end 87 | 88 | # Then your users can provide them when initializing the connection 89 | Faraday.new(...) do |f| 90 | # ... 91 | # in this example, { pool_size: 5 } will be provided as `connection_options` 92 | f.adapter :florp_http, pool_size: 5 do |client| 93 | # this block is useful to set properties unique to HTTP clients that are not 94 | # manageable through the Faraday API 95 | client.some_fancy_florp_http_property = 10 96 | end 97 | end 98 | ``` 99 | 100 | ### Nickname for your adapter: `.register_middleware` 101 | 102 | You may register a nickname for your adapter. People can then refer to your adapter with that name. 103 | You do that using `.register_middleware`, like this: 104 | 105 | ```ruby 106 | class FlorpHttp < ::Faraday::Adapter 107 | # ... 108 | end 109 | 110 | Faraday::Adapter.register_middleware(florp_http: FlorpHttp) 111 | ``` 112 | 113 | ## Does your backend support parallel operation? 114 | 115 | :warning: This is slightly more involved, and this section is not fully formed. 116 | 117 | Vague example, excerpted from [the test suite about parallel requests](https://github.com/lostisland/faraday/blob/master/spec/support/shared_examples/request_method.rb#L179) 118 | 119 | ```ruby 120 | response_1 = nil 121 | response_2 = nil 122 | 123 | conn.in_parallel do 124 | response_1 = conn.get('/about') 125 | response_2 = conn.get('/products') 126 | end 127 | 128 | puts response_1.status 129 | puts response_2.status 130 | ``` 131 | 132 | First, in your class definition, you can tell Faraday that your backend supports parallel operation: 133 | 134 | ```ruby 135 | class FlorpHttp < ::Faraday::Adapter 136 | dependency do 137 | require 'florp_http' 138 | end 139 | 140 | self.supports_parallel = true 141 | end 142 | ``` 143 | 144 | Then, implement a method which returns a ParallelManager: 145 | 146 | ```ruby 147 | class FlorpHttp < ::Faraday::Adapter 148 | dependency do 149 | require 'florp_http' 150 | end 151 | 152 | self.supports_parallel = true 153 | 154 | def self.setup_parallel_manager(_options = nil) 155 | FlorpParallelManager.new # NB: we will need to define this 156 | end 157 | end 158 | 159 | class FlorpParallelManager 160 | def add(request, method, *args, &block) 161 | # Collect the requests 162 | end 163 | 164 | def run 165 | # Process the requests 166 | end 167 | end 168 | ``` 169 | 170 | Compare to the finished example [em-synchrony](https://github.com/lostisland/faraday-em_synchrony/blob/main/lib/faraday/adapter/em_synchrony.rb) 171 | and its [ParallelManager implementation](https://github.com/lostisland/faraday-em_synchrony/blob/main/lib/faraday/adapter/em_synchrony/parallel_manager.rb). 172 | --------------------------------------------------------------------------------