├── .editorconfig ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── publish.yml │ └── refresh_team_page.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .yardopts ├── CHANGELOG.md ├── Gemfile ├── LICENSE.md ├── README.md ├── Rakefile ├── UPGRADING.md ├── bin ├── console ├── setup └── test ├── config └── external.yaml ├── docs ├── .nojekyll ├── README.md ├── _media │ ├── favicon.png │ ├── home-logo.svg │ ├── logo.png │ ├── middleware.png │ ├── overview.png │ ├── repo-card-slim.png │ └── repo-card.png ├── _sidebar.md ├── adapters │ ├── custom │ │ ├── index.md │ │ ├── parallel-requests.md │ │ ├── streaming.md │ │ └── testing.md │ ├── index.md │ ├── net-http.md │ └── test-adapter.md ├── advanced │ ├── parallel-requests.md │ └── streaming-responses.md ├── customization │ ├── connection-options.md │ ├── index.md │ ├── proxy-options.md │ ├── request-options.md │ └── ssl-options.md ├── getting-started │ ├── env-object.md │ ├── errors.md │ └── quick-start.md ├── index.html ├── index.md └── middleware │ ├── custom-middleware.md │ ├── included │ ├── authentication.md │ ├── index.md │ ├── instrumentation.md │ ├── json.md │ ├── logging.md │ ├── raising-errors.md │ └── url-encoding.md │ └── index.md ├── examples ├── client_spec.rb └── client_test.rb ├── faraday.gemspec ├── lib ├── faraday.rb └── faraday │ ├── adapter.rb │ ├── adapter │ └── test.rb │ ├── adapter_registry.rb │ ├── connection.rb │ ├── encoders │ ├── flat_params_encoder.rb │ └── nested_params_encoder.rb │ ├── error.rb │ ├── logging │ └── formatter.rb │ ├── methods.rb │ ├── middleware.rb │ ├── middleware_registry.rb │ ├── options.rb │ ├── options │ ├── connection_options.rb │ ├── env.rb │ ├── proxy_options.rb │ ├── request_options.rb │ └── ssl_options.rb │ ├── parameters.rb │ ├── rack_builder.rb │ ├── request.rb │ ├── request │ ├── authorization.rb │ ├── instrumentation.rb │ ├── json.rb │ └── url_encoded.rb │ ├── response.rb │ ├── response │ ├── json.rb │ ├── logger.rb │ └── raise_error.rb │ ├── utils.rb │ ├── utils │ ├── headers.rb │ └── params_hash.rb │ └── version.rb ├── package-lock.json ├── package.json └── spec ├── external_adapters └── faraday_specs_setup.rb ├── faraday ├── adapter │ └── test_spec.rb ├── adapter_registry_spec.rb ├── adapter_spec.rb ├── connection_spec.rb ├── error_spec.rb ├── middleware_registry_spec.rb ├── middleware_spec.rb ├── options │ ├── env_spec.rb │ ├── options_spec.rb │ ├── proxy_options_spec.rb │ └── request_options_spec.rb ├── params_encoders │ ├── flat_spec.rb │ └── nested_spec.rb ├── rack_builder_spec.rb ├── request │ ├── authorization_spec.rb │ ├── instrumentation_spec.rb │ ├── json_spec.rb │ └── url_encoded_spec.rb ├── request_spec.rb ├── response │ ├── json_spec.rb │ ├── logger_spec.rb │ └── raise_error_spec.rb ├── response_spec.rb ├── utils │ └── headers_spec.rb └── utils_spec.rb ├── faraday_spec.rb ├── spec_helper.rb └── support ├── disabling_stub.rb ├── fake_safe_buffer.rb ├── faraday_middleware_subclasses.rb ├── helper_methods.rb ├── shared_examples ├── adapter.rb ├── params_encoder.rb └── request_method.rb └── streaming_response_checker.rb /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 Docs 68 | 69 | The Faraday Docs are included in the Faraday repository, under the `/docs` folder and deployed to [GitHub Pages][website]. 70 | If you want to apply changes to it, please test it locally before opening your PR. 71 | You can find more information in the [Faraday Docs README][docs], including how to preview changes locally. 72 | 73 | 74 | [semver]: https://semver.org/ 75 | [changelog]: https://github.com/lostisland/faraday/releases 76 | [faraday_middleware]: https://github.com/lostisland/faraday_middleware 77 | [website]: https://lostisland.github.io/faraday 78 | [docs]: ../docs/README.md 79 | [Code of Conduct]: ./CODE_OF_CONDUCT.md 80 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "bundler" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | - package-ecosystem: "npm" 12 | directory: / 13 | schedule: 14 | interval: "weekly" 15 | -------------------------------------------------------------------------------- /.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@v4 26 | 27 | - name: Setup Ruby 3.x 28 | uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: 3 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: [ '3.0', '3.1', '3.2', '3.3', '3.4' ] 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@v4 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 | continue-on-error: ${{ matrix.experimental }} 67 | run: bundle exec bake test:external 68 | 69 | 70 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | name: Publish to Rubygems 10 | runs-on: ubuntu-latest 11 | 12 | permissions: 13 | contents: write 14 | id-token: write 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Setup Ruby 3.x 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | bundler-cache: true 23 | ruby-version: 3 24 | 25 | - name: Publish to RubyGems 26 | uses: rubygems/release-gem@v1 27 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## PROJECT::GENERAL 2 | coverage 3 | rdoc 4 | doc 5 | log 6 | pkg/* 7 | tmp 8 | .rvmrc 9 | .ruby-version 10 | .yardoc 11 | .DS_Store 12 | 13 | ## BUNDLER 14 | *.gem 15 | .bundle 16 | Gemfile.lock 17 | vendor/bundle 18 | external 19 | 20 | ## NPM 21 | node_modules 22 | 23 | ## PROJECT::SPECIFIC 24 | .rbx 25 | 26 | ## IDEs 27 | .idea/ 28 | .yardoc/ 29 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --format documentation 3 | --color 4 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2023-12-27 11:12:52 UTC using RuboCop version 1.59.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: 13 27 | # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. 28 | Metrics/AbcSize: 29 | Max: 42 30 | 31 | # Offense count: 3 32 | # Configuration parameters: CountComments, CountAsOne. 33 | Metrics/ClassLength: 34 | Max: 230 35 | 36 | # Offense count: 9 37 | # Configuration parameters: AllowedMethods, AllowedPatterns. 38 | Metrics/CyclomaticComplexity: 39 | Max: 13 40 | 41 | # Offense count: 27 42 | # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. 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: 7 52 | # Configuration parameters: AllowedMethods, AllowedPatterns. 53 | Metrics/PerceivedComplexity: 54 | Max: 14 55 | 56 | # Offense count: 19 57 | # This cop supports safe autocorrection (--autocorrect). 58 | # Configuration parameters: AllowOnlyRestArgument, UseAnonymousForwarding, RedundantRestArgumentNames, RedundantKeywordRestArgumentNames, RedundantBlockArgumentNames. 59 | # RedundantRestArgumentNames: args, arguments 60 | # RedundantKeywordRestArgumentNames: kwargs, options, opts 61 | # RedundantBlockArgumentNames: blk, block, proc 62 | Style/ArgumentsForwarding: 63 | Exclude: 64 | - 'lib/faraday.rb' 65 | - 'lib/faraday/rack_builder.rb' 66 | 67 | # Offense count: 3 68 | Style/DocumentDynamicEvalDefinition: 69 | Exclude: 70 | - 'lib/faraday/connection.rb' 71 | - 'lib/faraday/options.rb' 72 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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', '~> 3.0' 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 'racc', '~> 1.7' # for RuboCop, on Ruby 3.3 23 | gem 'rubocop' 24 | gem 'rubocop-packaging', '~> 0.5' 25 | gem 'rubocop-performance', '~> 1.0' 26 | gem 'yard-junk' 27 | end 28 | 29 | gemspec 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [![Faraday](./docs/_media/home-logo.svg)][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 | Take a look at [Awesome Faraday][awesome] for a list of available adapters and middleware. 10 | 11 | ## Why use Faraday? 12 | 13 | Faraday gives you the power of Rack middleware for manipulating HTTP requests and responses, 14 | making it easier to build sophisticated API clients or web service libraries that abstract away 15 | the details of how HTTP requests are made. 16 | 17 | Faraday comes with a lot of features out of the box, such as: 18 | * Support for multiple adapters (Net::HTTP, Typhoeus, Patron, Excon, HTTPClient, and more) 19 | * Persistent connections (keep-alive) 20 | * Parallel requests 21 | * Automatic response parsing (JSON, XML, YAML) 22 | * Customization of the request/response cycle with middleware 23 | * Support for streaming responses 24 | * Support for uploading files 25 | * And much more! 26 | 27 | ## Getting Started 28 | 29 | The best starting point is the [Faraday Website][website], with its introduction and explanation. 30 | 31 | Need more details? See the [Faraday API Documentation][apidoc] to see how it works internally, or take a look at [Advanced techniques for calling HTTP APIs in Ruby](https://mattbrictson.com/blog/advanced-http-techniques-in-ruby) blog post from [@mattbrictson](https://github.com/mattbrictson) 🚀 32 | 33 | ## Supported Ruby versions 34 | 35 | This library aims to support and is [tested against][actions] the currently officially supported Ruby 36 | implementations. This means that, even without a major release, we could add or drop support for Ruby versions, 37 | following their [EOL](https://endoflife.date/ruby). 38 | Currently that means we support Ruby 3.0+ 39 | 40 | If something doesn't work on one of these Ruby versions, it's a bug. 41 | 42 | This library may inadvertently work (or seem to work) on other Ruby 43 | implementations and versions, however support will only be provided for the versions listed 44 | above. 45 | 46 | If you would like this library to support another Ruby version, you may 47 | volunteer to be a maintainer. Being a maintainer entails making sure all tests 48 | run and pass on that implementation. When something breaks on your 49 | implementation, you will be responsible for providing patches in a timely 50 | fashion. If critical issues for a particular implementation exist at the time 51 | of a major release, support for that Ruby version may be dropped. 52 | 53 | ## Contribute 54 | 55 | Do you want to contribute to Faraday? 56 | Open the issues page and check for the `help wanted` label! 57 | But before you start coding, please read our [Contributing Guide][contributing] 58 | 59 | ## Copyright 60 | 61 | © 2009 - 2023, the Faraday Team. Website and branding design by [Elena Lo Piccolo](https://elelopic.design). 62 | 63 | [awesome]: https://github.com/lostisland/awesome-faraday/#adapters 64 | [website]: https://lostisland.github.io/faraday 65 | [contributing]: https://github.com/lostisland/faraday/blob/main/.github/CONTRIBUTING.md 66 | [apidoc]: https://www.rubydoc.info/github/lostisland/faraday 67 | [actions]: https://github.com/lostisland/faraday/actions 68 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec/core/rake_task' 4 | require 'bundler' 5 | 6 | Bundler::GemHelper.install_tasks 7 | 8 | RSpec::Core::RakeTask.new(:spec) do |task| 9 | task.ruby_opts = %w[-W] 10 | end 11 | 12 | task default: :spec 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/external.yaml: -------------------------------------------------------------------------------- 1 | faraday-net_http: 2 | url: https://github.com/lostisland/faraday-net_http.git 3 | command: bundle exec rspec 4 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lostisland/faraday/bbaa093dbc629b697ce4b6dee4cd882d0eef80d1/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Faraday Docs 2 | 3 | Faraday Docs are powered by [Docsify](https://docsify.js.org/#/). 4 | 5 | ## Development 6 | 7 | ### Setup 8 | 9 | From the Faraday project root, run the following: 10 | 11 | ```bash 12 | npm install # this will install the necessary dependencies 13 | ``` 14 | 15 | ### Running the Docs Locally 16 | 17 | To preview your changes locally, run the following: 18 | 19 | ```bash 20 | npm run docs 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/_media/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lostisland/faraday/bbaa093dbc629b697ce4b6dee4cd882d0eef80d1/docs/_media/favicon.png -------------------------------------------------------------------------------- /docs/_media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lostisland/faraday/bbaa093dbc629b697ce4b6dee4cd882d0eef80d1/docs/_media/logo.png -------------------------------------------------------------------------------- /docs/_media/middleware.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lostisland/faraday/bbaa093dbc629b697ce4b6dee4cd882d0eef80d1/docs/_media/middleware.png -------------------------------------------------------------------------------- /docs/_media/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lostisland/faraday/bbaa093dbc629b697ce4b6dee4cd882d0eef80d1/docs/_media/overview.png -------------------------------------------------------------------------------- /docs/_media/repo-card-slim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lostisland/faraday/bbaa093dbc629b697ce4b6dee4cd882d0eef80d1/docs/_media/repo-card-slim.png -------------------------------------------------------------------------------- /docs/_media/repo-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lostisland/faraday/bbaa093dbc629b697ce4b6dee4cd882d0eef80d1/docs/_media/repo-card.png -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | * Getting Started 2 | * [Quick Start](getting-started/quick-start.md) 3 | * [The Env Object](getting-started/env-object.md) 4 | * [Dealing with Errors](getting-started/errors.md) 5 | * Customization 6 | * [Configuration](customization/index.md) 7 | * [Connection Options](customization/connection-options.md) 8 | * [Request Options](customization/request-options.md) 9 | * [SSL Options](customization/ssl-options.md) 10 | * [Proxy Options](customization/proxy-options.md) 11 | * Middleware 12 | * [Overview](middleware/index.md) 13 | * [Included](middleware/included/index.md) 14 | * [Authentication](middleware/included/authentication.md) 15 | * [URL Encoding](middleware/included/url-encoding.md) 16 | * [JSON Encoding/Decoding](middleware/included/json.md) 17 | * [Instrumentation](middleware/included/instrumentation.md) 18 | * [Logging](middleware/included/logging.md) 19 | * [Raising Errors](middleware/included/raising-errors.md) 20 | * [Writing custom middleware](middleware/custom-middleware.md) 21 | * Adapters 22 | * [Overview](adapters/index.md) 23 | * [Net::HTTP](adapters/net-http.md) 24 | * [Test Adapter](adapters/test-adapter.md) 25 | * Writing custom adapters 26 | * [Overview](adapters/custom/index.md) 27 | * [Parallel Requests](adapters/custom/parallel-requests.md) 28 | * [Streaming Responses](adapters/custom/streaming.md) 29 | * [Test your adapter](adapters/custom/testing.md) 30 | * Advanced Features 31 | * [Parallel Requests](advanced/parallel-requests.md) 32 | * [Streaming Responses](advanced/streaming-responses.md) 33 | -------------------------------------------------------------------------------- /docs/adapters/custom/parallel-requests.md: -------------------------------------------------------------------------------- 1 | # Adding support for parallel requests 2 | 3 | !> This is slightly more involved, and this section is not fully formed. 4 | 5 | Vague example, excerpted from [the test suite about parallel requests](https://github.com/lostisland/faraday/blob/main/spec/support/shared_examples/request_method.rb#L179) 6 | 7 | ```ruby 8 | response_1 = nil 9 | response_2 = nil 10 | 11 | conn.in_parallel do 12 | response_1 = conn.get('/about') 13 | response_2 = conn.get('/products') 14 | end 15 | 16 | puts response_1.status 17 | puts response_2.status 18 | ``` 19 | 20 | First, in your class definition, you can tell Faraday that your backend supports parallel operation: 21 | 22 | ```ruby 23 | class FlorpHttp < ::Faraday::Adapter 24 | dependency do 25 | require 'florp_http' 26 | end 27 | 28 | self.supports_parallel = true 29 | end 30 | ``` 31 | 32 | Then, implement a method which returns a ParallelManager: 33 | 34 | ```ruby 35 | class FlorpHttp < ::Faraday::Adapter 36 | dependency do 37 | require 'florp_http' 38 | end 39 | 40 | self.supports_parallel = true 41 | 42 | def self.setup_parallel_manager(_options = nil) 43 | FlorpParallelManager.new # NB: we will need to define this 44 | end 45 | 46 | def call(env) 47 | # NB: you can call `in_parallel?` here to check if the current request 48 | # is part of a parallel batch. Useful if you need to collect all requests 49 | # into the ParallelManager before running them. 50 | end 51 | end 52 | 53 | class FlorpParallelManager 54 | # The execute method will be passed the same block as `in_parallel`, 55 | # so you can either collect the requests or just wrap them into a wrapper, 56 | # depending on how your adapter works. 57 | def execute(&block) 58 | run_async(&block) 59 | end 60 | end 61 | ``` 62 | 63 | ### A note on the old, deprecated interface 64 | 65 | Prior to the introduction of the `execute` method, the `ParallelManager` was expected to implement a `run` method 66 | and the execution of the block was done by the Faraday connection BEFORE calling that method. 67 | 68 | This approach made the `ParallelManager` implementation harder and forced you to keep state around. 69 | The new `execute` implementation allows to avoid this shortfall and support different flows. 70 | 71 | As of Faraday 2.0, `run` is still supported in case `execute` is not implemented by the `ParallelManager`, 72 | but this method should be considered deprecated. 73 | 74 | For reference, please see an example using `run` from [em-synchrony](https://github.com/lostisland/faraday-em_synchrony/blob/main/lib/faraday/adapter/em_synchrony.rb) 75 | and its [ParallelManager implementation](https://github.com/lostisland/faraday-em_synchrony/blob/main/lib/faraday/adapter/em_synchrony/parallel_manager.rb). 76 | -------------------------------------------------------------------------------- /docs/adapters/custom/streaming.md: -------------------------------------------------------------------------------- 1 | # Adding support for streaming 2 | 3 | Faraday supports streaming responses, which means that the response body is not loaded into memory all at once, 4 | but instead it is read in chunks. This can be particularly useful when dealing with large responses. 5 | Not all HTTP clients support this, so it is not required for adapters to support it. 6 | 7 | However, if you do want to support it in your adapter, you can do so by leveraging helpers provided by the env object. 8 | Let's see an example implementation first with some comments, and then we'll explain it in more detail: 9 | 10 | ```ruby 11 | module Faraday 12 | class Adapter 13 | class FlorpHttp < Faraday::Adapter 14 | def call(env) 15 | super 16 | if env.stream_response? # check if the user wants to stream the response 17 | # start a streaming response. 18 | # on_data is a block that will let users consume the response body 19 | http_response = env.stream_response do |&on_data| 20 | # perform the request using FlorpHttp 21 | # the block will be called for each chunk of data 22 | FlorpHttp.perform_request(...) do |chunk| 23 | on_data.call(chunk) 24 | end 25 | end 26 | # the body is already consumed by the block 27 | # so it's good practice to set it to nil 28 | http_response.body = nil 29 | else 30 | # perform the request normally, no streaming. 31 | http_response = FlorpHttp.perform_request(...) 32 | end 33 | save_response(env, http_response.status, http_response.body, http_response.headers, http_response.reason_phrase) 34 | end 35 | end 36 | end 37 | end 38 | ``` 39 | 40 | ## How it works 41 | 42 | ### `#stream_response?` 43 | 44 | The first helper method we use is `#stream_response?`. This method is provided by the env object and it returns true 45 | if the user wants to stream the response. This is controlled by the presence of an `on_data` callback in the request options. 46 | 47 | ### `#stream_response` 48 | 49 | The second helper is `#stream_response`. This method is also provided by the env object and it takes a block. 50 | The block will be called with a single argument, which is a callback that the user can use to consume the response body. 51 | All your adapter needs to do, is to call this callback with each chunk of data that you receive from the server. 52 | 53 | The `on_data` callback will internally call the callback provided by the user, so you don't need to worry about that. 54 | It will also keep track of the number of bytes that have been read, and pass that information to the user's callback. 55 | 56 | To see how this all works together, let's see an example of how a user would use this feature: 57 | 58 | ```ruby 59 | # A buffer to store the streamed data 60 | streamed = [] 61 | 62 | conn = Faraday.new('http://httpbingo.org') 63 | conn.get('/stream/100') do |req| 64 | # Set a callback which will receive tuples of chunk Strings, 65 | # the sum of characters received so far, and the response environment. 66 | # The latter will allow access to the response status, headers and reason, as well as the request info. 67 | req.options.on_data = proc do |chunk, overall_received_bytes, env| 68 | puts "Received #{overall_received_bytes} characters" 69 | streamed << chunk 70 | end 71 | end 72 | 73 | # Joins all response chunks together 74 | streamed.join 75 | ``` 76 | 77 | For more details on the user experience, check the [Streaming Responses] page. 78 | 79 | [Streaming Responses]: /advanced/streaming-responses.md 80 | -------------------------------------------------------------------------------- /docs/adapters/custom/testing.md: -------------------------------------------------------------------------------- 1 | # Test your custom adapter 2 | 3 | Faraday puts a lot of expectations on adapters, but it also provides you with useful tools to test your adapter 4 | against those expectations. This guide will walk you through the process of testing your adapter. 5 | 6 | ## The adapter test suite 7 | 8 | Faraday provides a test suite that you can use to test your adapter. 9 | The test suite is located in the `spec/external_adapters/faraday_specs_setup.rb`. 10 | 11 | All you need to do is to `require 'faraday_specs_setup'` in your adapter's `spec_helper.rb` file. 12 | This will load the `an adapter` shared example group that you can use to test your adapter. 13 | 14 | ```ruby 15 | require 'faraday_specs_setup' 16 | 17 | RSpec.describe Faraday::Adapter::FlorpHttp do 18 | it_behaves_like 'an adapter' 19 | 20 | # You can then add any other test specific to this adapter here... 21 | end 22 | ``` 23 | 24 | ## Testing optional features 25 | 26 | By default, `an adapter` will test your adapter against the required behaviour for an adapter. 27 | However, there are some optional "features" that your adapter can implement, like parallel requests or streaming. 28 | 29 | If your adapter implements any of those optional features, you can test it against those extra expectations 30 | by calling the `features` method: 31 | 32 | ```ruby 33 | RSpec.describe Faraday::Adapter::MyAdapter do 34 | # Since not all adapters support all the features Faraday has to offer, you can use 35 | # the `features` method to turn on only the ones you know you can support. 36 | features :request_body_on_query_methods, 37 | :compression, 38 | :streaming 39 | 40 | # Runs the tests provide by Faraday, according to the features specified above. 41 | it_behaves_like 'an adapter' 42 | 43 | # You can then add any other test specific to this adapter here... 44 | end 45 | ``` 46 | 47 | ### Available features 48 | 49 | | Feature | Description | 50 | |----------------------------------|----------------------------------------------------------------------------------------------------------| 51 | | `:compression` | Tests that your adapter can handle `gzip` and `deflate` compressions. | 52 | | `:local_socket_binding` | Tests that your adapter supports binding to a local socket via the `:bind` request option. | 53 | | `:parallel` | Tests that your adapter supports parallel requests. See [Parallel requests][parallel] for more details. | 54 | | `:reason_phrase_parse` | Tests that your adapter supports parsing the `reason_phrase` from the response. | 55 | | `:request_body_on_query_methods` | Tests that your adapter supports sending a request body on `GET`, `HEAD`, `DELETE` and `TRACE` requests. | 56 | | `:streaming` | Tests that your adapter supports streaming responses. See [Streaming][streaming] for more details. | 57 | | `:trace_method` | Tests your adapter against the `TRACE` HTTP method. | 58 | 59 | [streaming]: /adapters/custom/streaming.md 60 | [parallel]: /adapters/custom/parallel-requests.md 61 | -------------------------------------------------------------------------------- /docs/adapters/index.md: -------------------------------------------------------------------------------- 1 | # Adapters 2 | 3 | The Faraday Adapter interface determines how a Faraday request is turned into 4 | a Faraday response object. Adapters are typically implemented with common Ruby 5 | HTTP clients, but can have custom implementations. Adapters can be configured 6 | either globally or per Faraday Connection through the configuration block. 7 | 8 | For example, consider using `httpclient` as an adapter. Note that [faraday-httpclient](https://github.com/lostisland/faraday-httpclient) must be installed beforehand. 9 | 10 | If you want to configure it globally, do the following: 11 | 12 | ```ruby 13 | require 'faraday' 14 | require 'faraday/httpclient' 15 | 16 | Faraday.default_adapter = :httpclient 17 | ``` 18 | 19 | If you want to configure it per Faraday Connection, do the following: 20 | 21 | ```ruby 22 | require 'faraday' 23 | require 'faraday/httpclient' 24 | 25 | conn = Faraday.new do |f| 26 | f.adapter :httpclient 27 | end 28 | ``` 29 | 30 | ## Fantastic adapters and where to find them 31 | 32 | Except for the default [Net::HTTP][net_http] adapter and the [Test Adapter][testing] adapter, which is for _test purposes only_, 33 | adapters are distributed separately from Faraday and need to be manually installed. 34 | They are usually available as gems, or bundled with HTTP clients. 35 | 36 | While most adapters use a common Ruby HTTP client library, adapters can also 37 | have completely custom implementations. 38 | 39 | If you're just getting started you can find a list of featured adapters in [Awesome Faraday][awesome]. 40 | Anyone can create a Faraday adapter and distribute it. If you're interested learning more, check how to [build your own][build_adapters]! 41 | 42 | 43 | [testing]: /adapters/test-adapter.md 44 | [net_http]: /adapters/net-http.md 45 | [awesome]: https://github.com/lostisland/awesome-faraday/#adapters 46 | [build_adapters]: /adapters/custom/index.md 47 | -------------------------------------------------------------------------------- /docs/adapters/net-http.md: -------------------------------------------------------------------------------- 1 | # Net::HTTP Adapter 2 | 3 | Faraday's Net::HTTP adapter is the default adapter. It uses the `Net::HTTP` 4 | library that ships with Ruby's standard library. 5 | Unless you have a specific reason to use a different adapter, this is probably 6 | the adapter you want to use. 7 | 8 | With the release of Faraday 2.0, the Net::HTTP adapter has been moved into a [separate gem][faraday-net_http], 9 | but it has also been added as a dependency of Faraday. 10 | That means you can use it without having to install it or require it explicitly. 11 | 12 | [faraday-net_http]: https://github.com/lostisland/faraday-net_http 13 | -------------------------------------------------------------------------------- /docs/adapters/test-adapter.md: -------------------------------------------------------------------------------- 1 | # Test Adapter 2 | 3 | The built-in Faraday Test adapter lets you define stubbed HTTP requests. This can 4 | be used to mock out network services in an application's unit tests. 5 | 6 | The easiest way to do this is to create the stubbed requests when initializing 7 | a `Faraday::Connection`. Stubbing a request by path yields a block with a 8 | `Faraday::Env` object. The stub block expects an Array return value with three 9 | values: an Integer HTTP status code, a Hash of key/value headers, and a 10 | response body. 11 | 12 | ```ruby 13 | conn = Faraday.new do |builder| 14 | builder.adapter :test do |stub| 15 | # block returns an array with 3 items: 16 | # - Integer response status 17 | # - Hash HTTP headers 18 | # - String response body 19 | stub.get('/ebi') do |env| 20 | [ 21 | 200, 22 | { 'Content-Type': 'text/plain', }, 23 | 'shrimp' 24 | ] 25 | end 26 | 27 | # test exceptions too 28 | stub.get('/boom') do 29 | raise Faraday::ConnectionFailed 30 | end 31 | end 32 | end 33 | ``` 34 | 35 | You can define the stubbed requests outside of the test adapter block: 36 | 37 | ```ruby 38 | stubs = Faraday::Adapter::Test::Stubs.new do |stub| 39 | stub.get('/tamago') { |env| [200, {}, 'egg'] } 40 | end 41 | ``` 42 | 43 | This Stubs instance can be passed to a new Connection: 44 | 45 | ```ruby 46 | conn = Faraday.new do |builder| 47 | builder.adapter :test, stubs do |stub| 48 | stub.get('/ebi') { |env| [ 200, {}, 'shrimp' ]} 49 | end 50 | end 51 | ``` 52 | 53 | It's also possible to stub additional requests after the connection has been 54 | initialized. This is useful for testing. 55 | 56 | ```ruby 57 | stubs.get('/uni') { |env| [ 200, {}, 'urchin' ]} 58 | ``` 59 | 60 | You can also stub the request body with a string or a proc. 61 | It would be useful to pass a proc if it's OK only to check the parts of the request body are passed. 62 | 63 | ```ruby 64 | stubs.post('/kohada', 'where=sea&temperature=24') { |env| [ 200, {}, 'spotted gizzard shad' ]} 65 | stubs.post('/anago', -> (request_body) { JSON.parse(request_body).slice('name') == { 'name' => 'Wakamoto' } }) { |env| [200, {}, 'conger eel'] } 66 | ``` 67 | 68 | If you want to stub requests that exactly match a path, parameters, and headers, 69 | `strict_mode` would be useful. 70 | 71 | ```ruby 72 | stubs = Faraday::Adapter::Test::Stubs.new(strict_mode: true) do |stub| 73 | stub.get('/ikura?nori=true', 'X-Soy-Sauce' => '5ml' ) { |env| [200, {}, 'ikura gunkan maki'] } 74 | end 75 | ``` 76 | 77 | This stub expects the connection will be called like this: 78 | 79 | ```ruby 80 | conn.get('/ikura', { nori: 'true' }, { 'X-Soy-Sauce' => '5ml' } ) 81 | ``` 82 | 83 | If there are other parameters or headers included, the Faraday Test adapter 84 | will raise `Faraday::Test::Stubs::NotFound`. It also raises the error 85 | if the specified parameters (`nori`) or headers (`X-Soy-Sauce`) are omitted. 86 | 87 | You can also enable `strict_mode` after initializing the connection. 88 | In this case, all requests, including ones that have been already stubbed, 89 | will be handled in a strict way. 90 | 91 | ```ruby 92 | stubs.strict_mode = true 93 | ``` 94 | 95 | Finally, you can treat your stubs as mocks by verifying that all of the stubbed 96 | calls were made. NOTE: this feature is still fairly experimental. It will not 97 | verify the order or count of any stub. 98 | 99 | ```ruby 100 | stubs.verify_stubbed_calls 101 | ``` 102 | 103 | After the test case is completed (possibly in an `after` hook), you should clear 104 | the default connection to prevent it from being cached between different tests. 105 | This allows for each test to have its own set of stubs 106 | 107 | ```ruby 108 | Faraday.default_connection = nil 109 | ``` 110 | 111 | ## Examples 112 | 113 | Working [RSpec] and [test/unit] examples for a fictional JSON API client are 114 | available. 115 | 116 | [RSpec]: https://github.com/lostisland/faraday/blob/main/examples/client_spec.rb 117 | [test/unit]: https://github.com/lostisland/faraday/blob/main/examples/client_test.rb 118 | -------------------------------------------------------------------------------- /docs/advanced/parallel-requests.md: -------------------------------------------------------------------------------- 1 | # Parallel Requests 2 | 3 | Some adapters support running requests in parallel. 4 | This can be achieved using the `#in_parallel` method on the connection object. 5 | 6 | ```ruby 7 | # Install the Typhoeus adapter with `gem install faraday-typhoeus` first. 8 | require 'faraday/typhoeus' 9 | 10 | conn = Faraday.new('http://httpbingo.org') do |faraday| 11 | faraday.adapter :typhoeus 12 | end 13 | 14 | now = Time.now 15 | 16 | conn.in_parallel do 17 | conn.get('/delay/3') 18 | conn.get('/delay/3') 19 | end 20 | 21 | # This should take about 3 seconds, not 6. 22 | puts "Time taken: #{Time.now - now}" 23 | ``` 24 | 25 | ## A note on Async 26 | 27 | You might have heard about [Async] and its native integration with Ruby 3.0. 28 | The good news is that you can already use Async with Faraday (thanks to the [async-http-faraday] gem) 29 | and this does not require the use of `#in_parallel` to run parallel requests. 30 | Instead, you only need to wrap your Faraday code into an Async block: 31 | 32 | ```ruby 33 | # Install the Async adapter with `gem install async-http-faraday` first. 34 | require 'async/http/faraday' 35 | 36 | conn = Faraday.new('http://httpbingo.org') do |faraday| 37 | faraday.adapter :async_http 38 | end 39 | 40 | now = Time.now 41 | 42 | # NOTE: This is not limited to a single connection anymore! 43 | # You can run parallel requests spanning multiple connections. 44 | Async do 45 | Async { conn.get('/delay/3') } 46 | Async { conn.get('/delay/3') } 47 | end 48 | 49 | # This should take about 3 seconds, not 6. 50 | puts "Time taken: #{Time.now - now}" 51 | 52 | ``` 53 | 54 | The big advantage of using Async is that you can now run parallel requests *spanning multiple connections*, 55 | whereas the `#in_parallel` method only works for requests that are made through the same connection. 56 | 57 | [Async]: https://github.com/socketry/async 58 | [async-http-faraday]: https://github.com/socketry/async-http-faraday 59 | -------------------------------------------------------------------------------- /docs/advanced/streaming-responses.md: -------------------------------------------------------------------------------- 1 | # Streaming Responses 2 | 3 | Sometimes you might need to receive a streaming response. 4 | You can do this with the `on_data` request option. 5 | 6 | The `on_data` callback is a receives tuples of chunk Strings, and the total 7 | of received bytes so far. 8 | 9 | This example implements such a callback: 10 | 11 | ```ruby 12 | # A buffer to store the streamed data 13 | streamed = [] 14 | 15 | conn = Faraday.new('http://httpbingo.org') 16 | conn.get('/stream/100') do |req| 17 | # Set a callback which will receive tuples of chunk Strings, 18 | # the sum of characters received so far, and the response environment. 19 | # The latter will allow access to the response status, headers and reason, as well as the request info. 20 | req.options.on_data = Proc.new do |chunk, overall_received_bytes, env| 21 | puts "Received #{overall_received_bytes} characters" 22 | streamed << chunk 23 | end 24 | end 25 | 26 | # Joins all response chunks together 27 | streamed.join 28 | ``` 29 | 30 | The `on_data` streaming is currently only supported by some adapters. 31 | To see which ones, please refer to [Awesome Faraday][awesome] comparative table or check the adapter documentation. 32 | Moreover, the `env` parameter was only recently added, which means some adapters may only have partial support 33 | (i.e. only `chunk` and `overall_received_bytes` will be passed to your block). 34 | 35 | [awesome]: https://github.com/lostisland/awesome-faraday/#adapters 36 | -------------------------------------------------------------------------------- /docs/customization/connection-options.md: -------------------------------------------------------------------------------- 1 | # Connection Options 2 | 3 | When initializing a new Faraday connection with `Faraday.new`, you can pass a hash of options to customize the connection. 4 | All these options are optional. 5 | 6 | | Option | Type | Default | Description | 7 | |---------------------|-------------------|-----------------|---------------------------------------------------------------------------------------------------------------| 8 | | `:request` | Hash | nil | Hash of request options. Will be use to build [RequestOptions]. | 9 | | `:proxy` | URI, String, Hash | nil | Proxy options, either as a URL or as a Hash of [ProxyOptions]. | 10 | | `:ssl` | Hash | nil | Hash of SSL options. Will be use to build [SSLOptions]. | 11 | | `:url` | URI, String | nil | URI or String base URL. This can also be passed as positional argument. | 12 | | `:parallel_manager` | | nil | Default parallel manager to use. This is normally set by the adapter, but you have the option to override it. | 13 | | `:params` | Hash | nil | URI query unencoded key/value pairs. | 14 | | `:headers` | Hash | nil | Hash of unencoded HTTP header key/value pairs. | 15 | | `:builder_class` | Class | RackBuilder | A custom class to use as the middleware stack builder. | 16 | | `:builder` | Object | Rackbuilder.new | An instance of a custom class to use as the middleware stack builder. | 17 | 18 | ## Example 19 | 20 | ```ruby 21 | options = { 22 | request: { 23 | open_timeout: 5, 24 | timeout: 5 25 | }, 26 | proxy: { 27 | uri: 'https://proxy.com', 28 | user: 'proxy_user', 29 | password: 'proxy_password' 30 | }, 31 | ssl: { 32 | ca_file: '/path/to/ca_file', 33 | ca_path: '/path/to/ca_path', 34 | verify: true 35 | }, 36 | url: 'https://example.com', 37 | params: { foo: 'bar' }, 38 | headers: { 'X-Api-Key' => 'secret', 'X-Api-Version' => '2' } 39 | } 40 | 41 | conn = Faraday.new(options) do |faraday| 42 | # ... 43 | end 44 | ``` 45 | 46 | [RequestOptions]: /customization/request-options.md 47 | [ProxyOptions]: /customization/proxy-options.md 48 | [SSLOptions]: /customization/ssl-options.md 49 | -------------------------------------------------------------------------------- /docs/customization/index.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Faraday is highly configurable and allows you to customize the way requests are made. 4 | This applies to both the connection and the request, but can also cover things like SSL and proxy settings. 5 | 6 | Below are some examples of how to customize Faraday requests. 7 | Configuration can be set up with the connection and/or adjusted per request. 8 | 9 | As connection options: 10 | 11 | ```ruby 12 | conn = Faraday.new('http://httpbingo.org', request: { timeout: 5 }) 13 | conn.get('/ip') 14 | ``` 15 | 16 | Or as per-request options: 17 | 18 | ```ruby 19 | conn.get do |req| 20 | req.url '/ip' 21 | req.options.timeout = 5 22 | end 23 | ``` 24 | 25 | You can also inject arbitrary data into the request using the `context` option. 26 | This will be available in the `env` on all middleware. 27 | 28 | ```ruby 29 | conn.get do |req| 30 | req.url '/get' 31 | req.options.context = { 32 | foo: 'foo', 33 | bar: 'bar' 34 | } 35 | end 36 | ``` 37 | 38 | ## Changing how parameters are serialized 39 | 40 | Sometimes you need to send the same URL parameter multiple times with different values. 41 | This requires manually setting the parameter encoder and can be done on 42 | either per-connection or per-request basis. 43 | This applies to all HTTP verbs. 44 | 45 | Per-connection setting: 46 | 47 | ```ruby 48 | conn = Faraday.new request: { params_encoder: Faraday::FlatParamsEncoder } 49 | conn.get('', { roll: ['california', 'philadelphia'] }) 50 | ``` 51 | 52 | Per-request setting: 53 | 54 | ```ruby 55 | conn.get do |req| 56 | req.options.params_encoder = Faraday::FlatParamsEncoder 57 | req.params = { roll: ['california', 'philadelphia'] } 58 | end 59 | ``` 60 | 61 | ### Custom serializers 62 | 63 | You can build your custom encoder, if you like. 64 | 65 | The value of Faraday `params_encoder` can be any object that responds to: 66 | 67 | * `#encode(hash) #=> String` 68 | * `#decode(string) #=> Hash` 69 | 70 | The encoder will affect both how Faraday processes query strings and how it 71 | serializes POST bodies. 72 | 73 | The default encoder is `Faraday::NestedParamsEncoder`. 74 | 75 | ### Order of parameters 76 | 77 | By default, parameters are sorted by name while being serialized. 78 | 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. 79 | However you might find yourself dealing with a server that requires parameters to be in a specific order. 80 | When that happens, you can configure the encoder to skip sorting them. 81 | This configuration is supported by both the default `Faraday::NestedParamsEncoder` and `Faraday::FlatParamsEncoder`: 82 | 83 | ```ruby 84 | Faraday::NestedParamsEncoder.sort_params = false 85 | # or 86 | Faraday::FlatParamsEncoder.sort_params = false 87 | ``` 88 | 89 | ## Proxy 90 | 91 | Faraday will try to automatically infer the proxy settings from your system using [`URI#find_proxy`][ruby-find-proxy]. 92 | This will retrieve them from environment variables such as http_proxy, ftp_proxy, no_proxy, etc. 93 | If for any reason you want to disable this behaviour, you can do so by setting the global variable `ignore_env_proxy`: 94 | 95 | ```ruby 96 | Faraday.ignore_env_proxy = true 97 | ``` 98 | 99 | You can also specify a custom proxy when initializing the connection: 100 | 101 | ```ruby 102 | conn = Faraday.new('http://www.example.com', proxy: 'http://proxy.com') 103 | ``` 104 | 105 | [ruby-find-proxy]: https://ruby-doc.org/stdlib-2.6.3/libdoc/uri/rdoc/URI/Generic.html#method-i-find_proxy 106 | -------------------------------------------------------------------------------- /docs/customization/proxy-options.md: -------------------------------------------------------------------------------- 1 | # Proxy Options 2 | 3 | Proxy options can be provided to the connection constructor or set on a per-request basis via [RequestOptions](/customization/request-options.md). 4 | All these options are optional. 5 | 6 | | Option | Type | Default | Description | 7 | |-------------|-------------|---------|-----------------| 8 | | `:uri` | URI, String | nil | Proxy URL. | 9 | | `:user` | String | nil | Proxy user. | 10 | | `:password` | String | nil | Proxy password. | 11 | 12 | ## Example 13 | 14 | ```ruby 15 | # Proxy options can be passed to the connection constructor and will be applied to all requests. 16 | proxy_options = { 17 | uri: 'http://proxy.example.com:8080', 18 | user: 'username', 19 | password: 'password' 20 | } 21 | 22 | conn = Faraday.new(proxy: proxy_options) do |faraday| 23 | # ... 24 | end 25 | 26 | # You can then override them on a per-request basis. 27 | conn.get('/foo') do |req| 28 | req.options.proxy.update(uri: 'http://proxy2.example.com:8080') 29 | end 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/customization/request-options.md: -------------------------------------------------------------------------------- 1 | # Request Options 2 | 3 | Request options can be provided to the connection constructor or set on a per-request basis. 4 | All these options are optional. 5 | 6 | | Option | Type | Default | Description | 7 | |-------------------|-------------------|----------------------------------------------------------------|-------------------------------------------------------------------------| 8 | | `:params_encoder` | Class | `Faraday::Utils.nested_params_encoder` (`NestedParamsEncoder`) | A custom class to use as the params encoder. | 9 | | `:proxy` | URI, String, Hash | nil | Proxy options, either as a URL or as a Hash of [ProxyOptions]. | 10 | | `:bind` | Hash | nil | Hash of bind options. Requires the `:host` and `:port` keys. | 11 | | `:timeout` | Integer, Float | nil (adapter default) | The max number of seconds to wait for the request to complete. | 12 | | `:open_timeout` | Integer, Float | nil (adapter default) | The max number of seconds to wait for the connection to be established. | 13 | | `:read_timeout` | Integer, Float | nil (adapter default) | The max number of seconds to wait for one block to be read. | 14 | | `:write_timeout` | Integer, Float | nil (adapter default) | The max number of seconds to wait for one block to be written. | 15 | | `:boundary` | String | nil | The boundary string for multipart requests. | 16 | | `:context` | Hash | nil | Arbitrary data that you can pass to your request. | 17 | | `:on_data` | Proc | nil | A callback that will be called when data is received. See [Streaming] | 18 | 19 | ## Example 20 | 21 | ```ruby 22 | # Request options can be passed to the connection constructor and will be applied to all requests. 23 | request_options = { 24 | params_encoder: Faraday::FlatParamsEncoder, 25 | timeout: 5 26 | } 27 | 28 | conn = Faraday.new(request: request_options) do |faraday| 29 | # ... 30 | end 31 | 32 | # You can then override them on a per-request basis. 33 | conn.get('/foo') do |req| 34 | req.options.timeout = 10 35 | end 36 | ``` 37 | 38 | [ProxyOptions]: /customization/proxy-options.md 39 | [SSLOptions]: /advanced/streaming-responses.md 40 | -------------------------------------------------------------------------------- /docs/customization/ssl-options.md: -------------------------------------------------------------------------------- 1 | # SSL Options 2 | 3 | Faraday supports a number of SSL options, which can be provided while initializing the connection. 4 | 5 | | Option | Type | Default | Description | 6 | |--------------------|----------------------------------------|---------|------------------------------------------------------------------------------------------------------------------------------------| 7 | | `:verify` | Boolean | true | Verify SSL certificate. Defaults to `true`. | 8 | | `:verify_hostname` | Boolean | true | Verify SSL certificate hostname. Defaults to `true`. | 9 | | `:hostname` | String | nil | Server hostname for SNI (see [SSL docs](https://ruby-doc.org/3.2.2/exts/openssl/OpenSSL/SSL/SSLSocket.html#method-i-hostname-3D)). | 10 | | `:ca_file` | String | nil | Path to a CA file in PEM format. | 11 | | `:ca_path` | String | nil | Path to a CA directory. | 12 | | `:verify_mode` | Integer | nil | Any `OpenSSL::SSL::` constant (see [SSL docs](https://ruby-doc.org/3.2.2/exts/openssl/OpenSSL/SSL.html)). | 13 | | `:cert_store` | OpenSSL::X509::Store | nil | OpenSSL certificate store. | 14 | | `:client_cert` | OpenSSL::X509::Certificate | nil | Client certificate. | 15 | | `:client_key` | OpenSSL::PKey::RSA, OpenSSL::PKey::DSA | nil | Client private key. | 16 | | `:certificate` | OpenSSL::X509::Certificate | nil | Certificate (Excon only). | 17 | | `:private_key` | OpenSSL::PKey::RSA | nil | Private key (Excon only). | 18 | | `:verify_depth` | Integer | nil | Maximum depth for the certificate chain verification. | 19 | | `:version` | Integer | nil | SSL version (see [SSL docs](https://ruby-doc.org/3.2.2/exts/openssl/OpenSSL/SSL/SSLContext.html#method-i-ssl_version-3D)). | 20 | | `:min_version` | Integer | nil | Minimum SSL version (see [SSL docs](https://ruby-doc.org/3.2.2/exts/openssl/OpenSSL/SSL/SSLContext.html#method-i-min_version-3D)). | 21 | | `:max_version` | Integer | nil | Maximum SSL version (see [SSL docs](https://ruby-doc.org/3.2.2/exts/openssl/OpenSSL/SSL/SSLContext.html#method-i-max_version-3D)). | 22 | | `:ciphers` | String | nil | Ciphers supported (see [SSL docs](https://ruby-doc.org/3.2.2/exts/openssl/OpenSSL/SSL/SSLContext.html#method-i-ciphers-3D)). | 23 | 24 | ## Example 25 | 26 | ```ruby 27 | ssl_options = { 28 | ca_file: '/path/to/ca_file', 29 | min_version: :TLS1_2 30 | } 31 | 32 | conn = Faraday.new(ssl: options) do |faraday| 33 | # ... 34 | end 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/getting-started/env-object.md: -------------------------------------------------------------------------------- 1 | # The Env Object 2 | 3 | Inspired by Rack, Faraday uses an `env` object to pass data between middleware. 4 | This object is initialized at the beginning of the request and passed down the middleware stack. 5 | The adapter is then responsible to run the HTTP request and set the `response` property on the `env` object, 6 | which is then passed back up the middleware stack. 7 | 8 | You can read more about how the `env` object is used in the [Middleware - How it works](/middleware/index?id=how-it-works) section. 9 | 10 | Because of its nature, the `env` object is a complex structure that holds a lot of information and can 11 | therefore be a bit intimidating at first. This page will try to explain the different properties of the `env` object. 12 | 13 | ## Properties 14 | 15 | Please also note that these properties are not all available at the same time: while configuration 16 | and request properties are available at the beginning of the request, response properties are only 17 | available after the request has been performed (i.e. in the `on_complete` callback of middleware). 18 | 19 | 20 | | Property | Type | Request | Response | Description | 21 | |---------------------|----------------------------|:------------------:|:------------------:|-----------------------------| 22 | | `:method` | `Symbol` | :heavy_check_mark: | :heavy_check_mark: | The HTTP method to use. | 23 | | `:request_body` | `String` | :heavy_check_mark: | :heavy_check_mark: | The request body. | 24 | | `:url` | `URI` | :heavy_check_mark: | :heavy_check_mark: | The request URL. | 25 | | `:request` | `Faraday::RequestOptions` | :heavy_check_mark: | :heavy_check_mark: | The request options. | 26 | | `:request_headers` | `Faraday::Utils::Headers` | :heavy_check_mark: | :heavy_check_mark: | The request headers. | 27 | | `:ssl` | `Faraday::SSLOptions` | :heavy_check_mark: | :heavy_check_mark: | The SSL options. | 28 | | `:parallel_manager` | `Faraday::ParallelManager` | :heavy_check_mark: | :heavy_check_mark: | The parallel manager. | 29 | | `:params` | `Hash` | :heavy_check_mark: | :heavy_check_mark: | The request params. | 30 | | `:response` | `Faraday::Response` | :x: | :heavy_check_mark: | The response. | 31 | | `:response_headers` | `Faraday::Utils::Headers` | :x: | :heavy_check_mark: | The response headers. | 32 | | `:status` | `Integer` | :x: | :heavy_check_mark: | The response status code. | 33 | | `:reason_phrase` | `String` | :x: | :heavy_check_mark: | The response reason phrase. | 34 | | `:response_body` | `String` | :x: | :heavy_check_mark: | The response body. | 35 | 36 | ## Helpers 37 | 38 | The `env` object also provides some helper methods to make it easier to work with the properties. 39 | 40 | | Method | Description | 41 | |-------------------------|--------------------------------------------------------------------------------------------------| 42 | | `#body`/`#current_body` | Returns the request or response body, based on the presence of `#status`. | 43 | | `#success?` | Returns `true` if the response status is in the 2xx range. | 44 | | `#needs_body?` | Returns `true` if there's no body yet, and the method is in the set of `Env::MethodsWithBodies`. | 45 | | `#clear_body` | Clears the body, if it's present. That includes resetting the `Content-Length` header. | 46 | | `#parse_body?` | Returns `true` unless the status indicates otherwise (e.g. 204, 304). | 47 | | `#parallel?` | Returns `true` if a parallel manager is available. | 48 | | `#stream_response?` | Returns `true` if the `on_data` streaming callback has been provided. | 49 | | `#stream_response` | Helper method to implement streaming in adapters. See [Support streaming in your adapter]. | 50 | 51 | [Support streaming in your adapter]: /adapters/custom/streaming.md 52 | -------------------------------------------------------------------------------- /docs/getting-started/errors.md: -------------------------------------------------------------------------------- 1 | # Dealing with Errors 2 | 3 | As an abstraction layer between the user and the underlying HTTP library, 4 | it's important that Faraday provides a consistent interface for dealing with errors. 5 | This is especially important when dealing with multiple adapters, as each adapter may raise different errors. 6 | 7 | Below is a list of errors that Faraday may raise, and that you should be prepared to handle. 8 | 9 | | Error | Description | 10 | |-----------------------------|--------------------------------------------------------------------------------| 11 | | `Faraday::Error` | Base class for all Faraday errors, also used for generic or unexpected errors. | 12 | | `Faraday::ConnectionFailed` | Raised when the connection to the remote server failed. | 13 | | `Faraday::TimeoutError` | Raised when the connection to the remote server timed out. | 14 | | `Faraday::SSLError` | Raised when the connection to the remote server failed due to an SSL error. | 15 | 16 | If you add the `raise_error` middleware, Faraday will also raise additional errors for 4xx and 5xx responses. 17 | You can find the full list of errors in the [raise_error middleware](/middleware/included/raising-errors) page. 18 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Faraday Docs 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 40 | 41 | 42 |
43 | 44 | 45 | 46 | 47 | 106 | 107 | 108 | 109 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # ![Faraday](_media/home-logo.svg) 2 | 3 | Faraday is an HTTP client library abstraction layer that provides a common interface over many 4 | adapters (such as Net::HTTP) and embraces the concept of Rack middleware when processing the request/response cycle. 5 | 6 | ## Why use Faraday? 7 | 8 | Faraday gives you the power of Rack middleware for manipulating HTTP requests and responses, 9 | making it easier to build sophisticated API clients or web service libraries that abstract away 10 | the details of how HTTP requests are made. 11 | 12 | Faraday comes with a lot of features out of the box, such as: 13 | * Support for multiple adapters (Net::HTTP, Typhoeus, Patron, Excon, HTTPClient, and more) 14 | * Persistent connections (keep-alive) 15 | * Parallel requests 16 | * Automatic response parsing (JSON, XML, YAML) 17 | * Customization of the request/response cycle with middleware 18 | * Support for streaming responses 19 | * Support for uploading files 20 | * And much more! 21 | 22 | ## Who uses Faraday? 23 | 24 | Faraday is used by many popular Ruby libraries, such as: 25 | * [Signet](https://github.com/googleapis/signet) 26 | * [Octokit](https://github.com/octokit/octokit.rb) 27 | * [Oauth2](https://bestgems.org/gems/oauth2) 28 | * [Elasticsearch](https://github.com/elastic/elasticsearch-ruby) 29 | -------------------------------------------------------------------------------- /docs/middleware/custom-middleware.md: -------------------------------------------------------------------------------- 1 | # Writing custom middleware 2 | 3 | !> A template for writing your own middleware is available in the [faraday-middleware-template](https://github.com/lostisland/faraday-middleware-template) repository. 4 | 5 | The recommended way to write middleware is to make your middleware subclass `Faraday::Middleware`. 6 | `Faraday::Middleware` simply expects your subclass to implement two methods: `#on_request(env)` and `#on_complete(env)`. 7 | * `#on_request` is called when the request is being built and is given the `env` representing the request. 8 | * `#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. 9 | 10 | For both `env` parameters, please refer to the [Env Object](getting-started/env-object.md) page. 11 | 12 | ```ruby 13 | class MyMiddleware < Faraday::Middleware 14 | def on_request(env) 15 | # do something with the request 16 | # env[:request_headers].merge!(...) 17 | end 18 | 19 | def on_complete(env) 20 | # do something with the response 21 | # env[:response_headers].merge!(...) 22 | end 23 | end 24 | ``` 25 | 26 | ## Having more control 27 | 28 | For the majority of middleware, it's not necessary to override the `#call` method. You can instead use `#on_request` and `#on_complete`. 29 | 30 | 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). 31 | 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! 32 | 33 | ```ruby 34 | def call(request_env) 35 | # do something with the request 36 | # request_env[:request_headers].merge!(...) 37 | 38 | @app.call(request_env).on_complete do |response_env| 39 | # do something with the response 40 | # response_env[:response_headers].merge!(...) 41 | end 42 | end 43 | ``` 44 | 45 | It's important to do all processing of the response only in the `#on_complete` 46 | block. This enables middleware to work in parallel mode where requests are 47 | asynchronous. 48 | 49 | The `request_env` and `response_env` are both [Env Objects](getting-started/env-object.md) but note the amount of 50 | information available in each one will differ based on the request/response lifecycle. 51 | 52 | ## Accepting configuration options 53 | 54 | `Faraday::Middleware` also allows your middleware to accept configuration options. 55 | These are passed in when the middleware is added to the stack, and can be accessed via the `options` getter. 56 | 57 | ```ruby 58 | class MyMiddleware < Faraday::Middleware 59 | def on_request(_env) 60 | # access the foo option 61 | puts options[:foo] 62 | end 63 | end 64 | 65 | conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| 66 | faraday.use MyMiddleware, foo: 'bar' 67 | end 68 | ``` 69 | 70 | ## Registering your middleware 71 | 72 | Users can use your middleware using the class directly, but you can also register it with Faraday so that 73 | it can be used with the `use`, `request` or `response` methods as well. 74 | 75 | ```ruby 76 | # Register for `use` 77 | Faraday::Middleware.register_middleware(my_middleware: MyMiddleware) 78 | 79 | # Register for `request` 80 | Faraday::Request.register_middleware(my_middleware: MyMiddleware) 81 | 82 | # Register for `response` 83 | Faraday::Response.register_middleware(my_middleware: MyMiddleware) 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/middleware/included/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | The `Faraday::Request::Authorization` middleware allows you to automatically add an `Authorization` header 4 | to your requests. It also features a handy helper to manage Basic authentication. 5 | **Please note the way you use this middleware in Faraday 1.x is different**, 6 | examples are available at the bottom of this page. 7 | 8 | ```ruby 9 | Faraday.new(...) do |conn| 10 | conn.request :authorization, 'Bearer', 'authentication-token' 11 | end 12 | ``` 13 | 14 | ### With a proc 15 | 16 | You can also provide a proc, which will be evaluated on each request: 17 | 18 | ```ruby 19 | Faraday.new(...) do |conn| 20 | conn.request :authorization, 'Bearer', -> { MyAuthStorage.get_auth_token } 21 | end 22 | ``` 23 | 24 | If the proc takes an argument, it will receive the forwarded `env` (see [The Env Object](getting-started/env-object.md)): 25 | 26 | ```ruby 27 | Faraday.new(...) do |conn| 28 | conn.request :authorization, 'Bearer', ->(env) { MyAuthStorage.get_auth_token(env) } 29 | end 30 | ``` 31 | 32 | ### Basic Authentication 33 | 34 | The middleware will automatically Base64 encode your Basic username and password: 35 | 36 | ```ruby 37 | Faraday.new(...) do |conn| 38 | conn.request :authorization, :basic, 'username', 'password' 39 | end 40 | ``` 41 | 42 | ### Faraday 1.x usage 43 | 44 | In Faraday 1.x, the way you use this middleware is slightly different: 45 | 46 | ```ruby 47 | # Basic Auth request 48 | # Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= 49 | Faraday.new(...) do |conn| 50 | conn.request :basic_auth, 'username', 'password' 51 | end 52 | 53 | # Token Auth request 54 | # `options` are automatically converted into `key=value` format 55 | # Authorization: Token authentication-token 56 | Faraday.new(...) do |conn| 57 | conn.request :token_auth, 'authentication-token', **options 58 | end 59 | 60 | # Generic Auth Request 61 | # Authorization: Bearer authentication-token 62 | Faraday.new(...) do |conn| 63 | conn.request :authorization, 'Bearer', 'authentication-token' 64 | end 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/middleware/included/index.md: -------------------------------------------------------------------------------- 1 | # Included middleware 2 | 3 | Faraday ships with some useful middleware that you can use to customize your request/response lifecycle. 4 | Middleware are separated into two macro-categories: **Request Middleware** and **Response Middleware**. 5 | The former usually deal with the request, encoding the parameters or setting headers. 6 | The latter instead activate after the request is completed and a response has been received, like 7 | parsing the response body, logging useful info or checking the response status. 8 | 9 | ### Request Middleware 10 | 11 | **Request middleware** can modify Request details before the Adapter runs. Most 12 | middleware set Header values or transform the request body based on the 13 | content type. 14 | 15 | * [`Authorization`][authentication] allows you to automatically add an Authorization header to your requests. 16 | * [`UrlEncoded`][url_encoded] converts a `Faraday::Request#body` hash of key/value pairs into a url-encoded request body. 17 | * [`Json Request`][json-request] converts a `Faraday::Request#body` hash of key/value pairs into a JSON request body. 18 | * [`Instrumentation`][instrumentation] allows to instrument requests using different tools. 19 | 20 | 21 | ### Response Middleware 22 | 23 | **Response middleware** receives the response from the adapter and can modify its details 24 | before returning it. 25 | 26 | * [`Json Response`][json-response] parses response body into a hash of key/value pairs. 27 | * [`Logger`][logger] logs both the request and the response body and headers. 28 | * [`RaiseError`][raise_error] checks the response HTTP code and raises an exception if it is a 4xx or 5xx code. 29 | 30 | 31 | [authentication]: middleware/included/authentication.md 32 | [url_encoded]: middleware/included/url-encoding 33 | [json-request]: middleware/included/json#json-requests 34 | [instrumentation]: middleware/included/instrumentation 35 | [json-response]: middleware/included/json#json-responses 36 | [logger]: middleware/included/logging 37 | [raise_error]: middleware/included/raising-errors 38 | -------------------------------------------------------------------------------- /docs/middleware/included/instrumentation.md: -------------------------------------------------------------------------------- 1 | # Instrumentation 2 | 3 | The `Instrumentation` middleware allows to instrument requests using different tools. 4 | Options for this middleware include the instrumentation `name` and the `instrumenter` you want to use. 5 | They default to `request.faraday` and `ActiveSupport::Notifications` respectively, but you can provide your own: 6 | 7 | ```ruby 8 | conn = Faraday.new(...) do |f| 9 | f.request :instrumentation, name: 'custom_name', instrumenter: MyInstrumenter 10 | ... 11 | end 12 | ``` 13 | 14 | ### Example Usage 15 | 16 | The `Instrumentation` middleware will use `ActiveSupport::Notifications` by default as instrumenter, 17 | allowing you to subscribe to the default event name and instrument requests: 18 | 19 | ```ruby 20 | conn = Faraday.new('http://example.com') do |f| 21 | f.request :instrumentation 22 | ... 23 | end 24 | 25 | ActiveSupport::Notifications.subscribe('request.faraday') do |name, starts, ends, _, env| 26 | url = env[:url] 27 | http_method = env[:method].to_s.upcase 28 | duration = ends - starts 29 | $stdout.puts '[%s] %s %s (%.3f s)' % [url.host, http_method, url.request_uri, duration] 30 | end 31 | 32 | conn.get('/search', { a: 1, b: 2 }) 33 | #=> [example.com] GET /search?a=1&b=2 (0.529 s) 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/middleware/included/json.md: -------------------------------------------------------------------------------- 1 | # JSON Encoding/Decoding 2 | 3 | ## JSON Requests 4 | 5 | The `JSON` request middleware converts a `Faraday::Request#body` hash of key/value pairs into a JSON request body. 6 | The middleware also automatically sets the `Content-Type` header to `application/json`, 7 | processes only requests with matching Content-Type or those without a type and 8 | doesn't try to encode bodies that already are in string form. 9 | 10 | ### Example Usage 11 | 12 | ```ruby 13 | conn = Faraday.new(...) do |f| 14 | f.request :json 15 | ... 16 | end 17 | 18 | conn.post('/', { a: 1, b: 2 }) 19 | # POST with 20 | # Content-Type: application/json 21 | # Body: {"a":1,"b":2} 22 | ``` 23 | 24 | ### Using custom JSON encoders 25 | 26 | By default, middleware utilizes Ruby's `json` to generate JSON strings. 27 | 28 | Other encoders can be used by specifying `encoder` option for the middleware: 29 | * a module/class which implements `dump` 30 | * a module/class-method pair to be used 31 | 32 | ```ruby 33 | require 'oj' 34 | 35 | Faraday.new(...) do |f| 36 | f.request :json, encoder: Oj 37 | end 38 | 39 | Faraday.new(...) do |f| 40 | f.request :json, encoder: [Oj, :dump] 41 | end 42 | ``` 43 | 44 | ## JSON Responses 45 | 46 | The `JSON` response middleware parses response body into a hash of key/value pairs. 47 | The behaviour can be customized with the following options: 48 | * **parser_options:** options that will be sent to the JSON.parse method. Defaults to {}. 49 | * **content_type:** Single value or Array of response content-types that should be processed. Can be either strings or Regex. Defaults to `/\bjson$/`. 50 | * **preserve_raw:** If set to true, the original un-parsed response will be stored in the `response.env[:raw_body]` property. Defaults to `false`. 51 | 52 | ### Example Usage 53 | 54 | ```ruby 55 | conn = Faraday.new('http://httpbingo.org') do |f| 56 | f.response :json, **options 57 | end 58 | 59 | conn.get('json').body 60 | # => {"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"}} 61 | ``` 62 | 63 | ### Using custom JSON decoders 64 | 65 | By default, middleware utilizes Ruby's `json` to parse JSON strings. 66 | 67 | Other decoders can be used by specifying `decoder` parser option for the middleware: 68 | * a module/class which implements `load` 69 | * a module/class-method pair to be used 70 | 71 | ```ruby 72 | require 'oj' 73 | 74 | Faraday.new(...) do |f| 75 | f.response :json, parser_options: { decoder: Oj } 76 | end 77 | 78 | Faraday.new(...) do |f| 79 | f.response :json, parser_options: { decoder: [Oj, :load] } 80 | end 81 | ``` -------------------------------------------------------------------------------- /docs/middleware/included/logging.md: -------------------------------------------------------------------------------- 1 | # Logging 2 | 3 | The `Logger` middleware logs both the request and the response body and headers. 4 | It is highly customizable and allows to mask confidential information if necessary. 5 | 6 | ### Basic Usage 7 | 8 | ```ruby 9 | conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| 10 | faraday.response :logger # log requests and responses to $stdout 11 | end 12 | 13 | conn.get 14 | # => INFO -- request: GET http://httpbingo.org/ 15 | # => DEBUG -- request: User-Agent: "Faraday v1.0.0" 16 | # => INFO -- response: Status 301 17 | # => DEBUG -- response: date: "Sun, 19 May 2019 16:05:40 GMT" 18 | ``` 19 | 20 | ### Customize the logger 21 | 22 | By default, the `Logger` middleware uses the Ruby `Logger.new($stdout)`. 23 | You can customize it to use any logger you want by providing it when you add the middleware to the stack: 24 | 25 | ```ruby 26 | conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| 27 | faraday.response :logger, MyLogger.new($stdout) 28 | end 29 | ``` 30 | 31 | ### Include and exclude headers/bodies 32 | 33 | By default, the `logger` middleware logs only headers for security reasons, however, you can configure it 34 | to log bodies and errors as well, or disable headers logging if you need to. 35 | To do so, simply provide a configuration hash when you add the middleware to the stack: 36 | 37 | ```ruby 38 | conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| 39 | faraday.response :logger, nil, { headers: true, bodies: true, errors: true } 40 | end 41 | ``` 42 | 43 | You can also configure the `logger` middleware with a little more complex settings 44 | like "do not log the request bodies, but log the response bodies". 45 | 46 | ```ruby 47 | conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| 48 | faraday.response :logger, nil, { bodies: { request: false, response: true } } 49 | end 50 | ``` 51 | 52 | Please note this only works with the default formatter. 53 | 54 | ### Filter sensitive information 55 | 56 | You can filter sensitive information from Faraday logs using a regex matcher: 57 | 58 | ```ruby 59 | conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| 60 | faraday.response :logger do | logger | 61 | logger.filter(/(api_key=)([^&]+)/, '\1[REMOVED]') 62 | end 63 | end 64 | 65 | conn.get('/', api_key: 'secret') 66 | # => INFO -- request: GET http://httpbingo.org/?api_key=[REMOVED] 67 | # => DEBUG -- request: User-Agent: "Faraday v1.0.0" 68 | # => INFO -- response: Status 301 69 | # => DEBUG -- response: date: "Sun, 19 May 2019 16:12:36 GMT" 70 | ``` 71 | 72 | ### Change log level 73 | 74 | By default, the `logger` middleware logs on the `info` log level. It is possible to configure 75 | the severity by providing the `log_level` option: 76 | 77 | ```ruby 78 | conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| 79 | faraday.response :logger, nil, { bodies: true, log_level: :debug } 80 | end 81 | ``` 82 | 83 | ### Customize the formatter 84 | 85 | You can also provide a custom formatter to control how requests, responses and errors are logged. 86 | Any custom formatter MUST implement the `request` and `response` method, with one argument which 87 | will be passed being the Faraday environment. 88 | Any custom formatter CAN implement the `exception` method, 89 | with one argument which will be passed being the exception (StandardError). 90 | If you make your formatter inheriting from `Faraday::Logging::Formatter`, 91 | then the methods `debug`, `info`, `warn`, `error` and `fatal` are automatically delegated to the logger. 92 | 93 | ```ruby 94 | class MyFormatter < Faraday::Logging::Formatter 95 | def request(env) 96 | # Build a custom message using `env` 97 | info('Request') { 'Sending Request' } 98 | end 99 | 100 | def response(env) 101 | # Build a custom message using `env` 102 | info('Response') { 'Response Received' } 103 | end 104 | 105 | def exception(exc) 106 | # Build a custom message using `exc` 107 | info('Error') { 'Error Raised' } 108 | end 109 | end 110 | 111 | conn = Faraday.new(url: 'http://httpbingo.org/api_key=s3cr3t') do |faraday| 112 | faraday.response :logger, nil, formatter: MyFormatter 113 | end 114 | ``` 115 | -------------------------------------------------------------------------------- /docs/middleware/included/raising-errors.md: -------------------------------------------------------------------------------- 1 | # Raising Errors 2 | 3 | The `RaiseError` middleware raises a `Faraday::Error` exception if an HTTP 4 | response returns with a 4xx or 5xx status code. 5 | This greatly increases the ease of use of Faraday, as you don't have to check 6 | the response status code manually. 7 | These errors add to the list of default errors [raised by Faraday](getting-started/errors.md). 8 | 9 | All exceptions are initialized with a hash containing the response `status`, `headers`, and `body`. 10 | 11 | ```ruby 12 | conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| 13 | faraday.response :raise_error # raise Faraday::Error on status code 4xx or 5xx 14 | end 15 | 16 | begin 17 | conn.get('/wrong-url') # => Assume this raises a 404 response 18 | rescue Faraday::ResourceNotFound => e 19 | e.response_status #=> 404 20 | e.response_headers #=> { ... } 21 | e.response_body #=> "..." 22 | end 23 | ``` 24 | 25 | Specific exceptions are raised based on the HTTP Status code of the response. 26 | 27 | ## 4xx Errors 28 | 29 | An HTTP status in the 400-499 range typically represents an error 30 | by the client. They raise error classes inheriting from `Faraday::ClientError`. 31 | 32 | | Status Code | Exception Class | 33 | |---------------------------------------------------------------------|-------------------------------------| 34 | | [400](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400) | `Faraday::BadRequestError` | 35 | | [401](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401) | `Faraday::UnauthorizedError` | 36 | | [403](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403) | `Faraday::ForbiddenError` | 37 | | [404](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404) | `Faraday::ResourceNotFound` | 38 | | [407](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/407) | `Faraday::ProxyAuthError` | 39 | | [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) | `Faraday::RequestTimeoutError` | 40 | | [409](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409) | `Faraday::ConflictError` | 41 | | [422](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422) | `Faraday::UnprocessableEntityError` | 42 | | [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) | `Faraday::TooManyRequestsError` | 43 | | 4xx (any other) | `Faraday::ClientError` | 44 | 45 | ## 5xx Errors 46 | 47 | An HTTP status in the 500-599 range represents a server error, and raises a 48 | `Faraday::ServerError` exception. 49 | 50 | It's important to note that this exception is only returned if we receive a response and the 51 | HTTP status in such response is in the 500-599 range. 52 | Other kind of errors normally attributed to errors in the 5xx range (such as timeouts, failure to connect, etc...) 53 | are raised as specific exceptions inheriting from `Faraday::Error`. 54 | See [Faraday Errors](getting-started/errors.md) for more information on these. 55 | 56 | ### Missing HTTP status 57 | 58 | The HTTP response status may be nil due to a malformed HTTP response from the 59 | server, or a bug in the underlying HTTP library. This is considered a server error 60 | and raised as `Faraday::NilStatusError`, which inherits from `Faraday::ServerError`. 61 | 62 | ## Middleware Options 63 | 64 | The behavior of this middleware can be customized with the following options: 65 | 66 | | Option | Default | Description | 67 | |----------------------|---------|-------------| 68 | | **include_request** | true | When true, exceptions are initialized with request information including `method`, `url`, `url_path`, `params`, `headers`, and `body`. | 69 | | **allowed_statuses** | [] | An array of status codes that should not raise an error. | 70 | 71 | ### Example Usage 72 | 73 | ```ruby 74 | conn = Faraday.new(url: 'http://httpbingo.org') do |faraday| 75 | faraday.response :raise_error, include_request: true, allowed_statuses: [404] 76 | end 77 | 78 | begin 79 | conn.get('/wrong-url') # => Assume this raises a 404 response 80 | conn.get('/protected-url') # => Assume this raises a 401 response 81 | rescue Faraday::UnauthorizedError => e 82 | e.response[:status] # => 401 83 | e.response[:headers] # => { ... } 84 | e.response[:body] # => "..." 85 | e.response[:request][:url_path] # => "/protected-url" 86 | end 87 | ``` 88 | 89 | In this example, a `Faraday::UnauthorizedError` exception is raised for the `/protected-url` request, while the 90 | `/wrong-url` request does not raise an error because the status code `404` is in the `allowed_statuses` array. 91 | -------------------------------------------------------------------------------- /docs/middleware/included/url-encoding.md: -------------------------------------------------------------------------------- 1 | # URL Encoding 2 | 3 | The `UrlEncoded` middleware converts a `Faraday::Request#body` hash of key/value pairs into a url-encoded request body. 4 | The middleware also automatically sets the `Content-Type` header to `application/x-www-form-urlencoded`. 5 | The way parameters are serialized can be customized in the [Request Options](customization/request-options.md). 6 | 7 | 8 | ### Example Usage 9 | 10 | ```ruby 11 | conn = Faraday.new(...) do |f| 12 | f.request :url_encoded 13 | ... 14 | end 15 | 16 | conn.post('/', { a: 1, b: 2 }) 17 | # POST with 18 | # Content-Type: application/x-www-form-urlencoded 19 | # Body: a=1&b=2 20 | ``` 21 | 22 | Complex structures can also be passed 23 | 24 | ```ruby 25 | conn.post('/', { a: [1, 3], b: { c: 2, d: 4} }) 26 | # POST with 27 | # Content-Type: application/x-www-form-urlencoded 28 | # Body: a%5B%5D=1&a%5B%5D=3&b%5Bc%5D=2&b%5Bd%5D=4 29 | ``` 30 | 31 | [customize]: ../usage/customize#changing-how-parameters-are-serialized 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 = '>= 3.0' 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.5' 25 | spec.add_dependency 'json' 26 | spec.add_dependency 'logger' 27 | 28 | # Includes `examples` and `spec` to allow external adapter gems to run Faraday unit and integration tests 29 | spec.files = Dir['CHANGELOG.md', '{examples,lib,spec}/**/*', 'LICENSE.md', 'Rakefile', 'README.md'] 30 | spec.require_paths = %w[lib spec/external_adapters] 31 | spec.metadata = { 32 | 'homepage_uri' => 'https://lostisland.github.io/faraday', 33 | 'changelog_uri' => 34 | "https://github.com/lostisland/faraday/releases/tag/v#{spec.version}", 35 | 'source_code_uri' => 'https://github.com/lostisland/faraday', 36 | 'bug_tracker_uri' => 'https://github.com/lostisland/faraday/issues', 37 | 'rubygems_mfa_required' => 'true' 38 | } 39 | end 40 | -------------------------------------------------------------------------------- /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 = lambda(&: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/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/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 | -------------------------------------------------------------------------------- /lib/faraday/logging/formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pp' # This require is necessary for Hash#pretty_inspect to work, do not remove it, people rely on it. 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 | @options = DEFAULT_OPTIONS.merge(options) 17 | unless %i[debug info warn error fatal].include?(@options[:log_level]) 18 | @options[:log_level] = :info 19 | end 20 | @filter = [] 21 | end 22 | 23 | def_delegators :@logger, :debug, :info, :warn, :error, :fatal 24 | 25 | def request(env) 26 | public_send(log_level) do 27 | "request: #{env.method.upcase} #{apply_filters(env.url.to_s)}" 28 | end 29 | 30 | log_headers('request', env.request_headers) if log_headers?(:request) 31 | log_body('request', env[:body]) if env[:body] && log_body?(:request) 32 | end 33 | 34 | def response(env) 35 | public_send(log_level) { "response: Status #{env.status}" } 36 | 37 | log_headers('response', env.response_headers) if log_headers?(:response) 38 | log_body('response', env[:body]) if env[:body] && log_body?(:response) 39 | end 40 | 41 | def exception(exc) 42 | return unless log_errors? 43 | 44 | public_send(log_level) { "error: #{exc.full_message}" } 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 | return if headers.nil? 60 | 61 | headers.map { |k, v| "#{k}: #{v.inspect}" }.join("\n") 62 | end 63 | 64 | def dump_body(body) 65 | if body.respond_to?(:to_str) 66 | body.to_str 67 | else 68 | pretty_inspect(body) 69 | end 70 | end 71 | 72 | def pretty_inspect(body) 73 | body.pretty_inspect 74 | end 75 | 76 | def log_headers?(type) 77 | case @options[:headers] 78 | when Hash 79 | @options[:headers][type] 80 | else 81 | @options[:headers] 82 | end 83 | end 84 | 85 | def log_body?(type) 86 | case @options[:bodies] 87 | when Hash 88 | @options[:bodies][type] 89 | else 90 | @options[:bodies] 91 | end 92 | end 93 | 94 | def log_errors? 95 | @options[:errors] 96 | end 97 | 98 | def apply_filters(output) 99 | @filter.each do |pattern, replacement| 100 | output = output.to_s.gsub(pattern, replacement) 101 | end 102 | output 103 | end 104 | 105 | def log_level 106 | @options[:log_level] 107 | end 108 | 109 | def log_headers(type, headers) 110 | public_send(log_level) { "#{type}: #{apply_filters(dump_headers(headers))}" } 111 | end 112 | 113 | def log_body(type, body) 114 | public_send(log_level) { "#{type}: #{apply_filters(dump_body(body))}" } 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/faraday/middleware.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'monitor' 4 | 5 | module Faraday 6 | # Middleware is the basic base class of any Faraday middleware. 7 | class Middleware 8 | extend MiddlewareRegistry 9 | 10 | attr_reader :app, :options 11 | 12 | DEFAULT_OPTIONS = {}.freeze 13 | LOCK = Mutex.new 14 | 15 | def initialize(app = nil, options = {}) 16 | @app = app 17 | @options = self.class.default_options.merge(options) 18 | end 19 | 20 | class << self 21 | # Faraday::Middleware::default_options= allows user to set default options at the Faraday::Middleware 22 | # class level. 23 | # 24 | # @example Set the Faraday::Response::RaiseError option, `include_request` to `false` 25 | # my_app/config/initializers/my_faraday_middleware.rb 26 | # 27 | # Faraday::Response::RaiseError.default_options = { include_request: false } 28 | # 29 | def default_options=(options = {}) 30 | validate_default_options(options) 31 | LOCK.synchronize do 32 | @default_options = default_options.merge(options) 33 | end 34 | end 35 | 36 | # default_options attr_reader that initializes class instance variable 37 | # with the values of any Faraday::Middleware defaults, and merges with 38 | # subclass defaults 39 | def default_options 40 | @default_options ||= DEFAULT_OPTIONS.merge(self::DEFAULT_OPTIONS) 41 | end 42 | 43 | private 44 | 45 | def validate_default_options(options) 46 | invalid_keys = options.keys.reject { |opt| self::DEFAULT_OPTIONS.key?(opt) } 47 | return unless invalid_keys.any? 48 | 49 | raise(Faraday::InitializationError, 50 | "Invalid options provided. Keys not found in #{self}::DEFAULT_OPTIONS: #{invalid_keys.join(', ')}") 51 | end 52 | end 53 | 54 | def call(env) 55 | on_request(env) if respond_to?(:on_request) 56 | app.call(env).on_complete do |environment| 57 | on_complete(environment) if respond_to?(:on_complete) 58 | end 59 | rescue StandardError => e 60 | on_error(e) if respond_to?(:on_error) 61 | raise 62 | end 63 | 64 | def close 65 | if app.respond_to?(:close) 66 | app.close 67 | else 68 | warn "#{app} does not implement \#close!" 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 '' 16 | value = nil 17 | when String 18 | # URIs without a scheme should default to http (like 'example:123'). 19 | # This fixes #1282 and prevents a silent failure in some adapters. 20 | value = "http://#{value}" unless value.include?('://') 21 | value = { uri: Utils.URI(value) } 22 | when URI 23 | value = { uri: value } 24 | when Hash, Options 25 | if value[:uri] 26 | value = value.dup.tap do |duped| 27 | duped[:uri] = Utils.URI(duped[:uri]) 28 | end 29 | end 30 | end 31 | 32 | super(value) 33 | end 34 | 35 | memoized(:user) { uri&.user && Utils.unescape(uri.user) } 36 | memoized(:password) { uri&.password && Utils.unescape(uri.password) } 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /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/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 hostname 15 | # # @return [String] Server hostname used for SNI (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLSocket.html#method-i-hostname-3D) 16 | # # 17 | # # @!attribute ca_file 18 | # # @return [String] CA file 19 | # # 20 | # # @!attribute ca_path 21 | # # @return [String] CA path 22 | # # 23 | # # @!attribute verify_mode 24 | # # @return [Integer] Any `OpenSSL::SSL::` constant (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL.html) 25 | # # 26 | # # @!attribute cert_store 27 | # # @return [OpenSSL::X509::Store] certificate store 28 | # # 29 | # # @!attribute client_cert 30 | # # @return [String, OpenSSL::X509::Certificate] client certificate 31 | # # 32 | # # @!attribute client_key 33 | # # @return [String, OpenSSL::PKey::RSA, OpenSSL::PKey::DSA] client key 34 | # # 35 | # # @!attribute certificate 36 | # # @return [OpenSSL::X509::Certificate] certificate (Excon only) 37 | # # 38 | # # @!attribute private_key 39 | # # @return [OpenSSL::PKey::RSA, OpenSSL::PKey::DSA] private key (Excon only) 40 | # # 41 | # # @!attribute verify_depth 42 | # # @return [Integer] maximum depth for the certificate chain verification 43 | # # 44 | # # @!attribute version 45 | # # @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) 46 | # # 47 | # # @!attribute min_version 48 | # # @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) 49 | # # 50 | # # @!attribute max_version 51 | # # @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) 52 | # # 53 | # # @!attribute ciphers 54 | # # @return [String] cipher list in OpenSSL format (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html#method-i-ciphers-3D) 55 | # class SSLOptions < Options; end 56 | SSLOptions = Options.new(:verify, :verify_hostname, :hostname, 57 | :ca_file, :ca_path, :verify_mode, 58 | :cert_store, :client_cert, :client_key, 59 | :certificate, :private_key, :verify_depth, 60 | :version, :min_version, :max_version, :ciphers) do 61 | # @return [Boolean] true if should verify 62 | def verify? 63 | verify != false 64 | end 65 | 66 | # @return [Boolean] true if should not verify 67 | def disable? 68 | !verify? 69 | end 70 | 71 | # @return [Boolean] true if should verify_hostname 72 | def verify_hostname? 73 | verify_hostname != false 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /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/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 | remove_method :params= 46 | # Replace params, preserving the existing hash type. 47 | # 48 | # @param hash [Hash] new params 49 | def params=(hash) 50 | if params 51 | params.replace hash 52 | else 53 | member_set(:params, hash) 54 | end 55 | end 56 | 57 | remove_method :headers= 58 | # Replace request headers, preserving the existing hash type. 59 | # 60 | # @param hash [Hash] new headers 61 | def headers=(hash) 62 | if headers 63 | headers.replace hash 64 | else 65 | member_set(:headers, hash) 66 | end 67 | end 68 | 69 | # Update path and params. 70 | # 71 | # @param path [URI, String] 72 | # @param params [Hash, nil] 73 | # @return [void] 74 | def url(path, params = nil) 75 | if path.respond_to? :query 76 | if (query = path.query) 77 | path = path.dup 78 | path.query = nil 79 | end 80 | else 81 | anchor_index = path.index('#') 82 | path = path.slice(0, anchor_index) unless anchor_index.nil? 83 | path, query = path.split('?', 2) 84 | end 85 | self.path = path 86 | self.params.merge_query query, options.params_encoder 87 | self.params.update(params) if params 88 | end 89 | 90 | # @param key [Object] key to look up in headers 91 | # @return [Object] value of the given header name 92 | def [](key) 93 | headers[key] 94 | end 95 | 96 | # @param key [Object] key of header to write 97 | # @param value [Object] value of header 98 | def []=(key, value) 99 | headers[key] = value 100 | end 101 | 102 | # Marshal serialization support. 103 | # 104 | # @return [Hash] the hash ready to be serialized in Marshal. 105 | def marshal_dump 106 | { 107 | http_method: http_method, 108 | body: body, 109 | headers: headers, 110 | path: path, 111 | params: params, 112 | options: options 113 | } 114 | end 115 | 116 | # Marshal serialization support. 117 | # Restores the instance variables according to the +serialised+. 118 | # @param serialised [Hash] the serialised object. 119 | def marshal_load(serialised) 120 | self.http_method = serialised[:http_method] 121 | self.body = serialised[:body] 122 | self.headers = serialised[:headers] 123 | self.path = serialised[:path] 124 | self.params = serialised[:params] 125 | self.options = serialised[:options] 126 | end 127 | 128 | # @return [Env] the Env for this Request 129 | def to_env(connection) 130 | Env.new(http_method, body, connection.build_exclusive_url(path, params), 131 | options, headers, connection.ssl, connection.parallel_manager) 132 | end 133 | end 134 | end 135 | 136 | require 'faraday/request/authorization' 137 | require 'faraday/request/instrumentation' 138 | require 'faraday/request/json' 139 | require 'faraday/request/url_encoded' 140 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | remove_method :name 10 | # @return [String] 11 | def name 12 | self[:name] ||= 'request.faraday' 13 | end 14 | 15 | remove_method :instrumenter 16 | # @return [Class] 17 | def instrumenter 18 | self[:instrumenter] ||= ActiveSupport::Notifications 19 | end 20 | end 21 | 22 | # Instruments requests using Active Support. 23 | # 24 | # Measures time spent only for synchronous requests. 25 | # 26 | # @example Using ActiveSupport::Notifications to measure time spent 27 | # for Faraday requests. 28 | # ActiveSupport::Notifications 29 | # .subscribe('request.faraday') do |name, starts, ends, _, env| 30 | # url = env[:url] 31 | # http_method = env[:method].to_s.upcase 32 | # duration = ends - starts 33 | # $stderr.puts '[%s] %s %s (%.3f s)' % 34 | # [url.host, http_method, url.request_uri, duration] 35 | # end 36 | # @param app [#call] 37 | # @param options [nil, Hash] Options hash 38 | # @option options [String] :name ('request.faraday') 39 | # Name of the instrumenter 40 | # @option options [Class] :instrumenter (ActiveSupport::Notifications) 41 | # Active Support instrumenter class. 42 | def initialize(app, options = nil) 43 | super(app) 44 | @name, @instrumenter = Options.from(options) 45 | .values_at(:name, :instrumenter) 46 | end 47 | 48 | # @param env [Faraday::Env] 49 | def call(env) 50 | @instrumenter.instrument(@name, env) do 51 | @app.call(env) 52 | end 53 | end 54 | end 55 | end 56 | end 57 | 58 | Faraday::Request.register_middleware(instrumentation: Faraday::Request::Instrumentation) 59 | -------------------------------------------------------------------------------- /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$} 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 | if options[:encoder].is_a?(Array) && options[:encoder].size >= 2 28 | options[:encoder][0].public_send(options[:encoder][1], data) 29 | elsif options[:encoder].respond_to?(:dump) 30 | options[:encoder].dump(data) 31 | else 32 | ::JSON.generate(data) 33 | end 34 | end 35 | 36 | def match_content_type(env) 37 | return unless process_request?(env) 38 | 39 | env[:request_headers][CONTENT_TYPE] ||= MIME_TYPE 40 | yield env[:body] unless env[:body].respond_to?(:to_str) 41 | end 42 | 43 | def process_request?(env) 44 | type = request_type(env) 45 | body?(env) && (type.empty? || type.match?(MIME_TYPE_REGEX)) 46 | end 47 | 48 | def body?(env) 49 | body = env[:body] 50 | case body 51 | when true, false 52 | true 53 | when nil 54 | # NOTE: nil can be converted to `"null"`, but this middleware doesn't process `nil` for the compatibility. 55 | false 56 | else 57 | !(body.respond_to?(:to_str) && body.empty?) 58 | end 59 | end 60 | 61 | def request_type(env) 62 | type = env[:request_headers][CONTENT_TYPE].to_s 63 | type = type.split(';', 2).first if type.index(';') 64 | type 65 | end 66 | end 67 | end 68 | end 69 | 70 | Faraday::Request.register_middleware(json: Faraday::Request::Json) 71 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 15 | process_parser_options 16 | end 17 | 18 | def on_complete(env) 19 | process_response(env) if parse_response?(env) 20 | end 21 | 22 | private 23 | 24 | def process_response(env) 25 | env[:raw_body] = env[:body] if @preserve_raw 26 | env[:body] = parse(env[:body]) 27 | rescue StandardError, SyntaxError => e 28 | raise Faraday::ParsingError.new(e, env[:response]) 29 | end 30 | 31 | def parse(body) 32 | return if body.strip.empty? 33 | 34 | decoder, method_name = @decoder_options 35 | 36 | decoder.public_send(method_name, body, @parser_options || {}) 37 | end 38 | 39 | def parse_response?(env) 40 | process_response_type?(env) && 41 | env[:body].respond_to?(:to_str) 42 | end 43 | 44 | def process_response_type?(env) 45 | type = response_type(env) 46 | @content_types.empty? || @content_types.any? do |pattern| 47 | pattern.is_a?(Regexp) ? type.match?(pattern) : type == pattern 48 | end 49 | end 50 | 51 | def response_type(env) 52 | type = env[:response_headers][CONTENT_TYPE].to_s 53 | type = type.split(';', 2).first if type.index(';') 54 | type 55 | end 56 | 57 | def process_parser_options 58 | @decoder_options = @parser_options&.delete(:decoder) 59 | 60 | @decoder_options = 61 | if @decoder_options.is_a?(Array) && @decoder_options.size >= 2 62 | @decoder_options.slice(0, 2) 63 | elsif @decoder_options&.respond_to?(:load) # rubocop:disable Lint/RedundantSafeNavigation 64 | # In some versions of Rails, `nil` responds to `load` - hence the safe navigation check above 65 | [@decoder_options, :load] 66 | else 67 | [::JSON, :parse] 68 | end 69 | end 70 | end 71 | end 72 | end 73 | 74 | Faraday::Response.register_middleware(json: Faraday::Response::Json) 75 | -------------------------------------------------------------------------------- /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 | DEFAULT_OPTIONS = { formatter: Logging::Formatter }.merge(Logging::Formatter::DEFAULT_OPTIONS).freeze 14 | 15 | def initialize(app, logger = nil, options = {}) 16 | super(app, options) 17 | logger ||= ::Logger.new($stdout) 18 | formatter_class = @options.delete(:formatter) 19 | @formatter = formatter_class.new(logger: logger, options: @options) 20 | yield @formatter if block_given? 21 | end 22 | 23 | def call(env) 24 | @formatter.request(env) 25 | super 26 | end 27 | 28 | def on_complete(env) 29 | @formatter.response(env) 30 | end 31 | 32 | def on_error(exc) 33 | @formatter.exception(exc) if @formatter.respond_to?(:exception) 34 | end 35 | end 36 | end 37 | end 38 | 39 | Faraday::Response.register_middleware(logger: Faraday::Response::Logger) 40 | -------------------------------------------------------------------------------- /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) 10 | ServerErrorStatuses = (500...600) 11 | ClientErrorStatusesWithCustomExceptions = { 12 | 400 => Faraday::BadRequestError, 13 | 401 => Faraday::UnauthorizedError, 14 | 403 => Faraday::ForbiddenError, 15 | 404 => Faraday::ResourceNotFound, 16 | 408 => Faraday::RequestTimeoutError, 17 | 409 => Faraday::ConflictError, 18 | 422 => Faraday::UnprocessableEntityError, 19 | 429 => Faraday::TooManyRequestsError 20 | }.freeze 21 | # rubocop:enable Naming/ConstantName 22 | 23 | DEFAULT_OPTIONS = { include_request: true, allowed_statuses: [] }.freeze 24 | 25 | def on_complete(env) 26 | return if Array(options[:allowed_statuses]).include?(env[:status]) 27 | 28 | case env[:status] 29 | when *ClientErrorStatusesWithCustomExceptions.keys 30 | raise ClientErrorStatusesWithCustomExceptions[env[:status]], response_values(env) 31 | when 407 32 | # mimic the behavior that we get with proxy requests with HTTPS 33 | msg = %(407 "Proxy Authentication Required") 34 | raise Faraday::ProxyAuthError.new(msg, response_values(env)) 35 | when ClientErrorStatuses 36 | raise Faraday::ClientError, response_values(env) 37 | when ServerErrorStatuses 38 | raise Faraday::ServerError, response_values(env) 39 | when nil 40 | raise Faraday::NilStatusError, response_values(env) 41 | end 42 | end 43 | 44 | # Returns a hash of response data with the following keys: 45 | # - status 46 | # - headers 47 | # - body 48 | # - request 49 | # 50 | # The `request` key is omitted when the middleware is explicitly 51 | # configured with the option `include_request: false`. 52 | def response_values(env) 53 | response = { 54 | status: env.status, 55 | headers: env.response_headers, 56 | body: env.body 57 | } 58 | 59 | # Include the request data by default. If the middleware was explicitly 60 | # configured to _not_ include request data, then omit it. 61 | return response unless options[:include_request] 62 | 63 | response.merge( 64 | request: { 65 | method: env.method, 66 | url: env.url, 67 | url_path: env.url.path, 68 | params: query_params(env), 69 | headers: env.request_headers, 70 | body: env.request_body 71 | } 72 | ) 73 | end 74 | 75 | def query_params(env) 76 | env.request.params_encoder ||= Faraday::Utils.default_params_encoder 77 | env.params_encoder.decode(env.url.query) 78 | end 79 | end 80 | end 81 | end 82 | 83 | Faraday::Response.register_middleware(raise_error: Faraday::Response::RaiseError) 84 | -------------------------------------------------------------------------------- /lib/faraday/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'uri' 4 | require 'faraday/utils/headers' 5 | require 'faraday/utils/params_hash' 6 | 7 | module Faraday 8 | # Utils contains various static helper methods. 9 | module Utils 10 | module_function 11 | 12 | def build_query(params) 13 | FlatParamsEncoder.encode(params) 14 | end 15 | 16 | def build_nested_query(params) 17 | NestedParamsEncoder.encode(params) 18 | end 19 | 20 | def default_space_encoding 21 | @default_space_encoding ||= '+' 22 | end 23 | 24 | class << self 25 | attr_writer :default_space_encoding 26 | end 27 | 28 | ESCAPE_RE = /[^a-zA-Z0-9 .~_-]/ 29 | 30 | def escape(str) 31 | str.to_s.gsub(ESCAPE_RE) do |match| 32 | "%#{match.unpack('H2' * match.bytesize).join('%').upcase}" 33 | end.gsub(' ', default_space_encoding) 34 | end 35 | 36 | def unescape(str) 37 | CGI.unescape str.to_s 38 | end 39 | 40 | DEFAULT_SEP = /[&;] */n 41 | 42 | # Adapted from Rack 43 | def parse_query(query) 44 | FlatParamsEncoder.decode(query) 45 | end 46 | 47 | def parse_nested_query(query) 48 | NestedParamsEncoder.decode(query) 49 | end 50 | 51 | def default_params_encoder 52 | @default_params_encoder ||= NestedParamsEncoder 53 | end 54 | 55 | def basic_header_from(login, pass) 56 | value = ["#{login}:#{pass}"].pack('m') # Base64 encoding 57 | value.delete!("\n") 58 | "Basic #{value}" 59 | end 60 | 61 | class << self 62 | attr_writer :default_params_encoder 63 | end 64 | 65 | # Normalize URI() behavior across Ruby versions 66 | # 67 | # url - A String or URI. 68 | # 69 | # Returns a parsed URI. 70 | def URI(url) # rubocop:disable Naming/MethodName 71 | if url.respond_to?(:host) 72 | url 73 | elsif url.respond_to?(:to_str) 74 | default_uri_parser.call(url) 75 | else 76 | raise ArgumentError, 'bad argument (expected URI object or URI string)' 77 | end 78 | end 79 | 80 | def default_uri_parser 81 | @default_uri_parser ||= Kernel.method(:URI) 82 | end 83 | 84 | def default_uri_parser=(parser) 85 | @default_uri_parser = if parser.respond_to?(:call) || parser.nil? 86 | parser 87 | else 88 | parser.method(:parse) 89 | end 90 | end 91 | 92 | # Receives a String or URI and returns just 93 | # the path with the query string sorted. 94 | def normalize_path(url) 95 | url = URI(url) 96 | (url.path.start_with?('/') ? url.path : "/#{url.path}") + 97 | (url.query ? "?#{sort_query_params(url.query)}" : '') 98 | end 99 | 100 | # Recursive hash update 101 | def deep_merge!(target, hash) 102 | hash.each do |key, value| 103 | target[key] = if value.is_a?(Hash) && (target[key].is_a?(Hash) || target[key].is_a?(Options)) 104 | deep_merge(target[key], value) 105 | else 106 | value 107 | end 108 | end 109 | target 110 | end 111 | 112 | # Recursive hash merge 113 | def deep_merge(source, hash) 114 | deep_merge!(source.dup, hash) 115 | end 116 | 117 | def sort_query_params(query) 118 | query.split('&').sort.join('&') 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /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, ...) 66 | key = KeyMap[key] 67 | key = @names.fetch(key.downcase, key) 68 | super(key, ...) 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 dig(key, *rest) 81 | key = KeyMap[key] 82 | key = @names.fetch(key.downcase, key) 83 | super(key, *rest) 84 | end 85 | 86 | def include?(key) 87 | @names.include? key.downcase 88 | end 89 | 90 | alias has_key? include? 91 | alias member? include? 92 | alias key? include? 93 | 94 | def merge!(other) 95 | other.each { |k, v| self[k] = v } 96 | self 97 | end 98 | 99 | alias update merge! 100 | 101 | def merge(other) 102 | hash = dup 103 | hash.merge! other 104 | end 105 | 106 | def replace(other) 107 | clear 108 | @names.clear 109 | update other 110 | self 111 | end 112 | 113 | def to_hash 114 | {}.update(self) 115 | end 116 | 117 | def parse(header_string) 118 | return unless header_string && !header_string.empty? 119 | 120 | headers = header_string.split("\r\n") 121 | 122 | # Find the last set of response headers. 123 | start_index = headers.rindex { |x| x.start_with?('HTTP/') } || 0 124 | last_response = headers.slice(start_index, headers.size) 125 | 126 | last_response 127 | .tap { |a| a.shift if a.first.start_with?('HTTP/') } 128 | .map { |h| h.split(/:\s*/, 2) } # split key and value 129 | .reject { |p| p[0].nil? } # ignore blank lines 130 | .each { |key, value| add_parsed(key, value) } 131 | end 132 | 133 | protected 134 | 135 | attr_reader :names 136 | 137 | private 138 | 139 | # Join multiple values with a comma. 140 | def add_parsed(key, value) 141 | if key?(key) 142 | self[key] = self[key].to_s 143 | self[key] << ', ' << value 144 | else 145 | self[key] = value 146 | end 147 | end 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/faraday/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Faraday 4 | VERSION = '2.13.1' 5 | end 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "faraday-docs", 3 | "version": "0.1.0", 4 | "description": "Faraday Docs", 5 | "main": "index.js", 6 | "directories": { 7 | "doc": "docs", 8 | "example": "examples", 9 | "lib": "lib" 10 | }, 11 | "scripts": { 12 | "docs": "docsify serve ./docs" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/lostisland/faraday.git" 17 | }, 18 | "keywords": [ 19 | "docs", 20 | "faraday" 21 | ], 22 | "author": "Mattia Giuffrida", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/lostisland/faraday/issues" 26 | }, 27 | "homepage": "https://github.com/lostisland/faraday#readme", 28 | "dependencies": { 29 | "docsify-cli": "^4.4.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /spec/faraday/error_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Faraday::Error 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 | it { expect(subject.response_headers).to be_nil } 18 | it { expect(subject.response_body).to be_nil } 19 | end 20 | 21 | context 'with response hash' do 22 | let(:exception) { { status: 400 } } 23 | 24 | it { expect(subject.wrapped_exception).to be_nil } 25 | it { expect(subject.response).to eq(exception) } 26 | it { expect(subject.message).to eq('the server responded with status 400') } 27 | if RUBY_VERSION >= '3.4' 28 | it { expect(subject.inspect).to eq('#') } 29 | else 30 | it { expect(subject.inspect).to eq('#400}>') } 31 | end 32 | it { expect(subject.response_status).to eq(400) } 33 | it { expect(subject.response_headers).to be_nil } 34 | it { expect(subject.response_body).to be_nil } 35 | end 36 | 37 | context 'with string' do 38 | let(:exception) { 'custom message' } 39 | 40 | it { expect(subject.wrapped_exception).to be_nil } 41 | it { expect(subject.response).to be_nil } 42 | it { expect(subject.message).to eq('custom message') } 43 | it { expect(subject.inspect).to eq('#>') } 44 | it { expect(subject.response_status).to be_nil } 45 | it { expect(subject.response_headers).to be_nil } 46 | it { expect(subject.response_body).to be_nil } 47 | end 48 | 49 | context 'with anything else #to_s' do 50 | let(:exception) { %w[error1 error2] } 51 | 52 | it { expect(subject.wrapped_exception).to be_nil } 53 | it { expect(subject.response).to be_nil } 54 | it { expect(subject.message).to eq('["error1", "error2"]') } 55 | it { expect(subject.inspect).to eq('#>') } 56 | it { expect(subject.response_status).to be_nil } 57 | it { expect(subject.response_headers).to be_nil } 58 | it { expect(subject.response_body).to be_nil } 59 | end 60 | 61 | context 'with exception string and response hash' do 62 | let(:exception) { 'custom message' } 63 | let(:response) { { status: 400 } } 64 | 65 | it { expect(subject.wrapped_exception).to be_nil } 66 | it { expect(subject.response).to eq(response) } 67 | it { expect(subject.message).to eq('custom message') } 68 | if RUBY_VERSION >= '3.4' 69 | it { expect(subject.inspect).to eq('#') } 70 | else 71 | it { expect(subject.inspect).to eq('#400}>') } 72 | end 73 | it { expect(subject.response_status).to eq(400) } 74 | it { expect(subject.response_headers).to be_nil } 75 | it { expect(subject.response_body).to be_nil } 76 | end 77 | 78 | context 'with exception and response object' do 79 | let(:exception) { RuntimeError.new('test') } 80 | let(:body) { { test: 'test' } } 81 | let(:headers) { { 'Content-Type' => 'application/json' } } 82 | let(:response) { Faraday::Response.new(status: 400, response_headers: headers, response_body: body) } 83 | 84 | it { expect(subject.wrapped_exception).to eq(exception) } 85 | it { expect(subject.response).to eq(response) } 86 | it { expect(subject.message).to eq(exception.message) } 87 | it { expect(subject.backtrace).to eq(exception.backtrace) } 88 | it { expect(subject.response_status).to eq(400) } 89 | it { expect(subject.response_headers).to eq(headers) } 90 | it { expect(subject.response_body).to eq(body) } 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 hash' do 31 | hash = { user: 'user', password: 'pass', uri: 'http://@example.org' } 32 | options = Faraday::ProxyOptions.from(hash) 33 | expect(options.user).to eq('user') 34 | expect(options.password).to eq('pass') 35 | expect(options.uri).to be_a_kind_of(URI) 36 | expect(options.path).to eq('') 37 | expect(options.port).to eq(80) 38 | expect(options.host).to eq('example.org') 39 | expect(options.scheme).to eq('http') 40 | expect(options.inspect).to match('# empty string 66 | options = Faraday::ProxyOptions.from proxy_string 67 | expect(options).to be_a_kind_of(Faraday::ProxyOptions) 68 | expect(options.inspect).to eq('#') 69 | end 70 | end 71 | 72 | it 'allows hash access' do 73 | proxy = Faraday::ProxyOptions.from 'http://a%40b:pw%20d@example.org' 74 | expect(proxy.user).to eq('a@b') 75 | expect(proxy[:user]).to eq('a@b') 76 | expect(proxy.password).to eq('pw d') 77 | expect(proxy[:password]).to eq('pw d') 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | escaped = Rack::Utils.build_nested_query(params) 66 | expected = Rack::Utils.unescape(escaped).split('&') 67 | expect(result).to match_array(expected) 68 | end 69 | 70 | it 'encodes empty string array value' do 71 | expected = 'baz=&foo%5Bbar%5D=' 72 | result = Faraday::NestedParamsEncoder.encode(foo: { bar: '' }, baz: '') 73 | expect(result).to eq(expected) 74 | end 75 | 76 | it 'encodes nil array value' do 77 | expected = 'baz&foo%5Bbar%5D' 78 | result = Faraday::NestedParamsEncoder.encode(foo: { bar: nil }, baz: nil) 79 | expect(result).to eq(expected) 80 | end 81 | 82 | it 'encodes empty array value' do 83 | expected = 'baz%5B%5D&foo%5Bbar%5D%5B%5D' 84 | result = Faraday::NestedParamsEncoder.encode(foo: { bar: [] }, baz: []) 85 | expect(result).to eq(expected) 86 | end 87 | 88 | it 'encodes boolean values' do 89 | params = { a: true, b: false } 90 | expect(subject.encode(params)).to eq('a=true&b=false') 91 | end 92 | 93 | it 'encodes boolean values in array' do 94 | params = { a: [true, false] } 95 | expect(subject.encode(params)).to eq('a%5B%5D=true&a%5B%5D=false') 96 | end 97 | 98 | it 'encodes unsorted when asked' do 99 | params = { b: false, a: true } 100 | expect(subject.encode(params)).to eq('a=true&b=false') 101 | Faraday::NestedParamsEncoder.sort_params = false 102 | expect(subject.encode(params)).to eq('b=false&a=true') 103 | Faraday::NestedParamsEncoder.sort_params = true 104 | end 105 | 106 | it 'encodes arrays indices when asked' do 107 | params = { a: [0, 1, 2] } 108 | expect(subject.encode(params)).to eq('a%5B%5D=0&a%5B%5D=1&a%5B%5D=2') 109 | Faraday::NestedParamsEncoder.array_indices = true 110 | expect(subject.encode(params)).to eq('a%5B0%5D=0&a%5B1%5D=1&a%5B2%5D=2') 111 | Faraday::NestedParamsEncoder.array_indices = false 112 | end 113 | 114 | shared_examples 'a wrong decoding' do 115 | it do 116 | expect { subject.decode(query) }.to raise_error(TypeError) do |e| 117 | expect(e.message).to eq(error_message) 118 | end 119 | end 120 | end 121 | 122 | context 'when expecting hash but getting string' do 123 | let(:query) { 'a=1&a[b]=2' } 124 | let(:error_message) { "expected Hash (got String) for param `a'" } 125 | it_behaves_like 'a wrong decoding' 126 | end 127 | 128 | context 'when expecting hash but getting array' do 129 | let(:query) { 'a[]=1&a[b]=2' } 130 | let(:error_message) { "expected Hash (got Array) for param `a'" } 131 | it_behaves_like 'a wrong decoding' 132 | end 133 | 134 | context 'when expecting nested hash but getting non nested' do 135 | let(:query) { 'a[b]=1&a[b][c]=2' } 136 | let(:error_message) { "expected Hash (got String) for param `b'" } 137 | it_behaves_like 'a wrong decoding' 138 | end 139 | 140 | context 'when expecting array but getting hash' do 141 | let(:query) { 'a[b]=1&a[]=2' } 142 | let(:error_message) { "expected Array (got Hash) for param `a'" } 143 | it_behaves_like 'a wrong decoding' 144 | end 145 | 146 | context 'when expecting array but getting string' do 147 | let(:query) { 'a=1&a[]=2' } 148 | let(:error_message) { "expected Array (got String) for param `a'" } 149 | it_behaves_like 'a wrong decoding' 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 'true body' do 77 | let(:result) { process(true) } 78 | 79 | it 'encodes body' do 80 | expect(result_body).to eq('true') 81 | end 82 | 83 | it 'adds content type' do 84 | expect(result_type).to eq('application/json') 85 | end 86 | end 87 | 88 | context 'false body' do 89 | let(:result) { process(false) } 90 | 91 | it 'encodes body' do 92 | expect(result_body).to eq('false') 93 | end 94 | 95 | it 'adds content type' do 96 | expect(result_type).to eq('application/json') 97 | end 98 | end 99 | 100 | context 'object body with json type' do 101 | let(:result) { process({ a: 1 }, 'application/json; charset=utf-8') } 102 | 103 | it 'encodes 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/json; charset=utf-8') 109 | end 110 | end 111 | 112 | context 'object body with vendor json type' do 113 | let(:result) { process({ a: 1 }, 'application/vnd.myapp.v1+json; charset=utf-8') } 114 | 115 | it 'encodes body' do 116 | expect(result_body).to eq('{"a":1}') 117 | end 118 | 119 | it "doesn't change content type" do 120 | expect(result_type).to eq('application/vnd.myapp.v1+json; charset=utf-8') 121 | end 122 | end 123 | 124 | context 'object body with incompatible type' do 125 | let(:result) { process({ a: 1 }, 'application/xml; charset=utf-8') } 126 | 127 | it "doesn't change body" do 128 | expect(result_body).to eq(a: 1) 129 | end 130 | 131 | it "doesn't change content type" do 132 | expect(result_type).to eq('application/xml; charset=utf-8') 133 | end 134 | end 135 | 136 | context 'with encoder' do 137 | let(:encoder) do 138 | double('Encoder').tap do |e| 139 | allow(e).to receive(:dump) { |s, opts| JSON.generate(s, opts) } 140 | end 141 | end 142 | 143 | let(:result) { process(a: 1) } 144 | 145 | context 'when encoder is passed as object' do 146 | let(:middleware) { described_class.new(->(env) { Faraday::Response.new(env) }, { encoder: encoder }) } 147 | 148 | it 'calls specified JSON encoder\'s dump method' do 149 | expect(encoder).to receive(:dump).with({ a: 1 }) 150 | 151 | result 152 | end 153 | 154 | it 'encodes body' do 155 | expect(result_body).to eq('{"a":1}') 156 | end 157 | 158 | it 'adds content type' do 159 | expect(result_type).to eq('application/json') 160 | end 161 | end 162 | 163 | context 'when encoder is passed as an object-method pair' do 164 | let(:middleware) { described_class.new(->(env) { Faraday::Response.new(env) }, { encoder: [encoder, :dump] }) } 165 | 166 | it 'calls specified JSON encoder' do 167 | expect(encoder).to receive(:dump).with({ a: 1 }) 168 | 169 | result 170 | end 171 | 172 | it 'encodes body' do 173 | expect(result_body).to eq('{"a":1}') 174 | end 175 | 176 | it 'adds content type' do 177 | expect(result_type).to eq('application/json') 178 | end 179 | end 180 | 181 | context 'when encoder is not passed' do 182 | let(:middleware) { described_class.new(->(env) { Faraday::Response.new(env) }) } 183 | 184 | it 'calls JSON.generate' do 185 | expect(JSON).to receive(:generate).with({ a: 1 }) 186 | 187 | result 188 | end 189 | 190 | it 'encodes body' do 191 | expect(result_body).to eq('{"a":1}') 192 | end 193 | 194 | it 'adds content type' do 195 | expect(result_type).to eq('application/json') 196 | end 197 | end 198 | end 199 | end 200 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 '#dig' do 60 | before { subject['Content-Type'] = 'application/json' } 61 | 62 | it { expect(subject&.dig('Content-Type')).to eq('application/json') } 63 | it { expect(subject&.dig('CONTENT-TYPE')).to eq('application/json') } 64 | it { expect(subject&.dig(:content_type)).to eq('application/json') } 65 | it { expect(subject&.dig('invalid')).to be_nil } 66 | end 67 | 68 | describe '#parse' do 69 | context 'when response headers leave http status line out' do 70 | let(:headers) { "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n" } 71 | 72 | before { subject.parse(headers) } 73 | 74 | it { expect(subject.keys).to eq(%w[Content-Type]) } 75 | it { expect(subject['Content-Type']).to eq('text/html') } 76 | it { expect(subject['content-type']).to eq('text/html') } 77 | end 78 | 79 | context 'when response headers values include a colon' do 80 | let(:headers) { "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nLocation: http://httpbingo.org/\r\n\r\n" } 81 | 82 | before { subject.parse(headers) } 83 | 84 | it { expect(subject['location']).to eq('http://httpbingo.org/') } 85 | end 86 | 87 | context 'when response headers include a blank line' do 88 | let(:headers) { "HTTP/1.1 200 OK\r\n\r\nContent-Type: text/html\r\n\r\n" } 89 | 90 | before { subject.parse(headers) } 91 | 92 | it { expect(subject['content-type']).to eq('text/html') } 93 | end 94 | 95 | context 'when response headers include already stored keys' do 96 | let(:headers) { "HTTP/1.1 200 OK\r\nX-Numbers: 123\r\n\r\n" } 97 | 98 | before do 99 | h = subject 100 | h[:x_numbers] = 8 101 | h.parse(headers) 102 | end 103 | 104 | it do 105 | expect(subject[:x_numbers]).to eq('8, 123') 106 | end 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /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 | hostname: nil, 108 | ciphers: nil 109 | } 110 | end 111 | 112 | it 'does not overwrite an Options Struct value' do 113 | deep_merge = Faraday::Utils.deep_merge!(connection_options, url) 114 | 115 | expect(deep_merge.request.to_h).to eq(request) 116 | expect(deep_merge.ssl.to_h).to eq(ssl) 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /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 | expected_message = 22 | if RUBY_VERSION >= '3.4' 23 | "undefined method 'this_method_does_not_exist' for module Faraday" 24 | elsif RUBY_VERSION >= '3.3' 25 | "undefined method `this_method_does_not_exist' for module Faraday" 26 | else 27 | "undefined method `this_method_does_not_exist' for Faraday:Module" 28 | end 29 | 30 | expect { Faraday.this_method_does_not_exist }.to raise_error(NoMethodError, expected_message) 31 | end 32 | 33 | it 'proxied methods can be accessed' do 34 | allow(mock_connection).to receive(:this_should_be_proxied) 35 | 36 | expect(Faraday.method(:this_should_be_proxied)).to be_a(Method) 37 | end 38 | 39 | after do 40 | Faraday.default_connection = nil 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /spec/support/faraday_middleware_subclasses.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FaradayMiddlewareSubclasses 4 | class SubclassNoOptions < Faraday::Middleware 5 | end 6 | 7 | class SubclassOneOption < Faraday::Middleware 8 | DEFAULT_OPTIONS = { some_other_option: false }.freeze 9 | end 10 | 11 | class SubclassTwoOptions < Faraday::Middleware 12 | DEFAULT_OPTIONS = { some_option: true, some_other_option: false }.freeze 13 | end 14 | end 15 | 16 | Faraday::Response.register_middleware(no_options: FaradayMiddlewareSubclasses::SubclassNoOptions) 17 | Faraday::Response.register_middleware(one_option: FaradayMiddlewareSubclasses::SubclassOneOption) 18 | Faraday::Response.register_middleware(two_options: FaradayMiddlewareSubclasses::SubclassTwoOptions) 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------