├── .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 | # [][website]
2 |
3 | [](https://rubygems.org/gems/faraday)
4 | [](https://github.com/lostisland/faraday/actions?query=workflow%3ACI)
5 | [](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 | # 
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 |
--------------------------------------------------------------------------------