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

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
+574
View File
@@ -0,0 +1,574 @@
# Faraday Changelog
## The changelog has moved!
This file is not being updated anymore. Instead, please check the [Releases](https://github.com/lostisland/faraday/releases) page.
## [2.2.0](https://github.com/lostisland/faraday/compare/v2.1.0...v2.2.0) (2022-02-03)
* Reintroduce the possibility to register middleware with symbols, strings or procs in [#1391](https://github.com/lostisland/faraday/pull/1391)
## [2.1.0](https://github.com/lostisland/faraday/compare/v2.0.1...v2.1.0) (2022-01-15)
* Fix test adapter thread safety by @iMacTia in [#1380](https://github.com/lostisland/faraday/pull/1380)
* Add default adapter options by @hirasawayuki in [#1382](https://github.com/lostisland/faraday/pull/1382)
* CI: Add Ruby 3.1 to matrix by @petergoldstein in [#1374](https://github.com/lostisland/faraday/pull/1374)
* docs: fix regex pattern in logger.md examples by @hirasawayuki in [#1378](https://github.com/lostisland/faraday/pull/1378)
## [2.0.1](https://github.com/lostisland/faraday/compare/v2.0.0...v2.0.1) (2022-01-05)
* Re-add `faraday-net_http` as default adapter by @iMacTia in [#1366](https://github.com/lostisland/faraday/pull/1366)
* Updated sample format in UPGRADING.md by @vimutter in [#1361](https://github.com/lostisland/faraday/pull/1361)
* docs: Make UPGRADING examples more copyable by @olleolleolle in [#1363](https://github.com/lostisland/faraday/pull/1363)
## [2.0.0](https://github.com/lostisland/faraday/compare/v1.8.0...v2.0.0) (2022-01-04)
The next major release is here, and it comes almost 2 years after the release of v1.0!
This release changes the way you use Faraday and embraces a new paradigm of Faraday as an ecosystem, rather than a library.
What does that mean? It means that Faraday is less of a bundled tool and more of a framework for the community to build on top of.
As a result, all adapters and some middleware have moved out and are now shipped as standalone gems 🙌!
But this doesn't mean that upgrading from Faraday 1.x to Faraday 2.0 should be hard, in fact we've listed everything you need to do in the [UPGRADING.md](https://github.com/lostisland/faraday/blob/main/UPGRADING.md) doc.
Moreover, we've setup a new [awesome-faraday](https://github.com/lostisland/awesome-faraday) repository that will showcase a curated list of adapters and middleware 😎.
This release was the result of the efforts of the core team and all the contributors, new and old, that have helped achieve this milestone 👏.
## What's Changed
* Autoloading, dependency loading and middleware registry cleanup by @iMacTia in [#1301](https://github.com/lostisland/faraday/pull/1301)
* Move JSON middleware (request and response) from faraday_middleware by @iMacTia in [#1300](https://github.com/lostisland/faraday/pull/1300)
* Remove deprecated `Faraday::Request#method` by @olleolleolle in [#1303](https://github.com/lostisland/faraday/pull/1303)
* Remove deprecated `Faraday::UploadIO` by @iMacTia in [#1307](https://github.com/lostisland/faraday/pull/1307)
* [1.x] Deprecate Authorization helpers in `Faraday::Connection` by @iMacTia in [#1306](https://github.com/lostisland/faraday/pull/1306)
* Drop deprecated auth helpers from Connection and refactor auth middleware by @iMacTia in [#1308](https://github.com/lostisland/faraday/pull/1308)
* Add Faraday 1.x examples in authentication.md docs by @iMacTia in [#1320](https://github.com/lostisland/faraday/pull/1320)
* Fix passing a URL with embedded basic auth by @iMacTia in [#1324](https://github.com/lostisland/faraday/pull/1324)
* Register JSON middleware by @mollerhoj in [#1331](https://github.com/lostisland/faraday/pull/1331)
* Retry middleware should handle string exception class name consistently by @jrochkind in [#1334](https://github.com/lostisland/faraday/pull/1334)
* Improve request info in exceptions raised by RaiseError Middleware by @willianzocolau in [#1335](https://github.com/lostisland/faraday/pull/1335)
* Remove net-http adapter and update docs by @iMacTia in [#1336](https://github.com/lostisland/faraday/pull/1336)
* Explain plan for faraday_middleware in UPGRADING.md by @iMacTia in [#1339](https://github.com/lostisland/faraday/pull/1339)
* Scripts folder cleanup by @iMacTia in [#1340](https://github.com/lostisland/faraday/pull/1340)
* Replace `Hash#merge` with `Utils#deep_merge` for connection options by @xkwd in [#1343](https://github.com/lostisland/faraday/pull/1343)
* Callable authorizers by @sled in [#1345](https://github.com/lostisland/faraday/pull/1345)
* Default value for exc error by @DariuszMusielak in [#1351](https://github.com/lostisland/faraday/pull/1351)
* Don't call `retry_block` unless a retry is going to happen by @jrochkind in [#1350](https://github.com/lostisland/faraday/pull/1350)
* Improve documentation for v2 by @iMacTia in [#1353](https://github.com/lostisland/faraday/pull/1353)
* Remove default `default_adapter` (yes, you read that right) by @iMacTia in [#1354](https://github.com/lostisland/faraday/pull/1354)
* Remove retry middleware by @iMacTia in [#1356](https://github.com/lostisland/faraday/pull/1356)
* Remove multipart middleware and all its documentation and tests by @iMacTia in [#1357](https://github.com/lostisland/faraday/pull/1357)
## [1.9.3](https://github.com/lostisland/faraday/compare/v1.9.2...v1.9.3) (2022-01-06)
* Re-add support for Ruby 2.4+ by @iMacTia in [#1371](https://github.com/lostisland/faraday/pull/1371)
## [1.9.2](https://github.com/lostisland/faraday/compare/v1.9.1...v1.9.2) (2022-01-06)
* Add alias with legacy name to gemified middleware by @iMacTia in [#1372](https://github.com/lostisland/faraday/pull/1372)
## [1.9.1](https://github.com/lostisland/faraday/compare/v1.9.0...v1.9.1) (2022-01-06)
* Update adapter dependencies in Gemspec by @iMacTia in [#1370](https://github.com/lostisland/faraday/pull/1370)
## [1.9.0](https://github.com/lostisland/faraday/compare/v1.8.0...v1.9.0) (2022-01-06)
* Use external multipart and retry middleware by @iMacTia in [#1367](https://github.com/lostisland/faraday/pull/1367)
## [1.8.0](https://github.com/lostisland/faraday/releases/tag/v1.8.0) (2021-09-18)
### Features
* Backport authorization procs (#1322, @jarl-dk)
## [v1.7.0](https://github.com/lostisland/faraday/releases/tag/v1.7.0) (2021-08-09)
### Features
* Add strict_mode to Test::Stubs (#1298, @yykamei)
## [v1.6.0](https://github.com/lostisland/faraday/releases/tag/v1.6.0) (2021-08-01)
### Misc
* Use external Rack adapter (#1296, @iMacTia)
## [v1.5.1](https://github.com/lostisland/faraday/releases/tag/v1.5.1) (2021-07-11)
### Fixes
* Fix JRuby incompatibility after moving out EM adapters (#1294, @ahorek)
### Documentation
* Update YARD to follow RackBuilder (#1292, @kachick)
## [v1.5.0](https://github.com/lostisland/faraday/releases/tag/v1.5.0) (2021-07-04)
### Misc
* Use external httpclient adapter (#1289, @iMacTia)
* Use external patron adapter (#1290, @iMacTia)
## [v1.4.3](https://github.com/lostisland/faraday/releases/tag/v1.4.3) (2021-06-24)
### Fixes
* Silence warning (#1286, @gurgeous)
* Always dup url_prefix in Connection#build_exclusive_url (#1288, @alexeyds)
## [v1.4.2](https://github.com/lostisland/faraday/releases/tag/v1.4.2) (2021-05-22)
### Fixes
* Add proxy setting when url_prefix is changed (#1276, @ci)
* Default proxy scheme to http:// if necessary, fixes #1282 (#1283, @gurgeous)
### Documentation
* Improve introduction page (#1273, @gurgeous)
* Docs: add more middleware examples (#1277, @gurgeous)
### Misc
* Use external `em_http` and `em_synchrony` adapters (#1274, @iMacTia)
## [v1.4.1](https://github.com/lostisland/faraday/releases/tag/v1.4.1) (2021-04-18)
### Fixes
* Fix dependencies from external adapter gems (#1269, @iMacTia)
## [v1.4.0](https://github.com/lostisland/faraday/releases/tag/v1.4.0) (2021-04-16)
### Highlights
With this release, we continue the work of gradually moving out adapters into their own gems 🎉
Thanks to @MikeRogers0 for helping the Faraday team in progressing with this quest 👏
And thanks to @olleolleolle efforts, Faraday is becoming more inclusive than ever 🤗
Faraday's `master` branch has been renamed into `main`, we have an official policy on inclusive language and even a rubocop plugin to check for non-inclusive words ❤️!
Checkout the "Misc" section below for more details 🙌 !
### Fixes
* Fix NoMethodError undefined method 'coverage' (#1255, @Maroo-b)
### Documentation
* Some docs on EventMachine adapters. (#1232, @damau)
* CONTRIBUTING: Fix grammar and layout (#1261, @olleolleolle)
### Misc
* Replacing Net::HTTP::Persistent with faraday-net_http_persistent (#1250, @MikeRogers0)
* CI: Configure the regenerated Coveralls token (#1256, @olleolleolle)
* Replace Excon adapter with Faraday::Excon gem, and fix autoloading issue with Faraday::NetHttpPersistent (#1257, @iMacTia)
* Drop CodeClimate (#1259, @olleolleolle)
* CI: Rename default branch to main (#1263, @olleolleolle)
* Drop RDoc support file .document (#1264, @olleolleolle, @iMacTia)
* CONTRIBUTING: add a policy on inclusive language (#1262, @olleolleolle)
* Add rubocop-inclusivity (#1267, @olleolleolle, @iMacTia)
## [v1.3.1](https://github.com/lostisland/faraday/releases/tag/v1.3.1) (2021-04-16)
### Fixes
* Escape colon in path segment (#1237, @yarafan)
* Handle IPv6 address String on Faraday::Connection#proxy_from_env (#1252, @cosmo0920)
### Documentation
* Fix broken Rubydoc.info links (#1236, @nickcampbell18)
* Add httpx to list of external adapters (#1246, @HoneyryderChuck)
### Misc
* Refactor CI to remove duplicated line (#1230, @tricknotes)
* Gemspec: Pick a good ruby2_keywords release (#1241, @olleolleolle)
## [v1.3.0](https://github.com/lostisland/faraday/releases/tag/v1.3.0) (2020-12-31)
### Highlights
Faraday v1.3.0 is the first release to officially support Ruby 3.0 in the CI pipeline 🎉 🍾!
This is also the first release with a previously "included" adapter (Net::HTTP) being isolated into a [separate gem](https://github.com/lostisland/faraday-net_http) 🎊!
The new adapter is added to Faraday as a dependency for now, so that means full backwards-compatibility, but just to be safe be careful when upgrading!
This is a huge step towards are Faraday v2.0 objective of pushing adapters and middleware into separate gems.
Many thanks to the Faraday Team, @JanDintel and everyone who attended the [ROSS Conf remote event](https://www.rossconf.io/event/remote/)
### Features
* Improves consistency with Faraday::Error and Faraday::RaiseError (#1229, @qsona, @iMacTia)
### Fixes
* Don't assign to global ::Timer (#1227, @bpo)
### Documentation
* CHANGELOG: add releases after 1.0 (#1225, @olleolleolle)
* Improves retry middleware documentation. (#1228, @iMacTia)
### Misc
* Move out Net::HTTP adapter (#1222, @JanDintel, @iMacTia)
* Adds Ruby 3.0 to CI Matrix (#1226, @iMacTia)
## [v1.2.0](https://github.com/lostisland/faraday/releases/tag/v1.2.0) (2020-12-23)
### Features
* Introduces `on_request` and `on_complete` methods in `Faraday::Middleware`. (#1194, @iMacTia)
### Fixes
* Require 'date' to avoid retry exception (#1206, @rustygeldmacher)
* Fix rdebug recursion issue (#1205, @native-api)
* Update call to `em_http_ssl_patch` (#1202, @kylekeesling)
* `EmHttp` adapter: drop superfluous loaded? check (#1213, @olleolleolle)
* Avoid 1 use of keyword hackery (#1211, @grosser)
* Fix #1219 `Net::HTTP` still uses env proxy (#1221, @iMacTia)
### Documentation
* Add comment in gemspec to explain exposure of `examples` and `spec` folders. (#1192, @iMacTia)
* Adapters, how to create them (#1193, @olleolleolle)
* Update documentation on using the logger (#1196, @tijmenb)
* Adjust the retry documentation and spec to align with implementation (#1198, @nbeyer)
### Misc
* Test against ruby head (#1208, @grosser)
## [v1.1.0](https://github.com/lostisland/faraday/releases/tag/v1.1.0) (2020-10-17)
### Features
* Makes parameters sorting configurable (#1162 @wishdev)
* Introduces `flat_encode` option for multipart adapter. (#1163 @iMacTia)
* Include request info in exceptions raised by RaiseError Middleware (#1181 @SandroDamilano)
### Fixes
* Avoid `last arg as keyword param` warning when building user middleware on Ruby 2.7 (#1153 @dgholz)
* Limits net-http-persistent version to < 4.0 (#1156 @iMacTia)
* Update `typhoeus` to new stable version (`1.4`) (#1159 @AlexWayfer)
* Properly fix test failure with Rack 2.1+. (#1171 @voxik)
### Documentation
* Improves documentation on how to contribute to the site by using Docker. (#1175 @iMacTia)
* Remove retry_change_requests from documentation (#1185 @stim371)
### Misc
* Link from GitHub Actions badge to CI workflow (#1141 @olleolleolle)
* Return tests of `Test` adapter (#1147 @AlexWayfer)
* Add 1.0 release to wording in CONTRIBUTING (#1155 @olleolleolle)
* Fix linting bumping Rubocop to 0.90.0 (#1182 @iMacTia)
* Drop `git ls-files` in gemspec (#1183 @utkarsh2102)
* Upgrade CI to ruby/setup-ruby (#1187 @gogainda)
## [v1.0.1](https://github.com/lostisland/faraday/releases/tag/v1.0.1) (2020-03-29)
### Fixes
* Use Net::HTTP#start(&block) to ensure closed TCP connections (#1117)
* Fully qualify constants to be checked (#1122)
* Allows `parse` method to be private/protected in response middleware (#1123)
* Encode Spaces in Query Strings as '%20' Instead of '+' (#1125)
* Limits rack to v2.0.x (#1127)
* Adapter Registry reads also use mutex (#1136)
### Documentation
* Retry middleware documentation fix (#1109)
* Docs(retry): precise usage of retry-after (#1111)
* README: Link the logo to the website (#1112)
* Website: add search bar (#1116)
* Fix request/response mix-up in docs text (#1132)
## [v1.0](https://github.com/lostisland/faraday/releases/tag/v1.0.0) (2020-01-22)
Features:
* Add #trace support to Faraday::Connection #861 (@technoweenie)
* Add the log formatter that is easy to override and safe to inherit #889 (@prikha)
* Support standalone adapters #941 (@iMacTia)
* Introduce Faraday::ConflictError for 409 response code #979 (@lucasmoreno)
* Add support for setting `read_timeout` option separately #1003 (@springerigor)
* Refactor and cleanup timeout settings across adapters #1022 (@technoweenie)
* Create ParamPart class to allow multipart posts with JSON content and file upload at the same time #1017 (@jeremy-israel)
* Copy UploadIO const -> FilePart for consistency with ParamPart #1018, #1021 (@technoweenie)
* Implement streaming responses in the Excon adapter #1026 (@technoweenie)
* Add default implementation of `Middleware#close`. #1069 (@ioquatix)
* Add `Adapter#close` so that derived classes can call super. #1091 (@ioquatix)
* Add log_level option to logger default formatter #1079 (@amrrbakry)
* Fix empty array for FlatParamsEncoder `{key: []} -> "key="` #1084 (@mrexox)
Bugs:
* Explicitly require date for DateTime library in Retry middleware #844 (@nickpresta)
* Refactor Adapter as final endpoints #846 (@iMacTia)
* Separate Request and Response bodies in Faraday::Env #847 (@iMacTia)
* Implement Faraday::Connection#options to make HTTP requests with the OPTIONS verb. #857 (@technoweenie)
* Multipart: Drop Ruby 1.8 String behavior compat #892 (@olleolleolle)
* Fix Ruby warnings in Faraday::Options.memoized #962 (@technoweenie)
* Allow setting min/max SSL version for a Net::HTTP::Persistent connection #972, #973 (@bdewater, @olleolleolle)
* Fix instances of frozen empty string literals #1040 (@BobbyMcWho)
* remove temp_proxy and improve proxy tests #1063 (@technoweenie)
* improve error initializer consistency #1095 (@technoweenie)
Misc:
* Convert minitest suite to RSpec #832 (@iMacTia, with help from @gaynetdinov, @Insti, @technoweenie)
* Major effort to update code to RuboCop standards. #854 (@olleolleolle, @iMacTia, @technoweenie, @htwroclau, @jherdman, @Drenmi, @Insti)
* Rubocop #1044, #1047 (@BobbyMcWho, @olleolleolle)
* Documentation tweaks (@adsteel, @Hubro, @iMacTia, @olleolleolle, @technoweenie)
* Update license year #981 (@Kevin-Kawai)
* Configure Jekyll plugin jekyll-remote-theme to support Docker usage #999 (@Lewiscowles1986)
* Fix Ruby 2.7 warnings #1009 (@tenderlove)
* Cleanup adapter connections #1023 (@technoweenie)
* Describe clearing cached stubs #1045 (@viraptor)
* Add project metadata to the gemspec #1046 (@orien)
## v0.17.4
Fixes:
* NetHttp adapter: wrap Errno::EADDRNOTAVAIL (#1114, @embs)
* Fix === for subclasses of deprecated classes (#1243, @mervync)
## v0.17.3
Fixes:
* Reverts changes in error classes hierarchy. #1092 (@iMacTia)
* Fix Ruby 1.9 syntax errors and improve Error class testing #1094 (@BanzaiMan,
@mrexox, @technoweenie)
Misc:
* Stops using `&Proc.new` for block forwarding. #1083 (@olleolleolle)
* Update CI to test against ruby 2.0-2.7 #1087, #1099 (@iMacTia, @olleolleolle,
@technoweenie)
* require FARADAY_DEPRECATE=warn to show Faraday v1.0 deprecation warnings
#1098 (@technoweenie)
## v0.17.1
Final release before Faraday v1.0, with important fixes for Ruby 2.7.
Fixes:
* RaiseError response middleware raises exception if HTTP client returns a nil
status. #1042 (@jonnyom, @BobbyMcWho)
Misc:
* Fix Ruby 2.7 warnings (#1009)
* Add `Faraday::Deprecate` to warn about upcoming v1.0 changes. (#1054, #1059,
#1076, #1077)
* Add release notes up to current in CHANGELOG.md (#1066)
* Port minimal rspec suite from main branch to run backported tests. (#1058)
## v0.17.0
This release is the same as v0.15.4. It was pushed to cover up releases
v0.16.0-v0.16.2.
## v0.15.4
* Expose `pool_size` as a option for the NetHttpPersistent adapter (#834)
## v0.15.3
* Make Faraday::Request serialisable with Marshal. (#803)
* Add DEFAULT_EXCEPTIONS constant to Request::Retry (#814)
* Add support for Ruby 2.6 Net::HTTP write_timeout (#824)
## v0.15.2
* Prevents `Net::HTTP` adapters to retry request internally by setting `max_retries` to 0 if available (Ruby 2.5+). (#799)
* Fixes `NestedParamsEncoder` handling of empty array values (#801)
## v0.15.1
* NetHttpPersistent adapter better reuse of SSL connections (#793)
* Refactor: inline cached_connection (#797)
* Logger middleware: use $stdout instead of STDOUT (#794)
* Fix: do not memoize/reuse Patron session (#796)
Also in this release:
* Allow setting min/max ssl version for Net::HTTP (#792)
* Allow setting min/max ssl version for Excon (#795)
## v0.15.0
Features:
* Added retry block option to retry middleware. (#770)
* Retry middleware improvements (honour Retry-After header, retry statuses) (#773)
* Improve response logger middleware output (#784)
Fixes:
* Remove unused class error (#767)
* Fix minor typo in README (#760)
* Reuse persistent connections when using net-http-persistent (#778)
* Fix Retry middleware documentation (#781)
* Returns the http response when giving up on retrying by status (#783)
## v0.14.0
Features:
* Allow overriding env proxy #754 (@iMacTia)
* Remove legacy Typhoeus adapter #715 (@olleolleolle)
* External Typhoeus Adapter Compatibility #748 (@iMacTia)
* Warn about missing adapter when making a request #743 (@antstorm)
* Faraday::Adapter::Test stubs now support entire urls (with host) #741 (@erik-escobedo)
Fixes:
* If proxy is manually provided, this takes priority over `find_proxy` #724 (@iMacTia)
* Fixes the behaviour for Excon's open_timeout (not setting write_timeout anymore) #731 (@apachelogger)
* Handle all connection timeout messages in Patron #687 (@stayhero)
## v0.13.1
* Fixes an incompatibility with Addressable::URI being used as uri_parser
## v0.13.0
Features:
* Dynamically reloads the proxy when performing a request on an absolute domain (#701)
* Adapter support for Net::HTTP::Persistent v3.0.0 (#619)
Fixes:
* Prefer #hostname over #host. (#714)
* Fixes an edge-case issue with response headers parsing (missing HTTP header) (#719)
## v0.12.2
* Parse headers from aggregated proxy requests/responses (#681)
* Guard against invalid middleware configuration with warning (#685)
* Do not use :insecure option by default in Patron (#691)
* Fixes an issue with HTTPClient not raising a `Faraday::ConnectionFailed` (#702)
* Fixes YAML serialization/deserialization for `Faraday::Utils::Headers` (#690)
* Fixes an issue with Options having a nil value (#694)
* Fixes an issue with Faraday.default_connection not using Faraday.default_connection_options (#698)
* Fixes an issue with Options.merge! and Faraday instrumentation middleware (#710)
## v0.12.1
* Fix an issue with Patron tests failing on jruby
* Fix an issue with new `rewind_files` feature that was causing an exception when the body was not an Hash
* Expose wrapped_exception in all client errors
* Add Authentication Section to the ReadMe
## v0.12.0.1
* Hotfix release to address an issue with TravisCI deploy on Rubygems
## v0.12.0
Features:
* Proxy feature now relies on Ruby `URI::Generic#find_proxy` and can use `no_proxy` ENV variable (not compatible with ruby < 2.0)
* Adds support for `context` request option to pass arbitrary information to middlewares
Fixes:
* Fix an issue with options that was causing new options to override defaults ones unexpectedly
* Rewind `UploadIO`s on retry to fix a compatibility issue
* Make multipart boundary unique
* Improvements in `README.md`
## v0.11.0
Features:
* Add `filter` method to Logger middleware
* Add support for Ruby2.4 and Minitest 6
* Introduce block syntax to customise the adapter
Fixes:
* Fix an issue that was allowing to override `default_connection_options` from a connection instance
* Fix a bug that was causing newline escape characters ("\n") to be used when building the Authorization header
## v0.10.1
- Fix an issue with HTTPClient adapter that was causing the SSL to be reset on every request
- Rescue `IOError` instead of specific subclass
- `Faraday::Utils::Headers` can now be successfully serialised in YAML
- Handle `default_connection_options` set with hash
## v0.10.0
Breaking changes:
- Drop support for Ruby 1.8
Features:
- Include wrapped exception/reponse in ClientErrors
- Add `response.reason_phrase`
- Provide option to selectively skip logging request/response headers
- Add regex support for pattern matching in `test` adapter
Fixes:
- Add `Faraday.respond_to?` to find methods managed by `method_missing`
- em-http: `request.host` instead of `connection.host` should be taken for SSL validations
- Allow `default_connection_options` to be merged when options are passed as url parameter
- Improve splitting key-value pairs in raw HTTP headers
## v0.9.2
Adapters:
- Enable gzip compression for httpclient
- Fixes default certificate store for httpclient not having default paths.
- Make excon adapter compatible with 0.44 excon version
- Add compatibility with Patron 0.4.20
- Determine default port numbers in Net::HTTP adapters (Addressable compatibility)
- em-http: wrap "connection closed by server" as ConnectionFailed type
- Wrap Errno::ETIMEDOUT in Faraday::Error::TimeoutError
Utils:
- Add Rack-compatible support for parsing `a[][b]=c` nested queries
- Encode nil values in queries different than empty strings. Before: `a=`; now: `a`.
- Have `Faraday::Utils::Headers#replace` clear internal key cache
- Dup the internal key cache when a Headers hash is copied
Env and middleware:
- Ensure `env` stored on middleware response has reference to the response
- Ensure that Response properties are initialized during `on_complete` (VCR compatibility)
- Copy request options in Faraday::Connection#dup
- Env custom members should be copied by Env.from(env)
- Honour per-request `request.options.params_encoder`
- Fix `interval_randomness` data type for Retry middleware
- Add maximum interval option for Retry middleware
## v0.9.1
* Refactor Net:HTTP adapter so that with_net_http_connection can be overridden to allow pooled connections. (@Ben-M)
* Add configurable methods that bypass `retry_if` in the Retry request middleware. (@mike-bourgeous)
## v0.9.0
* Add HTTPClient adapter (@hakanensari)
* Improve Retry handler (@mislav)
* Remove autoloading by default (@technoweenie)
* Improve internal docs (@technoweenie, @mislav)
* Respect user/password in http proxy string (@mislav)
* Adapter options are structs. Reinforces consistent options across adapters
(@technoweenie)
* Stop stripping trailing / off base URLs in a Faraday::Connection. (@technoweenie)
* Add a configurable URI parser. (@technoweenie)
* Remove need to manually autoload when using the authorization header helpers on `Faraday::Connection`. (@technoweenie)
* `Faraday::Adapter::Test` respects the `Faraday::RequestOptions#params_encoder` option. (@technoweenie)
+20
View File
@@ -0,0 +1,20 @@
Copyright (c) 2009-2023 Rick Olson, Zack Hobson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+67
View File
@@ -0,0 +1,67 @@
# [![Faraday](./docs/_media/home-logo.svg)][website]
[![Gem Version](https://badge.fury.io/rb/faraday.svg)](https://rubygems.org/gems/faraday)
[![GitHub Actions CI](https://github.com/lostisland/faraday/workflows/CI/badge.svg)](https://github.com/lostisland/faraday/actions?query=workflow%3ACI)
[![GitHub Discussions](https://img.shields.io/github/discussions/lostisland/faraday?logo=github)](https://github.com/lostisland/faraday/discussions)
Faraday is an HTTP client library abstraction layer that provides a common interface over many
adapters (such as Net::HTTP) and embraces the concept of Rack middleware when processing the request/response cycle.
Take a look at [Awesome Faraday][awesome] for a list of available adapters and middleware.
## Why use Faraday?
Faraday gives you the power of Rack middleware for manipulating HTTP requests and responses,
making it easier to build sophisticated API clients or web service libraries that abstract away
the details of how HTTP requests are made.
Faraday comes with a lot of features out of the box, such as:
* Support for multiple adapters (Net::HTTP, Typhoeus, Patron, Excon, HTTPClient, and more)
* Persistent connections (keep-alive)
* Parallel requests
* Automatic response parsing (JSON, XML, YAML)
* Customization of the request/response cycle with middleware
* Support for streaming responses
* Support for uploading files
* And much more!
## Getting Started
The best starting point is the [Faraday Website][website], with its introduction and explanation.
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) 🚀
## Supported Ruby versions
This library aims to support and is [tested against][actions] the currently officially supported Ruby
implementations. This means that, even without a major release, we could add or drop support for Ruby versions,
following their [EOL](https://endoflife.date/ruby).
Currently that means we support Ruby 3.0+
If something doesn't work on one of these Ruby versions, it's a bug.
This library may inadvertently work (or seem to work) on other Ruby
implementations and versions, however support will only be provided for the versions listed
above.
If you would like this library to support another Ruby version, you may
volunteer to be a maintainer. Being a maintainer entails making sure all tests
run and pass on that implementation. When something breaks on your
implementation, you will be responsible for providing patches in a timely
fashion. If critical issues for a particular implementation exist at the time
of a major release, support for that Ruby version may be dropped.
## Contribute
Do you want to contribute to Faraday?
Open the issues page and check for the `help wanted` label!
But before you start coding, please read our [Contributing Guide][contributing]
## Copyright
&copy; 2009 - 2023, the Faraday Team. Website and branding design by [Elena Lo Piccolo](https://elelopic.design).
[awesome]: https://github.com/lostisland/awesome-faraday/#adapters
[website]: https://lostisland.github.io/faraday
[contributing]: https://github.com/lostisland/faraday/blob/main/.github/CONTRIBUTING.md
[apidoc]: https://www.rubydoc.info/github/lostisland/faraday
[actions]: https://github.com/lostisland/faraday/actions
+9
View File
@@ -0,0 +1,9 @@
# frozen_string_literal: true
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec) do |task|
task.ruby_opts = %w[-W]
end
task default: :spec
@@ -0,0 +1,119 @@
# frozen_string_literal: true
# Requires Ruby with rspec and faraday gems.
# rspec client_spec.rb
require 'faraday'
require 'json'
# Example API client
class Client
def initialize(conn)
@conn = conn
end
def httpbingo(jname, params: {})
res = @conn.get("/#{jname}", params)
data = JSON.parse(res.body)
data['origin']
end
def foo(params)
res = @conn.post('/foo', JSON.dump(params))
res.status
end
end
RSpec.describe Client do
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
let(:conn) { Faraday.new { |b| b.adapter(:test, stubs) } }
let(:client) { Client.new(conn) }
it 'parses origin' do
stubs.get('/ip') do |env|
# optional: you can inspect the Faraday::Env
expect(env.url.path).to eq('/ip')
[
200,
{ 'Content-Type': 'application/javascript' },
'{"origin": "127.0.0.1"}'
]
end
# uncomment to trigger stubs.verify_stubbed_calls failure
# stubs.get('/unused') { [404, {}, ''] }
expect(client.httpbingo('ip')).to eq('127.0.0.1')
stubs.verify_stubbed_calls
end
it 'handles 404' do
stubs.get('/api') do
[
404,
{ 'Content-Type': 'application/javascript' },
'{}'
]
end
expect(client.httpbingo('api')).to be_nil
stubs.verify_stubbed_calls
end
it 'handles exception' do
stubs.get('/api') do
raise Faraday::ConnectionFailed
end
expect { client.httpbingo('api') }.to raise_error(Faraday::ConnectionFailed)
stubs.verify_stubbed_calls
end
context 'When the test stub is run in strict_mode' do
let(:stubs) { Faraday::Adapter::Test::Stubs.new(strict_mode: true) }
it 'verifies the all parameter values are identical' do
stubs.get('/api?abc=123') do
[
200,
{ 'Content-Type': 'application/javascript' },
'{"origin": "127.0.0.1"}'
]
end
# uncomment to raise Stubs::NotFound
# expect(client.httpbingo('api', params: { abc: 123, foo: 'Kappa' })).to eq('127.0.0.1')
expect(client.httpbingo('api', params: { abc: 123 })).to eq('127.0.0.1')
stubs.verify_stubbed_calls
end
end
context 'When the Faraday connection is configured with FlatParamsEncoder' do
let(:conn) { Faraday.new(request: { params_encoder: Faraday::FlatParamsEncoder }) { |b| b.adapter(:test, stubs) } }
it 'handles the same multiple URL parameters' do
stubs.get('/api?a=x&a=y&a=z') { [200, { 'Content-Type' => 'application/json' }, '{"origin": "127.0.0.1"}'] }
# uncomment to raise Stubs::NotFound
# expect(client.httpbingo('api', params: { a: %w[x y] })).to eq('127.0.0.1')
expect(client.httpbingo('api', params: { a: %w[x y z] })).to eq('127.0.0.1')
stubs.verify_stubbed_calls
end
end
context 'When you want to test the body, you can use a proc as well as string' do
it 'tests with a string' do
stubs.post('/foo', '{"name":"YK"}') { [200, {}, ''] }
expect(client.foo(name: 'YK')).to eq 200
stubs.verify_stubbed_calls
end
it 'tests with a proc' do
check = ->(request_body) { JSON.parse(request_body).slice('name') == { 'name' => 'YK' } }
stubs.post('/foo', check) { [200, {}, ''] }
expect(client.foo(name: 'YK', created_at: Time.now)).to eq 200
stubs.verify_stubbed_calls
end
end
end
@@ -0,0 +1,144 @@
# frozen_string_literal: true
# Requires Ruby with test-unit and faraday gems.
# ruby client_test.rb
require 'faraday'
require 'json'
require 'test/unit'
# Example API client
class Client
def initialize(conn)
@conn = conn
end
def httpbingo(jname, params: {})
res = @conn.get("/#{jname}", params)
data = JSON.parse(res.body)
data['origin']
end
def foo(params)
res = @conn.post('/foo', JSON.dump(params))
res.status
end
end
# Example API client test
class ClientTest < Test::Unit::TestCase
def test_httpbingo_name
stubs = Faraday::Adapter::Test::Stubs.new
stubs.get('/api') do |env|
# optional: you can inspect the Faraday::Env
assert_equal '/api', env.url.path
[
200,
{ 'Content-Type': 'application/javascript' },
'{"origin": "127.0.0.1"}'
]
end
# uncomment to trigger stubs.verify_stubbed_calls failure
# stubs.get('/unused') { [404, {}, ''] }
cli = client(stubs)
assert_equal '127.0.0.1', cli.httpbingo('api')
stubs.verify_stubbed_calls
end
def test_httpbingo_not_found
stubs = Faraday::Adapter::Test::Stubs.new
stubs.get('/api') do
[
404,
{ 'Content-Type': 'application/javascript' },
'{}'
]
end
cli = client(stubs)
assert_nil cli.httpbingo('api')
stubs.verify_stubbed_calls
end
def test_httpbingo_exception
stubs = Faraday::Adapter::Test::Stubs.new
stubs.get('/api') do
raise Faraday::ConnectionFailed
end
cli = client(stubs)
assert_raise Faraday::ConnectionFailed do
cli.httpbingo('api')
end
stubs.verify_stubbed_calls
end
def test_strict_mode
stubs = Faraday::Adapter::Test::Stubs.new(strict_mode: true)
stubs.get('/api?abc=123') do
[
200,
{ 'Content-Type': 'application/javascript' },
'{"origin": "127.0.0.1"}'
]
end
cli = client(stubs)
assert_equal '127.0.0.1', cli.httpbingo('api', params: { abc: 123 })
# uncomment to raise Stubs::NotFound
# assert_equal '127.0.0.1', cli.httpbingo('api', params: { abc: 123, foo: 'Kappa' })
stubs.verify_stubbed_calls
end
def test_non_default_params_encoder
stubs = Faraday::Adapter::Test::Stubs.new(strict_mode: true)
stubs.get('/api?a=x&a=y&a=z') do
[
200,
{ 'Content-Type': 'application/javascript' },
'{"origin": "127.0.0.1"}'
]
end
conn = Faraday.new(request: { params_encoder: Faraday::FlatParamsEncoder }) do |builder|
builder.adapter :test, stubs
end
cli = Client.new(conn)
assert_equal '127.0.0.1', cli.httpbingo('api', params: { a: %w[x y z] })
# uncomment to raise Stubs::NotFound
# assert_equal '127.0.0.1', cli.httpbingo('api', params: { a: %w[x y] })
stubs.verify_stubbed_calls
end
def test_with_string_body
stubs = Faraday::Adapter::Test::Stubs.new do |stub|
stub.post('/foo', '{"name":"YK"}') { [200, {}, ''] }
end
cli = client(stubs)
assert_equal 200, cli.foo(name: 'YK')
stubs.verify_stubbed_calls
end
def test_with_proc_body
stubs = Faraday::Adapter::Test::Stubs.new do |stub|
check = ->(request_body) { JSON.parse(request_body).slice('name') == { 'name' => 'YK' } }
stub.post('/foo', check) { [200, {}, ''] }
end
cli = client(stubs)
assert_equal 200, cli.foo(name: 'YK', created_at: Time.now)
stubs.verify_stubbed_calls
end
def client(stubs)
conn = Faraday.new do |builder|
builder.adapter :test, stubs
end
Client.new(conn)
end
end
+157
View File
@@ -0,0 +1,157 @@
# frozen_string_literal: true
require 'cgi'
require 'date'
require 'set'
require 'forwardable'
require 'faraday/version'
require 'faraday/methods'
require 'faraday/error'
require 'faraday/middleware_registry'
require 'faraday/utils'
require 'faraday/options'
require 'faraday/connection'
require 'faraday/rack_builder'
require 'faraday/parameters'
require 'faraday/middleware'
require 'faraday/adapter'
require 'faraday/request'
require 'faraday/response'
require 'faraday/net_http'
# This is the main namespace for Faraday.
#
# It provides methods to create {Connection} objects, and HTTP-related
# methods to use directly.
#
# @example Helpful class methods for easy usage
# Faraday.get "http://faraday.com"
#
# @example Helpful class method `.new` to create {Connection} objects.
# conn = Faraday.new "http://faraday.com"
# conn.get '/'
#
module Faraday
CONTENT_TYPE = 'Content-Type'
class << self
# The root path that Faraday is being loaded from.
#
# This is the root from where the libraries are auto-loaded.
#
# @return [String]
attr_accessor :root_path
# Gets or sets the path that the Faraday libs are loaded from.
# @return [String]
attr_accessor :lib_path
# @overload default_adapter
# Gets the Symbol key identifying a default Adapter to use
# for the default {Faraday::Connection}. Defaults to `:net_http`.
# @return [Symbol] the default adapter
# @overload default_adapter=(adapter)
# Updates default adapter while resetting {.default_connection}.
# @return [Symbol] the new default_adapter.
attr_reader :default_adapter
# Option for the default_adapter
# @return [Hash] default_adapter options
attr_accessor :default_adapter_options
# Documented below, see default_connection
attr_writer :default_connection
# Tells Faraday to ignore the environment proxy (http_proxy).
# Defaults to `false`.
# @return [Boolean]
attr_accessor :ignore_env_proxy
# Initializes a new {Connection}.
#
# @param url [String,Hash] The optional String base URL to use as a prefix
# for all requests. Can also be the options Hash. Any of these
# values will be set on every request made, unless overridden
# for a specific request.
# @param options [Hash]
# @option options [String] :url Base URL
# @option options [Hash] :params Hash of unencoded URI query params.
# @option options [Hash] :headers Hash of unencoded HTTP headers.
# @option options [Hash] :request Hash of request options.
# @option options [Hash] :ssl Hash of SSL options.
# @option options [Hash] :proxy Hash of Proxy options.
# @return [Faraday::Connection]
#
# @example With an URL argument
# Faraday.new 'http://faraday.com'
# # => Faraday::Connection to http://faraday.com
#
# @example With an URL argument and an options hash
# Faraday.new 'http://faraday.com', params: { page: 1 }
# # => Faraday::Connection to http://faraday.com?page=1
#
# @example With everything in an options hash
# Faraday.new url: 'http://faraday.com',
# params: { page: 1 }
# # => Faraday::Connection to http://faraday.com?page=1
def new(url = nil, options = {}, &block)
options = Utils.deep_merge(default_connection_options, options)
Faraday::Connection.new(url, options, &block)
end
# Documented elsewhere, see default_adapter reader
def default_adapter=(adapter)
@default_connection = nil
@default_adapter = adapter
end
def respond_to_missing?(symbol, include_private = false)
default_connection.respond_to?(symbol, include_private) || super
end
# @overload default_connection
# Gets the default connection used for simple scripts.
# @return [Faraday::Connection] a connection configured with
# the default_adapter.
# @overload default_connection=(connection)
# @param connection [Faraday::Connection]
# Sets the default {Faraday::Connection} for simple scripts that
# access the Faraday constant directly, such as
# <code>Faraday.get "https://faraday.com"</code>.
def default_connection
@default_connection ||= Connection.new(default_connection_options)
end
# Gets the default connection options used when calling {Faraday#new}.
#
# @return [Faraday::ConnectionOptions]
def default_connection_options
@default_connection_options ||= ConnectionOptions.new
end
# Sets the default options used when calling {Faraday#new}.
#
# @param options [Hash, Faraday::ConnectionOptions]
def default_connection_options=(options)
@default_connection = nil
@default_connection_options = ConnectionOptions.from(options)
end
private
# Internal: Proxies method calls on the Faraday constant to
# .default_connection.
def method_missing(name, *args, &block)
if default_connection.respond_to?(name)
default_connection.send(name, *args, &block)
else
super
end
end
end
self.ignore_env_proxy = false
self.root_path = File.expand_path __dir__
self.lib_path = File.expand_path 'faraday', __dir__
self.default_adapter = :net_http
self.default_adapter_options = {}
end
@@ -0,0 +1,101 @@
# frozen_string_literal: true
module Faraday
# Base class for all Faraday adapters. Adapters are
# responsible for fulfilling a Faraday request.
class Adapter
extend MiddlewareRegistry
CONTENT_LENGTH = 'Content-Length'
# This module marks an Adapter as supporting parallel requests.
module Parallelism
attr_writer :supports_parallel
def supports_parallel?
@supports_parallel
end
def inherited(subclass)
super
subclass.supports_parallel = supports_parallel?
end
end
extend Parallelism
self.supports_parallel = false
def initialize(_app = nil, opts = {}, &block)
@app = ->(env) { env.response }
@connection_options = opts
@config_block = block
end
# Yields or returns an adapter's configured connection. Depends on
# #build_connection being defined on this adapter.
#
# @param env [Faraday::Env, Hash] The env object for a faraday request.
#
# @return The return value of the given block, or the HTTP connection object
# if no block is given.
def connection(env)
conn = build_connection(env)
return conn unless block_given?
yield conn
end
# Close any persistent connections. The adapter should still be usable
# after calling close.
def close
# Possible implementation:
# @app.close if @app.respond_to?(:close)
end
def call(env)
env.clear_body if env.needs_body?
env.response = Response.new
end
private
def save_response(env, status, body, headers = nil, reason_phrase = nil, finished: true)
env.status = status
env.body = body
env.reason_phrase = reason_phrase&.to_s&.strip
env.response_headers = Utils::Headers.new.tap do |response_headers|
response_headers.update headers unless headers.nil?
yield(response_headers) if block_given?
end
env.response.finish(env) unless env.parallel? || !finished
env.response
end
# Fetches either a read, write, or open timeout setting. Defaults to the
# :timeout value if a more specific one is not given.
#
# @param type [Symbol] Describes which timeout setting to get: :read,
# :write, or :open.
# @param options [Hash] Hash containing Symbol keys like :timeout,
# :read_timeout, :write_timeout, or :open_timeout
#
# @return [Integer, nil] Timeout duration in seconds, or nil if no timeout
# has been set.
def request_timeout(type, options)
key = TIMEOUT_KEYS.fetch(type) do
msg = "Expected :read, :write, :open. Got #{type.inspect} :("
raise ArgumentError, msg
end
options[key] || options[:timeout]
end
TIMEOUT_KEYS = {
read: :read_timeout,
open: :open_timeout,
write: :write_timeout
}.freeze
end
end
require 'faraday/adapter/test'
@@ -0,0 +1,311 @@
# frozen_string_literal: true
require 'timeout'
module Faraday
class Adapter
# @example
# test = Faraday::Connection.new do
# use Faraday::Adapter::Test do |stub|
# # Define matcher to match the request
# stub.get '/resource.json' do
# # return static content
# [200, {'Content-Type' => 'application/json'}, 'hi world']
# end
#
# # response with content generated based on request
# stub.get '/showget' do |env|
# [200, {'Content-Type' => 'text/plain'}, env[:method].to_s]
# end
#
# # A regular expression can be used as matching filter
# stub.get /\A\/items\/(\d+)\z/ do |env, meta|
# # in case regular expression is used, an instance of MatchData
# # can be received
# [200,
# {'Content-Type' => 'text/plain'},
# "showing item: #{meta[:match_data][1]}"
# ]
# end
#
# # Test the request body is the same as the stubbed body
# stub.post('/bar', 'name=YK&word=call') { [200, {}, ''] }
#
# # You can pass a proc as a stubbed body and check the request body in your way.
# # In this case, the proc should return true or false.
# stub.post('/foo', ->(request_body) do
# JSON.parse(request_body).slice('name') == { 'name' => 'YK' } }) { [200, {}, '']
# end
#
# # You can set strict_mode to exactly match the stubbed requests.
# stub.strict_mode = true
# end
# end
#
# resp = test.get '/resource.json'
# resp.body # => 'hi world'
#
# resp = test.get '/showget'
# resp.body # => 'get'
#
# resp = test.get '/items/1'
# resp.body # => 'showing item: 1'
#
# resp = test.get '/items/2'
# resp.body # => 'showing item: 2'
#
# resp = test.post '/bar', 'name=YK&word=call'
# resp.status # => 200
#
# resp = test.post '/foo', JSON.dump(name: 'YK', created_at: Time.now)
# resp.status # => 200
class Test < Faraday::Adapter
attr_accessor :stubs
# A stack of Stubs
class Stubs
class NotFound < StandardError
end
def initialize(strict_mode: false)
# { get: [Stub, Stub] }
@stack = {}
@consumed = {}
@strict_mode = strict_mode
@stubs_mutex = Monitor.new
yield(self) if block_given?
end
def empty?
@stack.empty?
end
# @param env [Faraday::Env]
def match(env)
request_method = env[:method]
return false unless @stack.key?(request_method)
stack = @stack[request_method]
consumed = (@consumed[request_method] ||= [])
@stubs_mutex.synchronize do
stub, meta = matches?(stack, env)
if stub
removed = stack.delete(stub)
consumed << removed unless removed.nil?
return stub, meta
end
end
matches?(consumed, env)
end
def get(path, headers = {}, &block)
new_stub(:get, path, headers, &block)
end
def head(path, headers = {}, &block)
new_stub(:head, path, headers, &block)
end
def post(path, body = nil, headers = {}, &block)
new_stub(:post, path, headers, body, &block)
end
def put(path, body = nil, headers = {}, &block)
new_stub(:put, path, headers, body, &block)
end
def patch(path, body = nil, headers = {}, &block)
new_stub(:patch, path, headers, body, &block)
end
def delete(path, headers = {}, &block)
new_stub(:delete, path, headers, &block)
end
def options(path, headers = {}, &block)
new_stub(:options, path, headers, &block)
end
# Raises an error if any of the stubbed calls have not been made.
def verify_stubbed_calls
failed_stubs = []
@stack.each do |method, stubs|
next if stubs.empty?
failed_stubs.concat(
stubs.map do |stub|
"Expected #{method} #{stub}."
end
)
end
raise failed_stubs.join(' ') unless failed_stubs.empty?
end
# Set strict_mode. If the value is true, this adapter tries to find matched requests strictly,
# which means that all of a path, parameters, and headers must be the same as an actual request.
def strict_mode=(value)
@strict_mode = value
@stack.each_value do |stubs|
stubs.each do |stub|
stub.strict_mode = value
end
end
end
protected
def new_stub(request_method, path, headers = {}, body = nil, &block)
normalized_path, host =
if path.is_a?(Regexp)
path
else
[
Faraday::Utils.normalize_path(path),
Faraday::Utils.URI(path).host
]
end
path, query = normalized_path.respond_to?(:split) ? normalized_path.split('?') : normalized_path
headers = Utils::Headers.new(headers)
stub = Stub.new(host, path, query, headers, body, @strict_mode, block)
(@stack[request_method] ||= []) << stub
end
# @param stack [Hash]
# @param env [Faraday::Env]
def matches?(stack, env)
stack.each do |stub|
match_result, meta = stub.matches?(env)
return stub, meta if match_result
end
nil
end
end
# Stub request
Stub = Struct.new(:host, :path, :query, :headers, :body, :strict_mode, :block) do
# @param env [Faraday::Env]
def matches?(env)
request_host = env[:url].host
request_path = Faraday::Utils.normalize_path(env[:url].path)
request_headers = env.request_headers
request_body = env[:body]
# meta is a hash used as carrier
# that will be yielded to consumer block
meta = {}
[(host.nil? || host == request_host) &&
path_match?(request_path, meta) &&
params_match?(env) &&
body_match?(request_body) &&
headers_match?(request_headers), meta]
end
def path_match?(request_path, meta)
if path.is_a?(Regexp)
!!(meta[:match_data] = path.match(request_path))
else
path == request_path
end
end
# @param env [Faraday::Env]
def params_match?(env)
request_params = env[:params]
params = env.params_encoder.decode(query) || {}
if strict_mode
return Set.new(params) == Set.new(request_params)
end
params.keys.all? do |key|
request_params[key] == params[key]
end
end
def headers_match?(request_headers)
if strict_mode
headers_with_user_agent = headers.dup.tap do |hs|
# NOTE: Set User-Agent in case it's not set when creating Stubs.
# Users would not want to set Faraday's User-Agent explicitly.
hs[:user_agent] ||= Connection::USER_AGENT
end
return Set.new(headers_with_user_agent) == Set.new(request_headers)
end
headers.keys.all? do |key|
request_headers[key] == headers[key]
end
end
def body_match?(request_body)
return true if body.to_s.empty?
case body
when Proc
body.call(request_body)
else
request_body == body
end
end
def to_s
"#{path} #{body}"
end
end
def initialize(app, stubs = nil, &block)
super(app)
@stubs = stubs || Stubs.new
configure(&block) if block
end
def configure
yield(stubs)
end
# @param env [Faraday::Env]
def call(env)
super
env.request.params_encoder ||= Faraday::Utils.default_params_encoder
env[:params] = env.params_encoder.decode(env[:url].query) || {}
stub, meta = stubs.match(env)
unless stub
raise Stubs::NotFound, "no stubbed request for #{env[:method]} " \
"#{env[:url]} #{env[:body]} #{env[:headers]}"
end
block_arity = stub.block.arity
params = if block_arity >= 0
[env, meta].take(block_arity)
else
[env, meta]
end
timeout = request_timeout(:open, env[:request])
timeout ||= request_timeout(:read, env[:request])
status, headers, body =
if timeout
::Timeout.timeout(timeout, Faraday::TimeoutError) do
stub.block.call(*params)
end
else
stub.block.call(*params)
end
# We need to explicitly pass `reason_phrase = nil` here to avoid keyword args conflicts.
# See https://github.com/lostisland/faraday/issues/1444
# TODO: remove `nil` explicit reason_phrase once Ruby 3.0 becomes minimum req. version
save_response(env, status, body, headers, nil)
@app.call(env)
end
end
end
end
Faraday::Adapter.register_middleware(test: Faraday::Adapter::Test)
@@ -0,0 +1,30 @@
# frozen_string_literal: true
require 'monitor'
module Faraday
# AdapterRegistry registers adapter class names so they can be looked up by a
# String or Symbol name.
class AdapterRegistry
def initialize
@lock = Monitor.new
@constants = {}
end
def get(name)
klass = @lock.synchronize do
@constants[name]
end
return klass if klass
Object.const_get(name).tap { |c| set(c, name) }
end
def set(klass, name = nil)
name ||= klass.to_s
@lock.synchronize do
@constants[name] = klass
end
end
end
end
@@ -0,0 +1,555 @@
# frozen_string_literal: true
module Faraday
# Connection objects manage the default properties and the middleware
# stack for fulfilling an HTTP request.
#
# @example
#
# conn = Faraday::Connection.new 'http://httpbingo.org'
#
# # GET http://httpbingo.org/nigiri
# conn.get 'nigiri'
# # => #<Faraday::Response>
#
class Connection
# A Set of allowed HTTP verbs.
METHODS = Set.new %i[get post put delete head patch options trace]
USER_AGENT = "Faraday v#{VERSION}".freeze
# @return [Hash] URI query unencoded key/value pairs.
attr_reader :params
# @return [Hash] unencoded HTTP header key/value pairs.
attr_reader :headers
# @return [String] a URI with the prefix used for all requests from this
# Connection. This includes a default host name, scheme, port, and path.
attr_reader :url_prefix
# @return [Faraday::RackBuilder] Builder for this Connection.
attr_reader :builder
# @return [Hash] SSL options.
attr_reader :ssl
# @return [Object] the parallel manager for this Connection.
attr_reader :parallel_manager
# Sets the default parallel manager for this connection.
attr_writer :default_parallel_manager
# @return [Hash] proxy options.
attr_reader :proxy
# Initializes a new Faraday::Connection.
#
# @param url [URI, String] URI or String base URL to use as a prefix for all
# requests (optional).
# @param options [Hash, Faraday::ConnectionOptions]
# @option options [URI, String] :url ('http:/') URI or String base URL
# @option options [Hash<String => String>] :params URI query unencoded
# key/value pairs.
# @option options [Hash<String => String>] :headers Hash of unencoded HTTP
# header key/value pairs.
# @option options [Hash] :request Hash of request options.
# @option options [Hash] :ssl Hash of SSL options.
# @option options [Hash, URI, String] :proxy proxy options, either as a URL
# or as a Hash
# @option options [URI, String] :proxy[:uri]
# @option options [String] :proxy[:user]
# @option options [String] :proxy[:password]
# @yield [self] after all setup has been done
def initialize(url = nil, options = nil)
options = ConnectionOptions.from(options)
if url.is_a?(Hash) || url.is_a?(ConnectionOptions)
options = Utils.deep_merge(options, url)
url = options.url
end
@parallel_manager = nil
@headers = Utils::Headers.new
@params = Utils::ParamsHash.new
@options = options.request
@ssl = options.ssl
@default_parallel_manager = options.parallel_manager
@manual_proxy = nil
@builder = options.builder || begin
# pass an empty block to Builder so it doesn't assume default middleware
options.new_builder(block_given? ? proc { |b| } : nil)
end
self.url_prefix = url || 'http:/'
@params.update(options.params) if options.params
@headers.update(options.headers) if options.headers
initialize_proxy(url, options)
yield(self) if block_given?
@headers[:user_agent] ||= USER_AGENT
end
def initialize_proxy(url, options)
@manual_proxy = !!options.proxy
@proxy =
if options.proxy
ProxyOptions.from(options.proxy)
else
proxy_from_env(url)
end
end
# Sets the Hash of URI query unencoded key/value pairs.
# @param hash [Hash]
def params=(hash)
@params.replace hash
end
# Sets the Hash of unencoded HTTP header key/value pairs.
# @param hash [Hash]
def headers=(hash)
@headers.replace hash
end
extend Forwardable
def_delegators :builder, :use, :request, :response, :adapter, :app
# Closes the underlying resources and/or connections. In the case of
# persistent connections, this closes all currently open connections
# but does not prevent new connections from being made.
def close
app.close
end
# @!method get(url = nil, params = nil, headers = nil)
# Makes a GET HTTP request without a body.
# @!scope class
#
# @param url [String, URI, nil] The optional String base URL to use as a prefix for
# all requests. Can also be the options Hash.
# @param params [Hash, nil] Hash of URI query unencoded key/value pairs.
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
#
# @example
# conn.get '/items', { page: 1 }, :accept => 'application/json'
#
# # ElasticSearch example sending a body with GET.
# conn.get '/twitter/tweet/_search' do |req|
# req.headers[:content_type] = 'application/json'
# req.params[:routing] = 'kimchy'
# req.body = JSON.generate(query: {...})
# end
#
# @yield [Faraday::Request] for further request customizations
# @return [Faraday::Response]
# @!method head(url = nil, params = nil, headers = nil)
# Makes a HEAD HTTP request without a body.
# @!scope class
#
# @param url [String, URI, nil] The optional String base URL to use as a prefix for
# all requests. Can also be the options Hash.
# @param params [Hash, nil] Hash of URI query unencoded key/value pairs.
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
#
# @example
# conn.head '/items/1'
#
# @yield [Faraday::Request] for further request customizations
# @return [Faraday::Response]
# @!method delete(url = nil, params = nil, headers = nil)
# Makes a DELETE HTTP request without a body.
# @!scope class
#
# @param url [String, URI, nil] The optional String base URL to use as a prefix for
# all requests. Can also be the options Hash.
# @param params [Hash, nil] Hash of URI query unencoded key/value pairs.
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
#
# @example
# conn.delete '/items/1'
#
# @yield [Faraday::Request] for further request customizations
# @return [Faraday::Response]
# @!method trace(url = nil, params = nil, headers = nil)
# Makes a TRACE HTTP request without a body.
# @!scope class
#
# @param url [String, URI, nil] The optional String base URL to use as a prefix for
# all requests. Can also be the options Hash.
# @param params [Hash, nil] Hash of URI query unencoded key/value pairs.
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
#
# @example
# conn.connect '/items/1'
#
# @yield [Faraday::Request] for further request customizations
# @return [Faraday::Response]
# @!visibility private
METHODS_WITH_QUERY.each do |method|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{method}(url = nil, params = nil, headers = nil)
run_request(:#{method}, url, nil, headers) do |request|
request.params.update(params) if params
yield request if block_given?
end
end
RUBY
end
# @overload options()
# Returns current Connection options.
#
# @overload options(url, params = nil, headers = nil)
# Makes an OPTIONS HTTP request to the given URL.
# @param url [String, URI, nil] String base URL to sue as a prefix for all requests.
# @param params [Hash, nil] Hash of URI query unencoded key/value pairs.
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
#
# @example
# conn.options '/items/1'
#
# @yield [Faraday::Request] for further request customizations
# @return [Faraday::Response]
def options(*args)
return @options if args.empty?
url, params, headers = *args
run_request(:options, url, nil, headers) do |request|
request.params.update(params) if params
yield request if block_given?
end
end
# @!method post(url = nil, body = nil, headers = nil)
# Makes a POST HTTP request with a body.
# @!scope class
#
# @param url [String, URI, nil] The optional String base URL to use as a prefix for
# all requests. Can also be the options Hash.
# @param body [String, nil] body for the request.
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
#
# @example
# conn.post '/items', data, content_type: 'application/json'
#
# # Simple ElasticSearch indexing sample.
# conn.post '/twitter/tweet' do |req|
# req.headers[:content_type] = 'application/json'
# req.params[:routing] = 'kimchy'
# req.body = JSON.generate(user: 'kimchy', ...)
# end
#
# @yield [Faraday::Request] for further request customizations
# @return [Faraday::Response]
# @!method put(url = nil, body = nil, headers = nil)
# Makes a PUT HTTP request with a body.
# @!scope class
#
# @param url [String, URI, nil] The optional String base URL to use as a prefix for
# all requests. Can also be the options Hash.
# @param body [String, nil] body for the request.
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
#
# @example
# conn.put '/products/123', data, content_type: 'application/json'
#
# # Star a gist.
# conn.put 'https://api.github.com/gists/GIST_ID/star' do |req|
# req.headers['Accept'] = 'application/vnd.github+json'
# req.headers['Authorization'] = 'Bearer <YOUR-TOKEN>'
# req.headers['X-GitHub-Api-Version'] = '2022-11-28'
# end
#
# @yield [Faraday::Request] for further request customizations
# @return [Faraday::Response]
# @!visibility private
METHODS_WITH_BODY.each do |method|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{method}(url = nil, body = nil, headers = nil, &block)
run_request(:#{method}, url, body, headers, &block)
end
RUBY
end
# Check if the adapter is parallel-capable.
#
# @yield if the adapter isn't parallel-capable, or if no adapter is set yet.
#
# @return [Object, nil] a parallel manager or nil if yielded
# @api private
def default_parallel_manager
@default_parallel_manager ||= begin
adapter = @builder.adapter.klass if @builder.adapter
if support_parallel?(adapter)
adapter.setup_parallel_manager
elsif block_given?
yield
end
end
end
# Determine if this Faraday::Connection can make parallel requests.
#
# @return [Boolean]
def in_parallel?
!!@parallel_manager
end
# Sets up the parallel manager to make a set of requests.
#
# @param manager [Object] The parallel manager that this Connection's
# Adapter uses.
#
# @yield a block to execute multiple requests.
# @return [void]
def in_parallel(manager = nil)
@parallel_manager = manager || default_parallel_manager do
warn 'Warning: `in_parallel` called but no parallel-capable adapter ' \
'on Faraday stack'
warn caller[2, 10].join("\n")
nil
end
yield
@parallel_manager&.run
ensure
@parallel_manager = nil
end
# Sets the Hash proxy options.
#
# @param new_value [Object]
def proxy=(new_value)
@manual_proxy = true
@proxy = new_value ? ProxyOptions.from(new_value) : nil
end
def_delegators :url_prefix, :scheme, :scheme=, :host, :host=, :port, :port=
def_delegator :url_prefix, :path, :path_prefix
# Parses the given URL with URI and stores the individual
# components in this connection. These components serve as defaults for
# requests made by this connection.
#
# @param url [String, URI]
# @param encoder [Object]
#
# @example
#
# conn = Faraday::Connection.new { ... }
# conn.url_prefix = "https://httpbingo.org/api"
# conn.scheme # => https
# conn.path_prefix # => "/api"
#
# conn.get("nigiri?page=2") # accesses https://httpbingo.org/api/nigiri
def url_prefix=(url, encoder = nil)
uri = @url_prefix = Utils.URI(url)
self.path_prefix = uri.path
params.merge_query(uri.query, encoder)
uri.query = nil
with_uri_credentials(uri) do |user, password|
set_basic_auth(user, password)
uri.user = uri.password = nil
end
@proxy = proxy_from_env(url) unless @manual_proxy
end
def set_basic_auth(user, password)
header = Faraday::Utils.basic_header_from(user, password)
headers[Faraday::Request::Authorization::KEY] = header
end
# Sets the path prefix and ensures that it always has a leading
# slash.
#
# @param value [String]
#
# @return [String] the new path prefix
def path_prefix=(value)
url_prefix.path = if value
value = "/#{value}" unless value[0, 1] == '/'
value
end
end
# Takes a relative url for a request and combines it with the defaults
# set on the connection instance.
#
# @param url [String, URI, nil]
# @param extra_params [Hash]
#
# @example
# conn = Faraday::Connection.new { ... }
# conn.url_prefix = "https://httpbingo.org/api?token=abc"
# conn.scheme # => https
# conn.path_prefix # => "/api"
#
# conn.build_url("nigiri?page=2")
# # => https://httpbingo.org/api/nigiri?token=abc&page=2
#
# conn.build_url("nigiri", page: 2)
# # => https://httpbingo.org/api/nigiri?token=abc&page=2
#
def build_url(url = nil, extra_params = nil)
uri = build_exclusive_url(url)
query_values = params.dup.merge_query(uri.query, options.params_encoder)
query_values.update(extra_params) if extra_params
uri.query =
if query_values.empty?
nil
else
query_values.to_query(options.params_encoder)
end
uri
end
# Builds and runs the Faraday::Request.
#
# @param method [Symbol] HTTP method.
# @param url [String, URI, nil] String or URI to access.
# @param body [String, nil] The request body that will eventually be converted to
# a string.
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
#
# @return [Faraday::Response]
def run_request(method, url, body, headers)
unless METHODS.include?(method)
raise ArgumentError, "unknown http method: #{method}"
end
request = build_request(method) do |req|
req.options.proxy = proxy_for_request(url)
req.url(url) if url
req.headers.update(headers) if headers
req.body = body if body
yield(req) if block_given?
end
builder.build_response(self, request)
end
# Creates and configures the request object.
#
# @param method [Symbol]
#
# @yield [Faraday::Request] if block given
# @return [Faraday::Request]
def build_request(method)
Request.create(method) do |req|
req.params = params.dup
req.headers = headers.dup
req.options = options.dup
yield(req) if block_given?
end
end
# Build an absolute URL based on url_prefix.
#
# @param url [String, URI, nil]
# @param params [Faraday::Utils::ParamsHash] A Faraday::Utils::ParamsHash to
# replace the query values
# of the resulting url (default: nil).
#
# @return [URI]
def build_exclusive_url(url = nil, params = nil, params_encoder = nil)
url = nil if url.respond_to?(:empty?) && url.empty?
base = url_prefix.dup
if url && !base.path.end_with?('/')
base.path = "#{base.path}/" # ensure trailing slash
end
url = url.to_s.gsub(':', '%3A') if URI.parse(url.to_s).opaque
uri = url ? base + url : base
if params
uri.query = params.to_query(params_encoder || options.params_encoder)
end
uri.query = nil if uri.query && uri.query.empty?
uri
end
# Creates a duplicate of this Faraday::Connection.
#
# @api private
#
# @return [Faraday::Connection]
def dup
self.class.new(build_exclusive_url,
headers: headers.dup,
params: params.dup,
builder: builder.dup,
ssl: ssl.dup,
request: options.dup)
end
# Yields username and password extracted from a URI if they both exist.
#
# @param uri [URI]
# @yield [username, password] any username and password
# @yieldparam username [String] any username from URI
# @yieldparam password [String] any password from URI
# @return [void]
# @api private
def with_uri_credentials(uri)
return unless uri.user && uri.password
yield(Utils.unescape(uri.user), Utils.unescape(uri.password))
end
def proxy_from_env(url)
return if Faraday.ignore_env_proxy
uri = nil
case url
when String
uri = Utils.URI(url)
uri = if uri.host.nil?
find_default_proxy
else
URI.parse("#{uri.scheme}://#{uri.host}").find_proxy
end
when URI
uri = url.find_proxy
when nil
uri = find_default_proxy
end
ProxyOptions.from(uri) if uri
end
def find_default_proxy
uri = ENV.fetch('http_proxy', nil)
return unless uri && !uri.empty?
uri = "http://#{uri}" unless uri.match?(/^http/i)
uri
end
def proxy_for_request(url)
return proxy if @manual_proxy
if url && Utils.URI(url).absolute?
proxy_from_env(url)
else
proxy
end
end
def support_parallel?(adapter)
adapter.respond_to?(:supports_parallel?) && adapter&.supports_parallel?
end
end
end
@@ -0,0 +1,105 @@
# frozen_string_literal: true
module Faraday
# FlatParamsEncoder manages URI params as a flat hash. Any Array values repeat
# the parameter multiple times.
module FlatParamsEncoder
class << self
extend Forwardable
def_delegators :'Faraday::Utils', :escape, :unescape
end
# Encode converts the given param into a URI querystring. Keys and values
# will converted to strings and appropriately escaped for the URI.
#
# @param params [Hash] query arguments to convert.
#
# @example
#
# encode({a: %w[one two three], b: true, c: "C"})
# # => 'a=one&a=two&a=three&b=true&c=C'
#
# @return [String] the URI querystring (without the leading '?')
def self.encode(params)
return nil if params.nil?
unless params.is_a?(Array)
unless params.respond_to?(:to_hash)
raise TypeError,
"Can't convert #{params.class} into Hash."
end
params = params.to_hash
params = params.map do |key, value|
key = key.to_s if key.is_a?(Symbol)
[key, value]
end
# Only to be used for non-Array inputs. Arrays should preserve order.
params.sort! if @sort_params
end
# The params have form [['key1', 'value1'], ['key2', 'value2']].
buffer = +''
params.each do |key, value|
encoded_key = escape(key)
if value.nil?
buffer << "#{encoded_key}&"
elsif value.is_a?(Array)
if value.empty?
buffer << "#{encoded_key}=&"
else
value.each do |sub_value|
encoded_value = escape(sub_value)
buffer << "#{encoded_key}=#{encoded_value}&"
end
end
else
encoded_value = escape(value)
buffer << "#{encoded_key}=#{encoded_value}&"
end
end
buffer.chop
end
# Decode converts the given URI querystring into a hash.
#
# @param query [String] query arguments to parse.
#
# @example
#
# decode('a=one&a=two&a=three&b=true&c=C')
# # => {"a"=>["one", "two", "three"], "b"=>"true", "c"=>"C"}
#
# @return [Hash] parsed keys and value strings from the querystring.
def self.decode(query)
return nil if query.nil?
empty_accumulator = {}
split_query = (query.split('&').map do |pair|
pair.split('=', 2) if pair && !pair.empty?
end).compact
split_query.each_with_object(empty_accumulator.dup) do |pair, accu|
pair[0] = unescape(pair[0])
pair[1] = true if pair[1].nil?
if pair[1].respond_to?(:to_str)
pair[1] = unescape(pair[1].to_str.tr('+', ' '))
end
if accu[pair[0]].is_a?(Array)
accu[pair[0]] << pair[1]
elsif accu[pair[0]]
accu[pair[0]] = [accu[pair[0]], pair[1]]
else
accu[pair[0]] = pair[1]
end
end
end
class << self
attr_accessor :sort_params
end
# Useful default for OAuth and caching.
@sort_params = true
end
end
@@ -0,0 +1,183 @@
# frozen_string_literal: true
module Faraday
# Sub-module for encoding parameters into query-string.
module EncodeMethods
# @param params [nil, Array, #to_hash] parameters to be encoded
#
# @return [String] the encoded params
#
# @raise [TypeError] if params can not be converted to a Hash
def encode(params)
return nil if params.nil?
unless params.is_a?(Array)
unless params.respond_to?(:to_hash)
raise TypeError, "Can't convert #{params.class} into Hash."
end
params = params.to_hash
params = params.map do |key, value|
key = key.to_s if key.is_a?(Symbol)
[key, value]
end
# Only to be used for non-Array inputs. Arrays should preserve order.
params.sort! if @sort_params
end
# The params have form [['key1', 'value1'], ['key2', 'value2']].
buffer = +''
params.each do |parent, value|
encoded_parent = escape(parent)
buffer << "#{encode_pair(encoded_parent, value)}&"
end
buffer.chop
end
protected
def encode_pair(parent, value)
if value.is_a?(Hash)
encode_hash(parent, value)
elsif value.is_a?(Array)
encode_array(parent, value)
elsif value.nil?
parent
else
encoded_value = escape(value)
"#{parent}=#{encoded_value}"
end
end
def encode_hash(parent, value)
value = value.map { |key, val| [escape(key), val] }.sort
buffer = +''
value.each do |key, val|
new_parent = "#{parent}%5B#{key}%5D"
buffer << "#{encode_pair(new_parent, val)}&"
end
buffer.chop
end
def encode_array(parent, value)
return "#{parent}%5B%5D" if value.empty?
buffer = +''
value.each_with_index do |val, index|
new_parent = if @array_indices
"#{parent}%5B#{index}%5D"
else
"#{parent}%5B%5D"
end
buffer << "#{encode_pair(new_parent, val)}&"
end
buffer.chop
end
end
# Sub-module for decoding query-string into parameters.
module DecodeMethods
# @param query [nil, String]
#
# @return [Array<Array, String>] the decoded params
#
# @raise [TypeError] if the nesting is incorrect
def decode(query)
return nil if query.nil?
params = {}
query.split('&').each do |pair|
next if pair.empty?
key, value = pair.split('=', 2)
key = unescape(key)
value = unescape(value.tr('+', ' ')) if value
decode_pair(key, value, params)
end
dehash(params, 0)
end
protected
SUBKEYS_REGEX = /[^\[\]]+(?:\]?\[\])?/
def decode_pair(key, value, context)
subkeys = key.scan(SUBKEYS_REGEX)
subkeys.each_with_index do |subkey, i|
is_array = subkey =~ /[\[\]]+\Z/
subkey = Regexp.last_match.pre_match if is_array
last_subkey = i == subkeys.length - 1
context = prepare_context(context, subkey, is_array, last_subkey)
add_to_context(is_array, context, value, subkey) if last_subkey
end
end
def prepare_context(context, subkey, is_array, last_subkey)
if !last_subkey || is_array
context = new_context(subkey, is_array, context)
end
if context.is_a?(Array) && !is_array
context = match_context(context, subkey)
end
context
end
def new_context(subkey, is_array, context)
value_type = is_array ? Array : Hash
if context[subkey] && !context[subkey].is_a?(value_type)
raise TypeError, "expected #{value_type.name} " \
"(got #{context[subkey].class.name}) for param `#{subkey}'"
end
context[subkey] ||= value_type.new
end
def match_context(context, subkey)
context << {} if !context.last.is_a?(Hash) || context.last.key?(subkey)
context.last
end
def add_to_context(is_array, context, value, subkey)
is_array ? context << value : context[subkey] = value
end
# Internal: convert a nested hash with purely numeric keys into an array.
# FIXME: this is not compatible with Rack::Utils.parse_nested_query
# @!visibility private
def dehash(hash, depth)
hash.each do |key, value|
hash[key] = dehash(value, depth + 1) if value.is_a?(Hash)
end
if depth.positive? && !hash.empty? && hash.keys.all? { |k| k =~ /^\d+$/ }
hash.sort.map(&:last)
else
hash
end
end
end
# This is the default encoder for Faraday requests.
# Using this encoder, parameters will be encoded respecting their structure,
# so you can send objects such as Arrays or Hashes as parameters
# for your requests.
module NestedParamsEncoder
class << self
attr_accessor :sort_params, :array_indices
extend Forwardable
def_delegators :'Faraday::Utils', :escape, :unescape
end
# Useful default for OAuth and caching.
@sort_params = true
@array_indices = false
extend EncodeMethods
extend DecodeMethods
end
end
+161
View File
@@ -0,0 +1,161 @@
# frozen_string_literal: true
# Faraday namespace.
module Faraday
# Faraday error base class.
class Error < StandardError
attr_reader :response, :wrapped_exception
def initialize(exc = nil, response = nil)
@wrapped_exception = nil unless defined?(@wrapped_exception)
@response = nil unless defined?(@response)
super(exc_msg_and_response!(exc, response))
end
def backtrace
if @wrapped_exception
@wrapped_exception.backtrace
else
super
end
end
def inspect
inner = +''
inner << " wrapped=#{@wrapped_exception.inspect}" if @wrapped_exception
inner << " response=#{@response.inspect}" if @response
inner << " #{super}" if inner.empty?
%(#<#{self.class}#{inner}>)
end
def response_status
return unless @response
@response.is_a?(Faraday::Response) ? @response.status : @response[:status]
end
def response_headers
return unless @response
@response.is_a?(Faraday::Response) ? @response.headers : @response[:headers]
end
def response_body
return unless @response
@response.is_a?(Faraday::Response) ? @response.body : @response[:body]
end
protected
# Pulls out potential parent exception and response hash, storing them in
# instance variables.
# exc - Either an Exception, a string message, or a response hash.
# response - Hash
# :status - Optional integer HTTP response status
# :headers - String key/value hash of HTTP response header
# values.
# :body - Optional string HTTP response body.
# :request - Hash
# :method - Symbol with the request HTTP method.
# :url - URI object with the url requested.
# :url_path - String with the url path requested.
# :params - String key/value hash of query params
# present in the request.
# :headers - String key/value hash of HTTP request
# header values.
# :body - String HTTP request body.
#
# If a subclass has to call this, then it should pass a string message
# to `super`. See NilStatusError.
def exc_msg_and_response!(exc, response = nil)
if @response.nil? && @wrapped_exception.nil?
@wrapped_exception, msg, @response = exc_msg_and_response(exc, response)
return msg
end
exc.to_s
end
# Pulls out potential parent exception and response hash.
def exc_msg_and_response(exc, response = nil)
return [exc, exc.message, response] if exc.respond_to?(:backtrace)
return [nil, "the server responded with status #{exc[:status]}", exc] \
if exc.respond_to?(:each_key)
[nil, exc.to_s, response]
end
end
# Faraday client error class. Represents 4xx status responses.
class ClientError < Error
end
# Raised by Faraday::Response::RaiseError in case of a 400 response.
class BadRequestError < ClientError
end
# Raised by Faraday::Response::RaiseError in case of a 401 response.
class UnauthorizedError < ClientError
end
# Raised by Faraday::Response::RaiseError in case of a 403 response.
class ForbiddenError < ClientError
end
# Raised by Faraday::Response::RaiseError in case of a 404 response.
class ResourceNotFound < ClientError
end
# Raised by Faraday::Response::RaiseError in case of a 407 response.
class ProxyAuthError < ClientError
end
# Raised by Faraday::Response::RaiseError in case of a 408 response.
class RequestTimeoutError < ClientError
end
# Raised by Faraday::Response::RaiseError in case of a 409 response.
class ConflictError < ClientError
end
# Raised by Faraday::Response::RaiseError in case of a 422 response.
class UnprocessableEntityError < ClientError
end
# Raised by Faraday::Response::RaiseError in case of a 429 response.
class TooManyRequestsError < ClientError
end
# Faraday server error class. Represents 5xx status responses.
class ServerError < Error
end
# A unified client error for timeouts.
class TimeoutError < ServerError
def initialize(exc = 'timeout', response = nil)
super(exc, response)
end
end
# Raised by Faraday::Response::RaiseError in case of a nil status in response.
class NilStatusError < ServerError
def initialize(exc, response = nil)
exc_msg_and_response!(exc, response)
super('http status could not be derived from the server response')
end
end
# A unified error for failed connections.
class ConnectionFailed < Error
end
# A unified client error for SSL errors.
class SSLError < Error
end
# Raised by middlewares that parse the response, like the JSON response middleware.
class ParsingError < Error
end
end
@@ -0,0 +1,118 @@
# frozen_string_literal: true
require 'pp' # This require is necessary for Hash#pretty_inspect to work, do not remove it, people rely on it.
module Faraday
module Logging
# Serves as an integration point to customize logging
class Formatter
extend Forwardable
DEFAULT_OPTIONS = { headers: true, bodies: false, errors: false,
log_level: :info }.freeze
def initialize(logger:, options:)
@logger = logger
@options = DEFAULT_OPTIONS.merge(options)
unless %i[debug info warn error fatal].include?(@options[:log_level])
@options[:log_level] = :info
end
@filter = []
end
def_delegators :@logger, :debug, :info, :warn, :error, :fatal
def request(env)
public_send(log_level, 'request') do
"#{env.method.upcase} #{apply_filters(env.url.to_s)}"
end
log_headers('request', env.request_headers) if log_headers?(:request)
log_body('request', env[:body]) if env[:body] && log_body?(:request)
end
def response(env)
public_send(log_level, 'response') { "Status #{env.status}" }
log_headers('response', env.response_headers) if log_headers?(:response)
log_body('response', env[:body]) if env[:body] && log_body?(:response)
end
def exception(exc)
return unless log_errors?
public_send(log_level, 'error') { exc.full_message }
log_headers('error', exc.response_headers) if exc.respond_to?(:response_headers) && log_headers?(:error)
return unless exc.respond_to?(:response_body) && exc.response_body && log_body?(:error)
log_body('error', exc.response_body)
end
def filter(filter_word, filter_replacement)
@filter.push([filter_word, filter_replacement])
end
private
def dump_headers(headers)
return if headers.nil?
headers.map { |k, v| "#{k}: #{v.inspect}" }.join("\n")
end
def dump_body(body)
if body.respond_to?(:to_str)
body.to_str
else
pretty_inspect(body)
end
end
def pretty_inspect(body)
body.pretty_inspect
end
def log_headers?(type)
case @options[:headers]
when Hash
@options[:headers][type]
else
@options[:headers]
end
end
def log_body?(type)
case @options[:bodies]
when Hash
@options[:bodies][type]
else
@options[:bodies]
end
end
def log_errors?
@options[:errors]
end
def apply_filters(output)
@filter.each do |pattern, replacement|
output = output.to_s.gsub(pattern, replacement)
end
output
end
def log_level
@options[:log_level]
end
def log_headers(type, headers)
public_send(log_level, type) { apply_filters(dump_headers(headers)) }
end
def log_body(type, body)
public_send(log_level, type) { apply_filters(dump_body(body)) }
end
end
end
end
@@ -0,0 +1,6 @@
# frozen_string_literal: true
module Faraday
METHODS_WITH_QUERY = %w[get head delete trace].freeze
METHODS_WITH_BODY = %w[post put patch].freeze
end
@@ -0,0 +1,33 @@
# frozen_string_literal: true
module Faraday
# Middleware is the basic base class of any Faraday middleware.
class Middleware
extend MiddlewareRegistry
attr_reader :app, :options
def initialize(app = nil, options = {})
@app = app
@options = options
end
def call(env)
on_request(env) if respond_to?(:on_request)
app.call(env).on_complete do |environment|
on_complete(environment) if respond_to?(:on_complete)
end
rescue StandardError => e
on_error(e) if respond_to?(:on_error)
raise
end
def close
if app.respond_to?(:close)
app.close
else
warn "#{app} does not implement \#close!"
end
end
end
end
@@ -0,0 +1,83 @@
# frozen_string_literal: true
require 'monitor'
module Faraday
# Adds the ability for other modules to register and lookup
# middleware classes.
module MiddlewareRegistry
def registered_middleware
@registered_middleware ||= {}
end
# Register middleware class(es) on the current module.
#
# @param mappings [Hash] Middleware mappings from a lookup symbol to a middleware class.
# @return [void]
#
# @example Lookup by a constant
#
# module Faraday
# class Whatever < Middleware
# # Middleware looked up by :foo returns Faraday::Whatever::Foo.
# register_middleware(foo: Whatever)
# end
# end
def register_middleware(**mappings)
middleware_mutex do
registered_middleware.update(mappings)
end
end
# Unregister a previously registered middleware class.
#
# @param key [Symbol] key for the registered middleware.
def unregister_middleware(key)
registered_middleware.delete(key)
end
# Lookup middleware class with a registered Symbol shortcut.
#
# @param key [Symbol] key for the registered middleware.
# @return [Class] a middleware Class.
# @raise [Faraday::Error] if given key is not registered
#
# @example
#
# module Faraday
# class Whatever < Middleware
# register_middleware(foo: Whatever)
# end
# end
#
# Faraday::Middleware.lookup_middleware(:foo)
# # => Faraday::Whatever
def lookup_middleware(key)
load_middleware(key) ||
raise(Faraday::Error, "#{key.inspect} is not registered on #{self}")
end
private
def middleware_mutex(&block)
@middleware_mutex ||= Monitor.new
@middleware_mutex.synchronize(&block)
end
def load_middleware(key)
value = registered_middleware[key]
case value
when Module
value
when Symbol, String
middleware_mutex do
@registered_middleware[key] = const_get(value)
end
when Proc
middleware_mutex do
@registered_middleware[key] = value.call
end
end
end
end
end
@@ -0,0 +1,219 @@
# frozen_string_literal: true
module Faraday
# Subclasses Struct with some special helpers for converting from a Hash to
# a Struct.
class Options < Struct
# Public
def self.from(value)
value ? new.update(value) : new
end
# Public
def each
return to_enum(:each) unless block_given?
members.each do |key|
yield(key.to_sym, send(key))
end
end
# Public
def update(obj)
obj.each do |key, value|
sub_options = self.class.options_for(key)
if sub_options
new_value = sub_options.from(value) if value
elsif value.is_a?(Hash)
new_value = value.dup
else
new_value = value
end
send(:"#{key}=", new_value) unless new_value.nil?
end
self
end
# Public
def delete(key)
value = send(key)
send(:"#{key}=", nil)
value
end
# Public
def clear
members.each { |member| delete(member) }
end
# Public
def merge!(other)
other.each do |key, other_value|
self_value = send(key)
sub_options = self.class.options_for(key)
new_value = if self_value && sub_options && other_value
self_value.merge(other_value)
else
other_value
end
send(:"#{key}=", new_value) unless new_value.nil?
end
self
end
# Public
def merge(other)
dup.merge!(other)
end
# Public
def deep_dup
self.class.from(self)
end
# Public
def fetch(key, *args)
unless symbolized_key_set.include?(key.to_sym)
key_setter = "#{key}="
if !args.empty?
send(key_setter, args.first)
elsif block_given?
send(key_setter, yield(key))
else
raise self.class.fetch_error_class, "key not found: #{key.inspect}"
end
end
send(key)
end
# Public
def values_at(*keys)
keys.map { |key| send(key) }
end
# Public
def keys
members.reject { |member| send(member).nil? }
end
# Public
def empty?
keys.empty?
end
# Public
def each_key(&block)
return to_enum(:each_key) unless block
keys.each(&block)
end
# Public
def key?(key)
keys.include?(key)
end
alias has_key? key?
# Public
def each_value(&block)
return to_enum(:each_value) unless block
values.each(&block)
end
# Public
def value?(value)
values.include?(value)
end
alias has_value? value?
# Public
def to_hash
hash = {}
members.each do |key|
value = send(key)
hash[key.to_sym] = value unless value.nil?
end
hash
end
# Internal
def inspect
values = []
members.each do |member|
value = send(member)
values << "#{member}=#{value.inspect}" if value
end
values = values.empty? ? '(empty)' : values.join(', ')
%(#<#{self.class} #{values}>)
end
# Internal
def self.options(mapping)
attribute_options.update(mapping)
end
# Internal
def self.options_for(key)
attribute_options[key]
end
# Internal
def self.attribute_options
@attribute_options ||= {}
end
def self.memoized(key, &block)
unless block
raise ArgumentError, '#memoized must be called with a block'
end
memoized_attributes[key.to_sym] = block
class_eval <<-RUBY, __FILE__, __LINE__ + 1
remove_method(key) if method_defined?(key, false)
def #{key}() self[:#{key}]; end
RUBY
end
def self.memoized_attributes
@memoized_attributes ||= {}
end
def [](key)
key = key.to_sym
if (method = self.class.memoized_attributes[key])
super(key) || (self[key] = instance_eval(&method))
else
super
end
end
def symbolized_key_set
@symbolized_key_set ||= Set.new(keys.map(&:to_sym))
end
def self.inherited(subclass)
super
subclass.attribute_options.update(attribute_options)
subclass.memoized_attributes.update(memoized_attributes)
end
def self.fetch_error_class
@fetch_error_class ||= if Object.const_defined?(:KeyError)
::KeyError
else
::IndexError
end
end
end
end
require 'faraday/options/request_options'
require 'faraday/options/ssl_options'
require 'faraday/options/proxy_options'
require 'faraday/options/connection_options'
require 'faraday/options/env'
@@ -0,0 +1,23 @@
# frozen_string_literal: true
module Faraday
# @!parse
# # ConnectionOptions contains the configurable properties for a Faraday
# # connection object.
# class ConnectionOptions < Options; end
ConnectionOptions = Options.new(:request, :proxy, :ssl, :builder, :url,
:parallel_manager, :params, :headers,
:builder_class) do
options request: RequestOptions, ssl: SSLOptions
memoized(:request) { self.class.options_for(:request).new }
memoized(:ssl) { self.class.options_for(:ssl).new }
memoized(:builder_class) { RackBuilder }
def new_builder(block)
builder_class.new(&block)
end
end
end
@@ -0,0 +1,204 @@
# frozen_string_literal: true
module Faraday
# @!parse
# # @!attribute method
# # @return [Symbol] HTTP method (`:get`, `:post`)
# #
# # @!attribute body
# # @return [String] The request body that will eventually be converted to a
# # string.
# #
# # @!attribute url
# # @return [URI] URI instance for the current request.
# #
# # @!attribute request
# # @return [Hash] options for configuring the request.
# # Options for configuring the request.
# #
# # - `:timeout` - time limit for the entire request (Integer in
# # seconds)
# # - `:open_timeout` - time limit for just the connection phase (e.g.
# # handshake) (Integer in seconds)
# # - `:read_timeout` - time limit for the first response byte received from
# # the server (Integer in seconds)
# # - `:write_timeout` - time limit for the client to send the request to the
# # server (Integer in seconds)
# # - `:on_data` - Proc for streaming
# # - `:proxy` - Hash of proxy options
# # - `:uri` - Proxy server URI
# # - `:user` - Proxy server username
# # - `:password` - Proxy server password
# #
# # @!attribute request_headers
# # @return [Hash] HTTP Headers to be sent to the server.
# #
# # @!attribute ssl
# # @return [Hash] options for configuring SSL requests
# #
# # @!attribute parallel_manager
# # @return [Object] sent if the connection is in parallel mode
# #
# # @!attribute params
# # @return [Hash]
# #
# # @!attribute response
# # @return [Response]
# #
# # @!attribute response_headers
# # @return [Hash] HTTP headers from the server
# #
# # @!attribute status
# # @return [Integer] HTTP response status code
# #
# # @!attribute reason_phrase
# # @return [String]
# class Env < Options; end
Env = Options.new(:method, :request_body, :url, :request,
:request_headers, :ssl, :parallel_manager, :params,
:response, :response_headers, :status,
:reason_phrase, :response_body) do
const_set(:ContentLength, 'Content-Length')
const_set(:StatusesWithoutBody, Set.new([204, 304]))
const_set(:SuccessfulStatuses, (200..299))
# A Set of HTTP verbs that typically send a body. If no body is set for
# these requests, the Content-Length header is set to 0.
const_set(:MethodsWithBodies, Set.new(Faraday::METHODS_WITH_BODY.map(&:to_sym)))
options request: RequestOptions,
request_headers: Utils::Headers, response_headers: Utils::Headers
extend Forwardable
def_delegators :request, :params_encoder
# Build a new Env from given value. Respects and updates `custom_members`.
#
# @param value [Object] a value fitting Option.from(v).
# @return [Env] from given value
def self.from(value)
env = super(value)
if value.respond_to?(:custom_members)
env.custom_members.update(value.custom_members)
end
env
end
# @param key [Object]
def [](key)
return self[current_body] if key == :body
if in_member_set?(key)
super(key)
else
custom_members[key]
end
end
# @param key [Object]
# @param value [Object]
def []=(key, value)
if key == :body
super(current_body, value)
return
end
if in_member_set?(key)
super(key, value)
else
custom_members[key] = value
end
end
def current_body
!!status ? :response_body : :request_body
end
def body
self[:body]
end
def body=(value)
self[:body] = value
end
# @return [Boolean] true if status is in the set of {SuccessfulStatuses}.
def success?
Env::SuccessfulStatuses.include?(status)
end
# @return [Boolean] true if there's no body yet, and the method is in the
# set of {Env::MethodsWithBodies}.
def needs_body?
!body && Env::MethodsWithBodies.include?(method)
end
# Sets content length to zero and the body to the empty string.
def clear_body
request_headers[Env::ContentLength] = '0'
self.body = +''
end
# @return [Boolean] true if the status isn't in the set of
# {Env::StatusesWithoutBody}.
def parse_body?
!Env::StatusesWithoutBody.include?(status)
end
# @return [Boolean] true if there is a parallel_manager
def parallel?
!!parallel_manager
end
def inspect
attrs = [nil]
members.each do |mem|
if (value = send(mem))
attrs << "@#{mem}=#{value.inspect}"
end
end
attrs << "@custom=#{custom_members.inspect}" unless custom_members.empty?
%(#<#{self.class}#{attrs.join(' ')}>)
end
def stream_response?
request.stream_response?
end
def stream_response(&block)
size = 0
yielded = false
block_result = block.call do |chunk| # rubocop:disable Performance/RedundantBlockCall
if chunk.bytesize.positive? || size.positive?
yielded = true
size += chunk.bytesize
request.on_data.call(chunk, size, self)
end
end
request.on_data.call(+'', 0, self) unless yielded
block_result
end
# @private
def custom_members
@custom_members ||= {}
end
# @private
if members.first.is_a?(Symbol)
def in_member_set?(key)
self.class.member_set.include?(key.to_sym)
end
else
def in_member_set?(key)
self.class.member_set.include?(key.to_s)
end
end
# @private
def self.member_set
@member_set ||= Set.new(members)
end
end
end
@@ -0,0 +1,36 @@
# frozen_string_literal: true
module Faraday
# @!parse
# # ProxyOptions contains the configurable properties for the proxy
# # configuration used when making an HTTP request.
# class ProxyOptions < Options; end
ProxyOptions = Options.new(:uri, :user, :password) do
extend Forwardable
def_delegators :uri, :scheme, :scheme=, :host, :host=, :port, :port=,
:path, :path=
def self.from(value)
case value
when ''
value = nil
when String
# URIs without a scheme should default to http (like 'example:123').
# This fixes #1282 and prevents a silent failure in some adapters.
value = "http://#{value}" unless value.include?('://')
value = { uri: Utils.URI(value) }
when URI
value = { uri: value }
when Hash, Options
if (uri = value.delete(:uri))
value[:uri] = Utils.URI(uri)
end
end
super(value)
end
memoized(:user) { uri&.user && Utils.unescape(uri.user) }
memoized(:password) { uri&.password && Utils.unescape(uri.password) }
end
end
@@ -0,0 +1,23 @@
# frozen_string_literal: true
module Faraday
# @!parse
# # RequestOptions contains the configurable properties for a Faraday request.
# class RequestOptions < Options; end
RequestOptions = Options.new(:params_encoder, :proxy, :bind,
:timeout, :open_timeout, :read_timeout,
:write_timeout, :boundary, :oauth,
:context, :on_data) do
def []=(key, value)
if key && key.to_sym == :proxy
super(key, value ? ProxyOptions.from(value) : nil)
else
super(key, value)
end
end
def stream_response?
on_data.is_a?(Proc)
end
end
end
@@ -0,0 +1,70 @@
# frozen_string_literal: true
module Faraday
# @!parse
# # SSL-related options.
# #
# # @!attribute verify
# # @return [Boolean] whether to verify SSL certificates or not
# #
# # @!attribute verify_hostname
# # @return [Boolean] whether to enable hostname verification on server certificates
# # during the handshake or not (see https://github.com/ruby/openssl/pull/60)
# #
# # @!attribute ca_file
# # @return [String] CA file
# #
# # @!attribute ca_path
# # @return [String] CA path
# #
# # @!attribute verify_mode
# # @return [Integer] Any `OpenSSL::SSL::` constant (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL.html)
# #
# # @!attribute cert_store
# # @return [OpenSSL::X509::Store] certificate store
# #
# # @!attribute client_cert
# # @return [String, OpenSSL::X509::Certificate] client certificate
# #
# # @!attribute client_key
# # @return [String, OpenSSL::PKey::RSA, OpenSSL::PKey::DSA] client key
# #
# # @!attribute certificate
# # @return [OpenSSL::X509::Certificate] certificate (Excon only)
# #
# # @!attribute private_key
# # @return [OpenSSL::PKey::RSA, OpenSSL::PKey::DSA] private key (Excon only)
# #
# # @!attribute verify_depth
# # @return [Integer] maximum depth for the certificate chain verification
# #
# # @!attribute version
# # @return [String, Symbol] SSL version (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html#method-i-ssl_version-3D)
# #
# # @!attribute min_version
# # @return [String, Symbol] minimum SSL version (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html#method-i-min_version-3D)
# #
# # @!attribute max_version
# # @return [String, Symbol] maximum SSL version (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html#method-i-max_version-3D)
# class SSLOptions < Options; end
SSLOptions = Options.new(:verify, :verify_hostname,
:ca_file, :ca_path, :verify_mode,
:cert_store, :client_cert, :client_key,
:certificate, :private_key, :verify_depth,
:version, :min_version, :max_version) do
# @return [Boolean] true if should verify
def verify?
verify != false
end
# @return [Boolean] true if should not verify
def disable?
!verify?
end
# @return [Boolean] true if should verify_hostname
def verify_hostname?
verify_hostname != false
end
end
end
@@ -0,0 +1,5 @@
# frozen_string_literal: true
require 'forwardable'
require 'faraday/encoders/nested_params_encoder'
require 'faraday/encoders/flat_params_encoder'
@@ -0,0 +1,251 @@
# frozen_string_literal: true
require 'faraday/adapter_registry'
module Faraday
# A Builder that processes requests into responses by passing through an inner
# middleware stack (heavily inspired by Rack).
#
# @example
# Faraday::Connection.new(url: 'http://httpbingo.org') do |builder|
# builder.request :url_encoded # Faraday::Request::UrlEncoded
# builder.adapter :net_http # Faraday::Adapter::NetHttp
# end
class RackBuilder
# Used to detect missing arguments
NO_ARGUMENT = Object.new
attr_accessor :handlers
# Error raised when trying to modify the stack after calling `lock!`
class StackLocked < RuntimeError; end
# borrowed from ActiveSupport::Dependencies::Reference &
# ActionDispatch::MiddlewareStack::Middleware
class Handler
REGISTRY = Faraday::AdapterRegistry.new
attr_reader :name
ruby2_keywords def initialize(klass, *args, &block)
@name = klass.to_s
REGISTRY.set(klass) if klass.respond_to?(:name)
@args = args
@block = block
end
def klass
REGISTRY.get(@name)
end
def inspect
@name
end
def ==(other)
if other.is_a? Handler
name == other.name
elsif other.respond_to? :name
klass == other
else
@name == other.to_s
end
end
def build(app = nil)
klass.new(app, *@args, &@block)
end
end
def initialize(&block)
@adapter = nil
@handlers = []
build(&block)
end
def initialize_dup(original)
super
@adapter = original.adapter
@handlers = original.handlers.dup
end
def build
raise_if_locked
block_given? ? yield(self) : request(:url_encoded)
adapter(Faraday.default_adapter, **Faraday.default_adapter_options) unless @adapter
end
def [](idx)
@handlers[idx]
end
# Locks the middleware stack to ensure no further modifications are made.
def lock!
@handlers.freeze
end
def locked?
@handlers.frozen?
end
ruby2_keywords def use(klass, *args, &block)
if klass.is_a? Symbol
use_symbol(Faraday::Middleware, klass, *args, &block)
else
raise_if_locked
raise_if_adapter(klass)
@handlers << self.class::Handler.new(klass, *args, &block)
end
end
ruby2_keywords def request(key, *args, &block)
use_symbol(Faraday::Request, key, *args, &block)
end
ruby2_keywords def response(key, *args, &block)
use_symbol(Faraday::Response, key, *args, &block)
end
ruby2_keywords def adapter(klass = NO_ARGUMENT, *args, &block)
return @adapter if klass == NO_ARGUMENT || klass.nil?
klass = Faraday::Adapter.lookup_middleware(klass) if klass.is_a?(Symbol)
@adapter = self.class::Handler.new(klass, *args, &block)
end
## methods to push onto the various positions in the stack:
ruby2_keywords def insert(index, *args, &block)
raise_if_locked
index = assert_index(index)
handler = self.class::Handler.new(*args, &block)
@handlers.insert(index, handler)
end
alias insert_before insert
ruby2_keywords def insert_after(index, *args, &block)
index = assert_index(index)
insert(index + 1, *args, &block)
end
ruby2_keywords def swap(index, *args, &block)
raise_if_locked
index = assert_index(index)
@handlers.delete_at(index)
insert(index, *args, &block)
end
def delete(handler)
raise_if_locked
@handlers.delete(handler)
end
# Processes a Request into a Response by passing it through this Builder's
# middleware stack.
#
# @param connection [Faraday::Connection]
# @param request [Faraday::Request]
#
# @return [Faraday::Response]
def build_response(connection, request)
app.call(build_env(connection, request))
end
# The "rack app" wrapped in middleware. All requests are sent here.
#
# The builder is responsible for creating the app object. After this,
# the builder gets locked to ensure no further modifications are made
# to the middleware stack.
#
# Returns an object that responds to `call` and returns a Response.
def app
@app ||= begin
lock!
ensure_adapter!
to_app
end
end
def to_app
# last added handler is the deepest and thus closest to the inner app
# adapter is always the last one
@handlers.reverse.inject(@adapter.build) do |app, handler|
handler.build(app)
end
end
def ==(other)
other.is_a?(self.class) &&
@handlers == other.handlers &&
@adapter == other.adapter
end
# ENV Keys
# :http_method - a symbolized request HTTP method (:get, :post)
# :body - the request body that will eventually be converted to a string.
# :url - URI instance for the current request.
# :status - HTTP response status code
# :request_headers - hash of HTTP Headers to be sent to the server
# :response_headers - Hash of HTTP headers from the server
# :parallel_manager - sent if the connection is in parallel mode
# :request - Hash of options for configuring the request.
# :timeout - open/read timeout Integer in seconds
# :open_timeout - read timeout Integer in seconds
# :proxy - Hash of proxy options
# :uri - Proxy Server URI
# :user - Proxy server username
# :password - Proxy server password
# :ssl - Hash of options for configuring SSL requests.
def build_env(connection, request)
exclusive_url = connection.build_exclusive_url(
request.path, request.params,
request.options.params_encoder
)
Env.new(request.http_method, request.body, exclusive_url,
request.options, request.headers, connection.ssl,
connection.parallel_manager)
end
private
LOCK_ERR = "can't modify middleware stack after making a request"
MISSING_ADAPTER_ERROR = "An attempt to run a request with a Faraday::Connection without adapter has been made.\n" \
"Please set Faraday.default_adapter or provide one when initializing the connection.\n" \
'For more info, check https://lostisland.github.io/faraday/usage/.'
def raise_if_locked
raise StackLocked, LOCK_ERR if locked?
end
def raise_if_adapter(klass)
return unless is_adapter?(klass)
raise 'Adapter should be set using the `adapter` method, not `use`'
end
def ensure_adapter!
raise MISSING_ADAPTER_ERROR unless @adapter
end
def adapter_set?
!@adapter.nil?
end
def is_adapter?(klass) # rubocop:disable Naming/PredicateName
klass <= Faraday::Adapter
end
ruby2_keywords def use_symbol(mod, key, *args, &block)
use(mod.lookup_middleware(key), *args, &block)
end
def assert_index(index)
idx = index.is_a?(Integer) ? index : @handlers.index(index)
raise "No such handler: #{index.inspect}" unless idx
idx
end
end
end
@@ -0,0 +1,139 @@
# frozen_string_literal: true
module Faraday
# Used to setup URLs, params, headers, and the request body in a sane manner.
#
# @example
# @connection.post do |req|
# req.url 'http://localhost', 'a' => '1' # 'http://localhost?a=1'
# req.headers['b'] = '2' # Header
# req.params['c'] = '3' # GET Param
# req['b'] = '2' # also Header
# req.body = 'abc'
# end
#
# @!attribute http_method
# @return [Symbol] the HTTP method of the Request
# @!attribute path
# @return [URI, String] the path
# @!attribute params
# @return [Hash] query parameters
# @!attribute headers
# @return [Faraday::Utils::Headers] headers
# @!attribute body
# @return [String] body
# @!attribute options
# @return [RequestOptions] options
Request = Struct.new(:http_method, :path, :params, :headers, :body, :options) do
extend MiddlewareRegistry
alias_method :member_get, :[]
private :member_get
alias_method :member_set, :[]=
private :member_set
# @param request_method [String]
# @yield [request] for block customization, if block given
# @yieldparam request [Request]
# @return [Request]
def self.create(request_method)
new(request_method).tap do |request|
yield(request) if block_given?
end
end
remove_method :params=
# Replace params, preserving the existing hash type.
#
# @param hash [Hash] new params
def params=(hash)
if params
params.replace hash
else
member_set(:params, hash)
end
end
remove_method :headers=
# Replace request headers, preserving the existing hash type.
#
# @param hash [Hash] new headers
def headers=(hash)
if headers
headers.replace hash
else
member_set(:headers, hash)
end
end
# Update path and params.
#
# @param path [URI, String]
# @param params [Hash, nil]
# @return [void]
def url(path, params = nil)
if path.respond_to? :query
if (query = path.query)
path = path.dup
path.query = nil
end
else
anchor_index = path.index('#')
path = path.slice(0, anchor_index) unless anchor_index.nil?
path, query = path.split('?', 2)
end
self.path = path
self.params.merge_query query, options.params_encoder
self.params.update(params) if params
end
# @param key [Object] key to look up in headers
# @return [Object] value of the given header name
def [](key)
headers[key]
end
# @param key [Object] key of header to write
# @param value [Object] value of header
def []=(key, value)
headers[key] = value
end
# Marshal serialization support.
#
# @return [Hash] the hash ready to be serialized in Marshal.
def marshal_dump
{
http_method: http_method,
body: body,
headers: headers,
path: path,
params: params,
options: options
}
end
# Marshal serialization support.
# Restores the instance variables according to the +serialised+.
# @param serialised [Hash] the serialised object.
def marshal_load(serialised)
self.http_method = serialised[:http_method]
self.body = serialised[:body]
self.headers = serialised[:headers]
self.path = serialised[:path]
self.params = serialised[:params]
self.options = serialised[:options]
end
# @return [Env] the Env for this Request
def to_env(connection)
Env.new(http_method, body, connection.build_exclusive_url(path, params),
options, headers, connection.ssl, connection.parallel_manager)
end
end
end
require 'faraday/request/authorization'
require 'faraday/request/instrumentation'
require 'faraday/request/json'
require 'faraday/request/url_encoded'
@@ -0,0 +1,54 @@
# frozen_string_literal: true
module Faraday
class Request
# Request middleware for the Authorization HTTP header
class Authorization < Faraday::Middleware
KEY = 'Authorization'
# @param app [#call]
# @param type [String, Symbol] Type of Authorization
# @param params [Array<String, Proc, #call>] parameters to build the Authorization header.
# If the type is `:basic`, then these can be a login and password pair.
# Otherwise, a single value is expected that will be appended after the type.
# This value can be a proc or an object responding to `.call`, in which case
# it will be invoked on each request.
def initialize(app, type, *params)
@type = type
@params = params
super(app)
end
# @param env [Faraday::Env]
def on_request(env)
return if env.request_headers[KEY]
env.request_headers[KEY] = header_from(@type, env, *@params)
end
private
# @param type [String, Symbol]
# @param env [Faraday::Env]
# @param params [Array]
# @return [String] a header value
def header_from(type, env, *params)
if type.to_s.casecmp('basic').zero? && params.size == 2
Utils.basic_header_from(*params)
elsif params.size != 1
raise ArgumentError, "Unexpected params received (got #{params.size} instead of 1)"
else
value = params.first
if (value.is_a?(Proc) && value.arity == 1) || (value.respond_to?(:call) && value.method(:call).arity == 1)
value = value.call(env)
elsif value.is_a?(Proc) || value.respond_to?(:call)
value = value.call
end
"#{type} #{value}"
end
end
end
end
end
Faraday::Request.register_middleware(authorization: Faraday::Request::Authorization)
@@ -0,0 +1,58 @@
# frozen_string_literal: true
module Faraday
class Request
# Middleware for instrumenting Requests.
class Instrumentation < Faraday::Middleware
# Options class used in Request::Instrumentation class.
Options = Faraday::Options.new(:name, :instrumenter) do
remove_method :name
# @return [String]
def name
self[:name] ||= 'request.faraday'
end
remove_method :instrumenter
# @return [Class]
def instrumenter
self[:instrumenter] ||= ActiveSupport::Notifications
end
end
# Instruments requests using Active Support.
#
# Measures time spent only for synchronous requests.
#
# @example Using ActiveSupport::Notifications to measure time spent
# for Faraday requests.
# ActiveSupport::Notifications
# .subscribe('request.faraday') do |name, starts, ends, _, env|
# url = env[:url]
# http_method = env[:method].to_s.upcase
# duration = ends - starts
# $stderr.puts '[%s] %s %s (%.3f s)' %
# [url.host, http_method, url.request_uri, duration]
# end
# @param app [#call]
# @param options [nil, Hash] Options hash
# @option options [String] :name ('request.faraday')
# Name of the instrumenter
# @option options [Class] :instrumenter (ActiveSupport::Notifications)
# Active Support instrumenter class.
def initialize(app, options = nil)
super(app)
@name, @instrumenter = Options.from(options)
.values_at(:name, :instrumenter)
end
# @param env [Faraday::Env]
def call(env)
@instrumenter.instrument(@name, env) do
@app.call(env)
end
end
end
end
end
Faraday::Request.register_middleware(instrumentation: Faraday::Request::Instrumentation)
@@ -0,0 +1,70 @@
# frozen_string_literal: true
require 'json'
module Faraday
class Request
# Request middleware that encodes the body as JSON.
#
# Processes only requests with matching Content-type or those without a type.
# If a request doesn't have a type but has a body, it sets the Content-type
# to JSON MIME-type.
#
# Doesn't try to encode bodies that already are in string form.
class Json < Middleware
MIME_TYPE = 'application/json'
MIME_TYPE_REGEX = %r{^application/(vnd\..+\+)?json$}
def on_request(env)
match_content_type(env) do |data|
env[:body] = encode(data)
end
end
private
def encode(data)
if options[:encoder].is_a?(Array) && options[:encoder].size >= 2
options[:encoder][0].public_send(options[:encoder][1], data)
elsif options[:encoder].respond_to?(:dump)
options[:encoder].dump(data)
else
::JSON.generate(data)
end
end
def match_content_type(env)
return unless process_request?(env)
env[:request_headers][CONTENT_TYPE] ||= MIME_TYPE
yield env[:body] unless env[:body].respond_to?(:to_str)
end
def process_request?(env)
type = request_type(env)
body?(env) && (type.empty? || type.match?(MIME_TYPE_REGEX))
end
def body?(env)
body = env[:body]
case body
when true, false
true
when nil
# NOTE: nil can be converted to `"null"`, but this middleware doesn't process `nil` for the compatibility.
false
else
!(body.respond_to?(:to_str) && body.empty?)
end
end
def request_type(env)
type = env[:request_headers][CONTENT_TYPE].to_s
type = type.split(';', 2).first if type.index(';')
type
end
end
end
end
Faraday::Request.register_middleware(json: Faraday::Request::Json)
@@ -0,0 +1,60 @@
# frozen_string_literal: true
module Faraday
class Request
# Middleware for supporting urlencoded requests.
class UrlEncoded < Faraday::Middleware
unless defined?(::Faraday::Request::UrlEncoded::CONTENT_TYPE)
CONTENT_TYPE = 'Content-Type'
end
class << self
attr_accessor :mime_type
end
self.mime_type = 'application/x-www-form-urlencoded'
# Encodes as "application/x-www-form-urlencoded" if not already encoded or
# of another type.
#
# @param env [Faraday::Env]
def call(env)
match_content_type(env) do |data|
params = Faraday::Utils::ParamsHash[data]
env.body = params.to_query(env.params_encoder)
end
@app.call env
end
# @param env [Faraday::Env]
# @yield [request_body] Body of the request
def match_content_type(env)
return unless process_request?(env)
env.request_headers[CONTENT_TYPE] ||= self.class.mime_type
return if env.body.respond_to?(:to_str) || env.body.respond_to?(:read)
yield(env.body)
end
# @param env [Faraday::Env]
#
# @return [Boolean] True if the request has a body and its Content-Type is
# urlencoded.
def process_request?(env)
type = request_type(env)
env.body && (type.empty? || (type == self.class.mime_type))
end
# @param env [Faraday::Env]
#
# @return [String]
def request_type(env)
type = env.request_headers[CONTENT_TYPE].to_s
type = type.split(';', 2).first if type.index(';')
type
end
end
end
end
Faraday::Request.register_middleware(url_encoded: Faraday::Request::UrlEncoded)
@@ -0,0 +1,91 @@
# frozen_string_literal: true
require 'forwardable'
module Faraday
# Response represents an HTTP response from making an HTTP request.
class Response
extend Forwardable
extend MiddlewareRegistry
def initialize(env = nil)
@env = Env.from(env) if env
@on_complete_callbacks = []
end
attr_reader :env
def status
finished? ? env.status : nil
end
def reason_phrase
finished? ? env.reason_phrase : nil
end
def headers
finished? ? env.response_headers : {}
end
def_delegator :headers, :[]
def body
finished? ? env.body : nil
end
def finished?
!!env
end
def on_complete(&block)
if finished?
yield(env)
else
@on_complete_callbacks << block
end
self
end
def finish(env)
raise 'response already finished' if finished?
@env = env.is_a?(Env) ? env : Env.from(env)
@on_complete_callbacks.each { |callback| callback.call(@env) }
self
end
def success?
finished? && env.success?
end
def to_hash
{
status: env.status, body: env.body,
response_headers: env.response_headers,
url: env.url
}
end
# because @on_complete_callbacks cannot be marshalled
def marshal_dump
finished? ? to_hash : nil
end
def marshal_load(env)
@env = Env.from(env)
end
# Expand the env with more properties, without overriding existing ones.
# Useful for applying request params after restoring a marshalled Response.
def apply_request(request_env)
raise "response didn't finish yet" unless finished?
@env = Env.from(request_env).update(@env)
self
end
end
end
require 'faraday/response/json'
require 'faraday/response/logger'
require 'faraday/response/raise_error'
@@ -0,0 +1,73 @@
# frozen_string_literal: true
require 'json'
module Faraday
class Response
# Parse response bodies as JSON.
class Json < Middleware
def initialize(app = nil, parser_options: nil, content_type: /\bjson$/, preserve_raw: false)
super(app)
@parser_options = parser_options
@content_types = Array(content_type)
@preserve_raw = preserve_raw
process_parser_options
end
def on_complete(env)
process_response(env) if parse_response?(env)
end
private
def process_response(env)
env[:raw_body] = env[:body] if @preserve_raw
env[:body] = parse(env[:body])
rescue StandardError, SyntaxError => e
raise Faraday::ParsingError.new(e, env[:response])
end
def parse(body)
return if body.strip.empty?
decoder, method_name = @decoder_options
decoder.public_send(method_name, body, @parser_options || {})
end
def parse_response?(env)
process_response_type?(env) &&
env[:body].respond_to?(:to_str)
end
def process_response_type?(env)
type = response_type(env)
@content_types.empty? || @content_types.any? do |pattern|
pattern.is_a?(Regexp) ? type.match?(pattern) : type == pattern
end
end
def response_type(env)
type = env[:response_headers][CONTENT_TYPE].to_s
type = type.split(';', 2).first if type.index(';')
type
end
def process_parser_options
@decoder_options = @parser_options&.delete(:decoder)
@decoder_options =
if @decoder_options.is_a?(Array) && @decoder_options.size >= 2
@decoder_options.slice(0, 2)
elsif @decoder_options.respond_to?(:load)
[@decoder_options, :load]
else
[::JSON, :parse]
end
end
end
end
end
Faraday::Response.register_middleware(json: Faraday::Response::Json)
@@ -0,0 +1,37 @@
# frozen_string_literal: true
require 'forwardable'
require 'logger'
require 'faraday/logging/formatter'
module Faraday
class Response
# Logger is a middleware that logs internal events in the HTTP request
# lifecycle to a given Logger object. By default, this logs to STDOUT. See
# Faraday::Logging::Formatter to see specifically what is logged.
class Logger < Middleware
def initialize(app, logger = nil, options = {})
super(app)
logger ||= ::Logger.new($stdout)
formatter_class = options.delete(:formatter) || Logging::Formatter
@formatter = formatter_class.new(logger: logger, options: options)
yield @formatter if block_given?
end
def call(env)
@formatter.request(env)
super
end
def on_complete(env)
@formatter.response(env)
end
def on_error(exc)
@formatter.exception(exc) if @formatter.respond_to?(:exception)
end
end
end
end
Faraday::Response.register_middleware(logger: Faraday::Response::Logger)
@@ -0,0 +1,83 @@
# frozen_string_literal: true
module Faraday
class Response
# RaiseError is a Faraday middleware that raises exceptions on common HTTP
# client or server error responses.
class RaiseError < Middleware
# rubocop:disable Naming/ConstantName
ClientErrorStatuses = (400...500)
ServerErrorStatuses = (500...600)
# rubocop:enable Naming/ConstantName
def on_complete(env)
case env[:status]
when 400
raise Faraday::BadRequestError, response_values(env)
when 401
raise Faraday::UnauthorizedError, response_values(env)
when 403
raise Faraday::ForbiddenError, response_values(env)
when 404
raise Faraday::ResourceNotFound, response_values(env)
when 407
# mimic the behavior that we get with proxy requests with HTTPS
msg = %(407 "Proxy Authentication Required")
raise Faraday::ProxyAuthError.new(msg, response_values(env))
when 408
raise Faraday::RequestTimeoutError, response_values(env)
when 409
raise Faraday::ConflictError, response_values(env)
when 422
raise Faraday::UnprocessableEntityError, response_values(env)
when 429
raise Faraday::TooManyRequestsError, response_values(env)
when ClientErrorStatuses
raise Faraday::ClientError, response_values(env)
when ServerErrorStatuses
raise Faraday::ServerError, response_values(env)
when nil
raise Faraday::NilStatusError, response_values(env)
end
end
# Returns a hash of response data with the following keys:
# - status
# - headers
# - body
# - request
#
# The `request` key is omitted when the middleware is explicitly
# configured with the option `include_request: false`.
def response_values(env)
response = {
status: env.status,
headers: env.response_headers,
body: env.body
}
# Include the request data by default. If the middleware was explicitly
# configured to _not_ include request data, then omit it.
return response unless options.fetch(:include_request, true)
response.merge(
request: {
method: env.method,
url: env.url,
url_path: env.url.path,
params: query_params(env),
headers: env.request_headers,
body: env.request_body
}
)
end
def query_params(env)
env.request.params_encoder ||= Faraday::Utils.default_params_encoder
env.params_encoder.decode(env.url.query)
end
end
end
end
Faraday::Response.register_middleware(raise_error: Faraday::Response::RaiseError)
+121
View File
@@ -0,0 +1,121 @@
# frozen_string_literal: true
require 'uri'
require 'faraday/utils/headers'
require 'faraday/utils/params_hash'
module Faraday
# Utils contains various static helper methods.
module Utils
module_function
def build_query(params)
FlatParamsEncoder.encode(params)
end
def build_nested_query(params)
NestedParamsEncoder.encode(params)
end
def default_space_encoding
@default_space_encoding ||= '+'
end
class << self
attr_writer :default_space_encoding
end
ESCAPE_RE = /[^a-zA-Z0-9 .~_-]/
def escape(str)
str.to_s.gsub(ESCAPE_RE) do |match|
"%#{match.unpack('H2' * match.bytesize).join('%').upcase}"
end.gsub(' ', default_space_encoding)
end
def unescape(str)
CGI.unescape str.to_s
end
DEFAULT_SEP = /[&;] */n
# Adapted from Rack
def parse_query(query)
FlatParamsEncoder.decode(query)
end
def parse_nested_query(query)
NestedParamsEncoder.decode(query)
end
def default_params_encoder
@default_params_encoder ||= NestedParamsEncoder
end
def basic_header_from(login, pass)
value = ["#{login}:#{pass}"].pack('m') # Base64 encoding
value.delete!("\n")
"Basic #{value}"
end
class << self
attr_writer :default_params_encoder
end
# Normalize URI() behavior across Ruby versions
#
# url - A String or URI.
#
# Returns a parsed URI.
def URI(url) # rubocop:disable Naming/MethodName
if url.respond_to?(:host)
url
elsif url.respond_to?(:to_str)
default_uri_parser.call(url)
else
raise ArgumentError, 'bad argument (expected URI object or URI string)'
end
end
def default_uri_parser
@default_uri_parser ||= Kernel.method(:URI)
end
def default_uri_parser=(parser)
@default_uri_parser = if parser.respond_to?(:call) || parser.nil?
parser
else
parser.method(:parse)
end
end
# Receives a String or URI and returns just
# the path with the query string sorted.
def normalize_path(url)
url = URI(url)
(url.path.start_with?('/') ? url.path : "/#{url.path}") +
(url.query ? "?#{sort_query_params(url.query)}" : '')
end
# Recursive hash update
def deep_merge!(target, hash)
hash.each do |key, value|
target[key] = if value.is_a?(Hash) && (target[key].is_a?(Hash) || target[key].is_a?(Options))
deep_merge(target[key], value)
else
value
end
end
target
end
# Recursive hash merge
def deep_merge(source, hash)
deep_merge!(source.dup, hash)
end
def sort_query_params(query)
query.split('&').sort.join('&')
end
end
end
@@ -0,0 +1,144 @@
# frozen_string_literal: true
module Faraday
module Utils
# A case-insensitive Hash that preserves the original case of a header
# when set.
#
# Adapted from Rack::Utils::HeaderHash
class Headers < ::Hash
def self.from(value)
new(value)
end
def self.allocate
new_self = super
new_self.initialize_names
new_self
end
def initialize(hash = nil)
super()
@names = {}
update(hash || {})
end
def initialize_names
@names = {}
end
# on dup/clone, we need to duplicate @names hash
def initialize_copy(other)
super
@names = other.names.dup
end
# need to synchronize concurrent writes to the shared KeyMap
keymap_mutex = Mutex.new
# symbol -> string mapper + cache
KeyMap = Hash.new do |map, key|
value = if key.respond_to?(:to_str)
key
else
key.to_s.split('_') # user_agent: %w(user agent)
.each(&:capitalize!) # => %w(User Agent)
.join('-') # => "User-Agent"
end
keymap_mutex.synchronize { map[key] = value }
end
KeyMap[:etag] = 'ETag'
def [](key)
key = KeyMap[key]
super(key) || super(@names[key.downcase])
end
def []=(key, val)
key = KeyMap[key]
key = (@names[key.downcase] ||= key)
# join multiple values with a comma
val = val.to_ary.join(', ') if val.respond_to?(:to_ary)
super(key, val)
end
def fetch(key, *args, &block)
key = KeyMap[key]
key = @names.fetch(key.downcase, key)
super(key, *args, &block)
end
def delete(key)
key = KeyMap[key]
key = @names[key.downcase]
return unless key
@names.delete key.downcase
super(key)
end
def include?(key)
@names.include? key.downcase
end
alias has_key? include?
alias member? include?
alias key? include?
def merge!(other)
other.each { |k, v| self[k] = v }
self
end
alias update merge!
def merge(other)
hash = dup
hash.merge! other
end
def replace(other)
clear
@names.clear
update other
self
end
def to_hash
{}.update(self)
end
def parse(header_string)
return unless header_string && !header_string.empty?
headers = header_string.split("\r\n")
# Find the last set of response headers.
start_index = headers.rindex { |x| x.start_with?('HTTP/') } || 0
last_response = headers.slice(start_index, headers.size)
last_response
.tap { |a| a.shift if a.first.start_with?('HTTP/') }
.map { |h| h.split(/:\s*/, 2) } # split key and value
.reject { |p| p[0].nil? } # ignore blank lines
.each { |key, value| add_parsed(key, value) }
end
protected
attr_reader :names
private
# Join multiple values with a comma.
def add_parsed(key, value)
if key?(key)
self[key] = self[key].to_s
self[key] << ', ' << value
else
self[key] = value
end
end
end
end
end
@@ -0,0 +1,61 @@
# frozen_string_literal: true
module Faraday
module Utils
# A hash with stringified keys.
class ParamsHash < Hash
def [](key)
super(convert_key(key))
end
def []=(key, value)
super(convert_key(key), value)
end
def delete(key)
super(convert_key(key))
end
def include?(key)
super(convert_key(key))
end
alias has_key? include?
alias member? include?
alias key? include?
def update(params)
params.each do |key, value|
self[key] = value
end
self
end
alias merge! update
def merge(params)
dup.update(params)
end
def replace(other)
clear
update(other)
end
def merge_query(query, encoder = nil)
return self unless query && !query.empty?
update((encoder || Utils.default_params_encoder).decode(query))
end
def to_query(encoder = nil)
(encoder || Utils.default_params_encoder).encode(self)
end
private
def convert_key(key)
key.to_s
end
end
end
end
@@ -0,0 +1,5 @@
# frozen_string_literal: true
module Faraday
VERSION = '2.9.0'
end
@@ -0,0 +1,14 @@
# frozen_string_literal: true
require 'webmock/rspec'
WebMock.disable_net_connect!(allow_localhost: true)
require_relative '../support/helper_methods'
require_relative '../support/disabling_stub'
require_relative '../support/streaming_response_checker'
require_relative '../support/shared_examples/adapter'
require_relative '../support/shared_examples/request_method'
RSpec.configure do |config|
config.include Faraday::HelperMethods
end
@@ -0,0 +1,442 @@
# frozen_string_literal: true
RSpec.describe Faraday::Adapter::Test do
let(:stubs) do
described_class::Stubs.new do |stub|
stub.get('http://domain.test/hello') do
[200, { 'Content-Type' => 'text/html' }, 'domain: hello']
end
stub.get('http://wrong.test/hello') do
[200, { 'Content-Type' => 'text/html' }, 'wrong: hello']
end
stub.get('http://wrong.test/bait') do
[404, { 'Content-Type' => 'text/html' }]
end
stub.get('/hello') do
[200, { 'Content-Type' => 'text/html' }, 'hello']
end
stub.get('/method-echo') do |env|
[200, { 'Content-Type' => 'text/html' }, env[:method].to_s]
end
stub.get(%r{\A/resources/\d+(?:\?|\z)}) do
[200, { 'Content-Type' => 'text/html' }, 'show']
end
stub.get(%r{\A/resources/(specified)\z}) do |_env, meta|
[200, { 'Content-Type' => 'text/html' }, "show #{meta[:match_data][1]}"]
end
end
end
let(:connection) do
Faraday.new do |builder|
builder.adapter :test, stubs
end
end
let(:response) { connection.get('/hello') }
context 'with simple path sets status' do
subject { response.status }
it { is_expected.to eq 200 }
end
context 'with simple path sets headers' do
subject { response.headers['Content-Type'] }
it { is_expected.to eq 'text/html' }
end
context 'with simple path sets body' do
subject { response.body }
it { is_expected.to eq 'hello' }
end
context 'with host points to the right stub' do
subject { connection.get('http://domain.test/hello').body }
it { is_expected.to eq 'domain: hello' }
end
describe 'can be called several times' do
subject { connection.get('/hello').body }
it { is_expected.to eq 'hello' }
end
describe 'can handle regular expression path' do
subject { connection.get('/resources/1').body }
it { is_expected.to eq 'show' }
end
describe 'can handle single parameter block' do
subject { connection.get('/method-echo').body }
it { is_expected.to eq 'get' }
end
describe 'can handle regular expression path with captured result' do
subject { connection.get('/resources/specified').body }
it { is_expected.to eq 'show specified' }
end
context 'with get params' do
subject { connection.get('/param?a=1').body }
before do
stubs.get('/param?a=1') { [200, {}, 'a'] }
end
it { is_expected.to eq 'a' }
end
describe 'ignoring unspecified get params' do
before do
stubs.get('/optional?a=1') { [200, {}, 'a'] }
end
context 'with multiple params' do
subject { connection.get('/optional?a=1&b=1').body }
it { is_expected.to eq 'a' }
end
context 'with single param' do
subject { connection.get('/optional?a=1').body }
it { is_expected.to eq 'a' }
end
context 'without params' do
subject(:request) { connection.get('/optional') }
it do
expect { request }.to raise_error(
Faraday::Adapter::Test::Stubs::NotFound
)
end
end
end
context 'with http headers' do
before do
stubs.get('/yo', 'X-HELLO' => 'hello') { [200, {}, 'a'] }
stubs.get('/yo') { [200, {}, 'b'] }
end
context 'with header' do
subject do
connection.get('/yo') { |env| env.headers['X-HELLO'] = 'hello' }.body
end
it { is_expected.to eq 'a' }
end
context 'without header' do
subject do
connection.get('/yo').body
end
it { is_expected.to eq 'b' }
end
end
describe 'different outcomes for the same request' do
def make_request
connection.get('/foo')
end
subject(:request) { make_request.body }
before do
stubs.get('/foo') { [200, { 'Content-Type' => 'text/html' }, 'hello'] }
stubs.get('/foo') { [200, { 'Content-Type' => 'text/html' }, 'world'] }
end
context 'the first request' do
it { is_expected.to eq 'hello' }
end
context 'the second request' do
before do
make_request
end
it { is_expected.to eq 'world' }
end
end
describe 'yielding env to stubs' do
subject { connection.get('http://foo.com/foo?a=1').body }
before do
stubs.get '/foo' do |env|
expect(env[:url].path).to eq '/foo'
expect(env[:url].host).to eq 'foo.com'
expect(env[:params]['a']).to eq '1'
expect(env[:request_headers]['Accept']).to eq 'text/plain'
[200, {}, 'a']
end
connection.headers['Accept'] = 'text/plain'
end
it { is_expected.to eq 'a' }
end
describe 'params parsing' do
subject { connection.get('http://foo.com/foo?a[b]=1').body }
context 'with default encoder' do
before do
stubs.get '/foo' do |env|
expect(env[:params]['a']['b']).to eq '1'
[200, {}, 'a']
end
end
it { is_expected.to eq 'a' }
end
context 'with nested encoder' do
before do
stubs.get '/foo' do |env|
expect(env[:params]['a']['b']).to eq '1'
[200, {}, 'a']
end
connection.options.params_encoder = Faraday::NestedParamsEncoder
end
it { is_expected.to eq 'a' }
end
context 'with flat encoder' do
before do
stubs.get '/foo' do |env|
expect(env[:params]['a[b]']).to eq '1'
[200, {}, 'a']
end
connection.options.params_encoder = Faraday::FlatParamsEncoder
end
it { is_expected.to eq 'a' }
end
end
describe 'raising an error if no stub was found' do
describe 'for request' do
subject(:request) { connection.get('/invalid') { [200, {}, []] } }
it { expect { request }.to raise_error described_class::Stubs::NotFound }
end
describe 'for specified host' do
subject(:request) { connection.get('http://domain.test/bait') }
it { expect { request }.to raise_error described_class::Stubs::NotFound }
end
describe 'for request without specified header' do
subject(:request) { connection.get('/yo') }
before do
stubs.get('/yo', 'X-HELLO' => 'hello') { [200, {}, 'a'] }
end
it { expect { request }.to raise_error described_class::Stubs::NotFound }
end
end
describe 'for request with non default params encoder' do
let(:connection) do
Faraday.new(request: { params_encoder: Faraday::FlatParamsEncoder }) do |builder|
builder.adapter :test, stubs
end
end
let(:stubs) do
described_class::Stubs.new do |stubs|
stubs.get('/path?a=x&a=y&a=z') { [200, {}, 'a'] }
end
end
context 'when all flat param values are correctly set' do
subject(:request) { connection.get('/path?a=x&a=y&a=z') }
it { expect(request.status).to eq 200 }
end
shared_examples 'raise NotFound when params do not satisfy the flat param values' do |params|
subject(:request) { connection.get('/path', params) }
context "with #{params.inspect}" do
it { expect { request }.to raise_error described_class::Stubs::NotFound }
end
end
it_behaves_like 'raise NotFound when params do not satisfy the flat param values', { a: %w[x] }
it_behaves_like 'raise NotFound when params do not satisfy the flat param values', { a: %w[x y] }
it_behaves_like 'raise NotFound when params do not satisfy the flat param values', { a: %w[x z y] } # NOTE: The order of the value is also compared.
it_behaves_like 'raise NotFound when params do not satisfy the flat param values', { b: %w[x y z] }
end
describe 'strict_mode' do
let(:stubs) do
described_class::Stubs.new(strict_mode: true) do |stubs|
stubs.get('/strict?a=12&b=xy', 'Authorization' => 'Bearer m_ck', 'X-C' => 'hello') { [200, {}, 'a'] }
stubs.get('/with_user_agent?a=12&b=xy', authorization: 'Bearer m_ck', 'User-Agent' => 'My Agent') { [200, {}, 'a'] }
end
end
context 'when params and headers are exactly set' do
subject(:request) { connection.get('/strict', { a: '12', b: 'xy' }, { authorization: 'Bearer m_ck', x_c: 'hello' }) }
it { expect(request.status).to eq 200 }
end
context 'when params and headers are exactly set with a custom user agent' do
subject(:request) { connection.get('/with_user_agent', { a: '12', b: 'xy' }, { authorization: 'Bearer m_ck', 'User-Agent' => 'My Agent' }) }
it { expect(request.status).to eq 200 }
end
shared_examples 'raise NotFound when params do not satisfy the strict check' do |params|
subject(:request) { connection.get('/strict', params, { 'Authorization' => 'Bearer m_ck', 'X-C' => 'hello' }) }
context "with #{params.inspect}" do
it { expect { request }.to raise_error described_class::Stubs::NotFound }
end
end
it_behaves_like 'raise NotFound when params do not satisfy the strict check', { a: '12' }
it_behaves_like 'raise NotFound when params do not satisfy the strict check', { b: 'xy' }
it_behaves_like 'raise NotFound when params do not satisfy the strict check', { a: '123', b: 'xy' }
it_behaves_like 'raise NotFound when params do not satisfy the strict check', { a: '12', b: 'xyz' }
it_behaves_like 'raise NotFound when params do not satisfy the strict check', { a: '12', b: 'xy', c: 'hello' }
it_behaves_like 'raise NotFound when params do not satisfy the strict check', { additional: 'special', a: '12', b: 'xy', c: 'hello' }
shared_examples 'raise NotFound when headers do not satisfy the strict check' do |path, headers|
subject(:request) { connection.get(path, { a: 12, b: 'xy' }, headers) }
context "with #{headers.inspect}" do
it { expect { request }.to raise_error described_class::Stubs::NotFound }
end
end
it_behaves_like 'raise NotFound when headers do not satisfy the strict check', '/strict', { authorization: 'Bearer m_ck' }
it_behaves_like 'raise NotFound when headers do not satisfy the strict check', '/strict', { 'X-C' => 'hello' }
it_behaves_like 'raise NotFound when headers do not satisfy the strict check', '/strict', { authorization: 'Bearer m_ck', 'x-c': 'Hi' }
it_behaves_like 'raise NotFound when headers do not satisfy the strict check', '/strict', { authorization: 'Basic m_ck', 'x-c': 'hello' }
it_behaves_like 'raise NotFound when headers do not satisfy the strict check', '/strict', { authorization: 'Bearer m_ck', 'x-c': 'hello', x_special: 'special' }
it_behaves_like 'raise NotFound when headers do not satisfy the strict check', '/with_user_agent', { authorization: 'Bearer m_ck' }
it_behaves_like 'raise NotFound when headers do not satisfy the strict check', '/with_user_agent', { authorization: 'Bearer m_ck', user_agent: 'Unknown' }
it_behaves_like 'raise NotFound when headers do not satisfy the strict check', '/with_user_agent', { authorization: 'Bearer m_ck', user_agent: 'My Agent', x_special: 'special' }
context 'when strict_mode is disabled' do
before do
stubs.strict_mode = false
end
shared_examples 'does not raise NotFound even when params do not satisfy the strict check' do |params|
subject(:request) { connection.get('/strict', params, { 'Authorization' => 'Bearer m_ck', 'X-C' => 'hello' }) }
context "with #{params.inspect}" do
it { expect(request.status).to eq 200 }
end
end
it_behaves_like 'does not raise NotFound even when params do not satisfy the strict check', { a: '12', b: 'xy' }
it_behaves_like 'does not raise NotFound even when params do not satisfy the strict check', { a: '12', b: 'xy', c: 'hello' }
it_behaves_like 'does not raise NotFound even when params do not satisfy the strict check', { additional: 'special', a: '12', b: 'xy', c: 'hello' }
shared_examples 'does not raise NotFound even when headers do not satisfy the strict check' do |path, headers|
subject(:request) { connection.get(path, { a: 12, b: 'xy' }, headers) }
context "with #{headers.inspect}" do
it { expect(request.status).to eq 200 }
end
end
it_behaves_like 'does not raise NotFound even when headers do not satisfy the strict check', '/strict', { authorization: 'Bearer m_ck', 'x-c': 'hello' }
it_behaves_like 'does not raise NotFound even when headers do not satisfy the strict check', '/strict', { authorization: 'Bearer m_ck', 'x-c': 'hello', x_special: 'special' }
it_behaves_like 'does not raise NotFound even when headers do not satisfy the strict check', '/strict', { authorization: 'Bearer m_ck', 'x-c': 'hello', user_agent: 'Special Agent' }
it_behaves_like 'does not raise NotFound even when headers do not satisfy the strict check', '/with_user_agent', { authorization: 'Bearer m_ck', user_agent: 'My Agent' }
it_behaves_like 'does not raise NotFound even when headers do not satisfy the strict check', '/with_user_agent', { authorization: 'Bearer m_ck', user_agent: 'My Agent', x_special: 'special' }
end
describe 'body_match?' do
let(:stubs) do
described_class::Stubs.new do |stubs|
stubs.post('/no_check') { [200, {}, 'ok'] }
stubs.post('/with_string', 'abc') { [200, {}, 'ok'] }
stubs.post(
'/with_proc',
->(request_body) { JSON.parse(request_body, symbolize_names: true) == { x: '!', a: [{ m: [{ a: true }], n: 123 }] } },
{ content_type: 'application/json' }
) do
[200, {}, 'ok']
end
end
end
context 'when trying without any args for body' do
subject(:without_body) { connection.post('/no_check') }
it { expect(without_body.status).to eq 200 }
end
context 'when trying with string body stubs' do
subject(:with_string) { connection.post('/with_string', 'abc') }
it { expect(with_string.status).to eq 200 }
end
context 'when trying with proc body stubs' do
subject(:with_proc) do
connection.post('/with_proc', JSON.dump(a: [{ n: 123, m: [{ a: true }] }], x: '!'), { 'Content-Type' => 'application/json' })
end
it { expect(with_proc.status).to eq 200 }
end
end
end
describe 'request timeout' do
subject(:request) do
connection.get('/sleep') do |req|
req.options.timeout = timeout
end
end
before do
stubs.get('/sleep') do
sleep(0.01)
[200, {}, '']
end
end
context 'when request is within timeout' do
let(:timeout) { 1 }
it { expect(request.status).to eq 200 }
end
context 'when request is too slow' do
let(:timeout) { 0.001 }
it 'raises an exception' do
expect { request }.to raise_error(Faraday::TimeoutError)
end
end
end
end
@@ -0,0 +1,28 @@
# frozen_string_literal: true
RSpec.describe Faraday::AdapterRegistry do
describe '#initialize' do
subject(:registry) { described_class.new }
it { expect { registry.get(:FinFangFoom) }.to raise_error(NameError) }
it { expect { registry.get('FinFangFoom') }.to raise_error(NameError) }
it 'looks up class by string name' do
expect(registry.get('Faraday::Connection')).to eq(Faraday::Connection)
end
it 'looks up class by symbol name' do
expect(registry.get(:Faraday)).to eq(Faraday)
end
it 'caches lookups with implicit name' do
registry.set :symbol
expect(registry.get('symbol')).to eq(:symbol)
end
it 'caches lookups with explicit name' do
registry.set 'string', :name
expect(registry.get(:name)).to eq('string')
end
end
end
@@ -0,0 +1,55 @@
# frozen_string_literal: true
RSpec.describe Faraday::Adapter do
let(:adapter) { Faraday::Adapter.new }
let(:request) { {} }
context '#request_timeout' do
it 'gets :read timeout' do
expect(timeout(:read)).to eq(nil)
request[:timeout] = 5
request[:write_timeout] = 1
expect(timeout(:read)).to eq(5)
request[:read_timeout] = 2
expect(timeout(:read)).to eq(2)
end
it 'gets :open timeout' do
expect(timeout(:open)).to eq(nil)
request[:timeout] = 5
request[:write_timeout] = 1
expect(timeout(:open)).to eq(5)
request[:open_timeout] = 2
expect(timeout(:open)).to eq(2)
end
it 'gets :write timeout' do
expect(timeout(:write)).to eq(nil)
request[:timeout] = 5
request[:read_timeout] = 1
expect(timeout(:write)).to eq(5)
request[:write_timeout] = 2
expect(timeout(:write)).to eq(2)
end
it 'attempts unknown timeout type' do
expect { timeout(:unknown) }.to raise_error(ArgumentError)
end
def timeout(type)
adapter.send(:request_timeout, type, request)
end
end
end
@@ -0,0 +1,808 @@
# frozen_string_literal: true
class CustomEncoder
def encode(params)
params.map { |k, v| "#{k.upcase}-#{v.to_s.upcase}" }.join(',')
end
def decode(params)
params.split(',').to_h { |pair| pair.split('-') }
end
end
shared_examples 'initializer with url' do
context 'with simple url' do
let(:address) { 'http://httpbingo.org' }
it { expect(subject.host).to eq('httpbingo.org') }
it { expect(subject.port).to eq(80) }
it { expect(subject.scheme).to eq('http') }
it { expect(subject.path_prefix).to eq('/') }
it { expect(subject.params).to eq({}) }
end
context 'with complex url' do
let(:address) { 'http://httpbingo.org:815/fish?a=1' }
it { expect(subject.port).to eq(815) }
it { expect(subject.path_prefix).to eq('/fish') }
it { expect(subject.params).to eq('a' => '1') }
end
context 'with IPv6 address' do
let(:address) { 'http://[::1]:85/' }
it { expect(subject.host).to eq('[::1]') }
it { expect(subject.port).to eq(85) }
end
end
shared_examples 'default connection options' do
after { Faraday.default_connection_options = nil }
it 'works with implicit url' do
conn = Faraday.new 'http://httpbingo.org/foo'
expect(conn.options.timeout).to eq(10)
end
it 'works with option url' do
conn = Faraday.new url: 'http://httpbingo.org/foo'
expect(conn.options.timeout).to eq(10)
end
it 'works with instance connection options' do
conn = Faraday.new 'http://httpbingo.org/foo', request: { open_timeout: 1 }
expect(conn.options.timeout).to eq(10)
expect(conn.options.open_timeout).to eq(1)
end
it 'default connection options persist with an instance overriding' do
conn = Faraday.new 'http://nigiri.com/bar'
conn.options.timeout = 1
expect(Faraday.default_connection_options.request.timeout).to eq(10)
other = Faraday.new url: 'https://httpbingo.org/foo'
other.options.timeout = 1
expect(Faraday.default_connection_options.request.timeout).to eq(10)
end
it 'default connection uses default connection options' do
expect(Faraday.default_connection.options.timeout).to eq(10)
end
end
RSpec.describe Faraday::Connection do
let(:conn) { Faraday::Connection.new(url, options) }
let(:url) { nil }
let(:options) { nil }
describe '.new' do
subject { conn }
context 'with implicit url param' do
# Faraday::Connection.new('http://httpbingo.org')
let(:url) { address }
it_behaves_like 'initializer with url'
end
context 'with explicit url param' do
# Faraday::Connection.new(url: 'http://httpbingo.org')
let(:url) { { url: address } }
it_behaves_like 'initializer with url'
end
context 'with custom builder' do
let(:custom_builder) { Faraday::RackBuilder.new }
let(:options) { { builder: custom_builder } }
it { expect(subject.builder).to eq(custom_builder) }
end
context 'with custom params' do
let(:options) { { params: { a: 1 } } }
it { expect(subject.params).to eq('a' => 1) }
end
context 'with custom params and params in url' do
let(:url) { 'http://httpbingo.org/fish?a=1&b=2' }
let(:options) { { params: { a: 3 } } }
it { expect(subject.params).to eq('a' => 3, 'b' => '2') }
end
context 'with basic_auth in url' do
let(:url) { 'http://Aladdin:open%20sesame@httpbingo.org/fish' }
it { expect(subject.headers['Authorization']).to eq('Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==') }
end
context 'with custom headers' do
let(:options) { { headers: { user_agent: 'Faraday' } } }
it { expect(subject.headers['User-agent']).to eq('Faraday') }
end
context 'with ssl false' do
let(:options) { { ssl: { verify: false } } }
it { expect(subject.ssl.verify?).to be_falsey }
end
context 'with verify_hostname false' do
let(:options) { { ssl: { verify_hostname: false } } }
it { expect(subject.ssl.verify_hostname?).to be_falsey }
end
context 'with empty block' do
let(:conn) { Faraday::Connection.new {} }
it { expect(conn.builder.handlers.size).to eq(0) }
end
context 'with block' do
let(:conn) do
Faraday::Connection.new(params: { 'a' => '1' }) do |faraday|
faraday.adapter :test
faraday.url_prefix = 'http://httpbingo.org/omnom'
end
end
it { expect(conn.builder.handlers.size).to eq(0) }
it { expect(conn.path_prefix).to eq('/omnom') }
end
end
describe '#close' do
before { Faraday.default_adapter = :test }
after { Faraday.default_adapter = nil }
it 'can close underlying app' do
expect(conn.app).to receive(:close)
conn.close
end
end
describe '#build_exclusive_url' do
context 'with relative path' do
subject { conn.build_exclusive_url('sake.html') }
it 'uses connection host as default host' do
conn.host = 'httpbingo.org'
expect(subject.host).to eq('httpbingo.org')
expect(subject.scheme).to eq('http')
end
it do
conn.path_prefix = '/fish'
expect(subject.path).to eq('/fish/sake.html')
end
it do
conn.path_prefix = '/'
expect(subject.path).to eq('/sake.html')
end
it do
conn.path_prefix = 'fish'
expect(subject.path).to eq('/fish/sake.html')
end
it do
conn.path_prefix = '/fish/'
expect(subject.path).to eq('/fish/sake.html')
end
end
context 'with absolute path' do
subject { conn.build_exclusive_url('/sake.html') }
after { expect(subject.path).to eq('/sake.html') }
it { conn.path_prefix = '/fish' }
it { conn.path_prefix = '/' }
it { conn.path_prefix = 'fish' }
it { conn.path_prefix = '/fish/' }
end
context 'with complete url' do
subject { conn.build_exclusive_url('http://httpbingo.org/sake.html?a=1') }
it { expect(subject.scheme).to eq('http') }
it { expect(subject.host).to eq('httpbingo.org') }
it { expect(subject.port).to eq(80) }
it { expect(subject.path).to eq('/sake.html') }
it { expect(subject.query).to eq('a=1') }
end
it 'overrides connection port for absolute url' do
conn.port = 23
uri = conn.build_exclusive_url('http://httpbingo.org')
expect(uri.port).to eq(80)
end
it 'does not add ending slash given nil url' do
conn.url_prefix = 'http://httpbingo.org/nigiri'
uri = conn.build_exclusive_url
expect(uri.path).to eq('/nigiri')
end
it 'does not add ending slash given empty url' do
conn.url_prefix = 'http://httpbingo.org/nigiri'
uri = conn.build_exclusive_url('')
expect(uri.path).to eq('/nigiri')
end
it 'does not use connection params' do
conn.url_prefix = 'http://httpbingo.org/nigiri'
conn.params = { a: 1 }
expect(conn.build_exclusive_url.to_s).to eq('http://httpbingo.org/nigiri')
end
it 'allows to provide params argument' do
conn.url_prefix = 'http://httpbingo.org/nigiri'
conn.params = { a: 1 }
params = Faraday::Utils::ParamsHash.new
params[:a] = 2
uri = conn.build_exclusive_url(nil, params)
expect(uri.to_s).to eq('http://httpbingo.org/nigiri?a=2')
end
it 'handles uri instances' do
uri = conn.build_exclusive_url(URI('/sake.html'))
expect(uri.path).to eq('/sake.html')
end
it 'always returns new URI instance' do
conn.url_prefix = 'http://httpbingo.org'
uri1 = conn.build_exclusive_url(nil)
uri2 = conn.build_exclusive_url(nil)
expect(uri1).not_to equal(uri2)
end
context 'with url_prefixed connection' do
let(:url) { 'http://httpbingo.org/get/' }
it 'parses url and changes scheme' do
conn.scheme = 'https'
uri = conn.build_exclusive_url('sake.html')
expect(uri.to_s).to eq('https://httpbingo.org/get/sake.html')
end
it 'joins url to base with ending slash' do
uri = conn.build_exclusive_url('sake.html')
expect(uri.to_s).to eq('http://httpbingo.org/get/sake.html')
end
it 'used default base with ending slash' do
uri = conn.build_exclusive_url
expect(uri.to_s).to eq('http://httpbingo.org/get/')
end
it 'overrides base' do
uri = conn.build_exclusive_url('/sake/')
expect(uri.to_s).to eq('http://httpbingo.org/sake/')
end
end
context 'with colon in path' do
let(:url) { 'http://service.com' }
it 'joins url to base when used absolute path' do
conn = Faraday.new(url: url)
uri = conn.build_exclusive_url('/service:search?limit=400')
expect(uri.to_s).to eq('http://service.com/service:search?limit=400')
end
it 'joins url to base when used relative path' do
conn = Faraday.new(url: url)
uri = conn.build_exclusive_url('service:search?limit=400')
expect(uri.to_s).to eq('http://service.com/service%3Asearch?limit=400')
end
it 'joins url to base when used with path prefix' do
conn = Faraday.new(url: url)
conn.path_prefix = '/api'
uri = conn.build_exclusive_url('service:search?limit=400')
expect(uri.to_s).to eq('http://service.com/api/service%3Asearch?limit=400')
end
end
context 'with a custom `default_uri_parser`' do
let(:url) { 'http://httpbingo.org' }
let(:parser) { Addressable::URI }
around do |example|
with_default_uri_parser(parser) do
example.run
end
end
it 'does not raise error' do
expect { conn.build_exclusive_url('/nigiri') }.not_to raise_error
end
end
end
describe '#build_url' do
let(:url) { 'http://httpbingo.org/nigiri' }
it 'uses params' do
conn.params = { a: 1, b: 1 }
expect(conn.build_url.to_s).to eq('http://httpbingo.org/nigiri?a=1&b=1')
end
it 'merges params' do
conn.params = { a: 1, b: 1 }
url = conn.build_url(nil, b: 2, c: 3)
expect(url.to_s).to eq('http://httpbingo.org/nigiri?a=1&b=2&c=3')
end
end
describe '#build_request' do
let(:url) { 'https://ahttpbingo.org/sake.html' }
let(:request) { conn.build_request(:get) }
before do
conn.headers = { 'Authorization' => 'token abc123' }
request.headers.delete('Authorization')
end
it { expect(conn.headers.keys).to eq(['Authorization']) }
it { expect(conn.headers.include?('Authorization')).to be_truthy }
it { expect(request.headers.keys).to be_empty }
it { expect(request.headers.include?('Authorization')).to be_falsey }
end
describe '#to_env' do
subject { conn.build_request(:get).to_env(conn).url }
let(:url) { 'http://httpbingo.org/sake.html' }
let(:options) { { params: @params } }
it 'parses url params into query' do
@params = { 'a[b]' => '1 + 2' }
expect(subject.query).to eq('a%5Bb%5D=1+%2B+2')
end
it 'escapes per spec' do
@params = { 'a' => '1+2 foo~bar.-baz' }
expect(subject.query).to eq('a=1%2B2+foo~bar.-baz')
end
it 'bracketizes nested params in query' do
@params = { 'a' => { 'b' => 'c' } }
expect(subject.query).to eq('a%5Bb%5D=c')
end
it 'bracketizes repeated params in query' do
@params = { 'a' => [1, 2] }
expect(subject.query).to eq('a%5B%5D=1&a%5B%5D=2')
end
it 'without braketizing repeated params in query' do
@params = { 'a' => [1, 2] }
conn.options.params_encoder = Faraday::FlatParamsEncoder
expect(subject.query).to eq('a=1&a=2')
end
end
describe 'proxy support' do
it 'accepts string' do
with_env 'http_proxy' => 'http://env-proxy.com:80' do
conn.proxy = 'http://proxy.com'
expect(conn.proxy.host).to eq('proxy.com')
end
end
it 'accepts uri' do
with_env 'http_proxy' => 'http://env-proxy.com:80' do
conn.proxy = URI.parse('http://proxy.com')
expect(conn.proxy.host).to eq('proxy.com')
end
end
it 'accepts hash with string uri' do
with_env 'http_proxy' => 'http://env-proxy.com:80' do
conn.proxy = { uri: 'http://proxy.com', user: 'rick' }
expect(conn.proxy.host).to eq('proxy.com')
expect(conn.proxy.user).to eq('rick')
end
end
it 'accepts hash' do
with_env 'http_proxy' => 'http://env-proxy.com:80' do
conn.proxy = { uri: URI.parse('http://proxy.com'), user: 'rick' }
expect(conn.proxy.host).to eq('proxy.com')
expect(conn.proxy.user).to eq('rick')
end
end
it 'accepts http env' do
with_env 'http_proxy' => 'http://env-proxy.com:80' do
expect(conn.proxy.host).to eq('env-proxy.com')
end
end
it 'accepts http env with auth' do
with_env 'http_proxy' => 'http://a%40b:my%20pass@proxy.com:80' do
expect(conn.proxy.user).to eq('a@b')
expect(conn.proxy.password).to eq('my pass')
end
end
it 'accepts env without scheme' do
with_env 'http_proxy' => 'localhost:8888' do
uri = conn.proxy[:uri]
expect(uri.host).to eq('localhost')
expect(uri.port).to eq(8888)
end
end
it 'fetches no proxy from nil env' do
with_env 'http_proxy' => nil do
expect(conn.proxy).to be_nil
end
end
it 'fetches no proxy from blank env' do
with_env 'http_proxy' => '' do
expect(conn.proxy).to be_nil
end
end
it 'does not accept uppercase env' do
with_env 'HTTP_PROXY' => 'http://localhost:8888/' do
expect(conn.proxy).to be_nil
end
end
it 'allows when url in no proxy list' do
with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do
conn = Faraday::Connection.new('http://example.com')
expect(conn.proxy).to be_nil
end
end
it 'allows when url in no proxy list with url_prefix' do
with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do
conn = Faraday::Connection.new
conn.url_prefix = 'http://example.com'
expect(conn.proxy).to be_nil
end
end
it 'allows when prefixed url is not in no proxy list' do
with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do
conn = Faraday::Connection.new('http://prefixedexample.com')
expect(conn.proxy.host).to eq('proxy.com')
end
end
it 'allows when subdomain url is in no proxy list' do
with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do
conn = Faraday::Connection.new('http://subdomain.example.com')
expect(conn.proxy).to be_nil
end
end
it 'allows when url not in no proxy list' do
with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example2.com' do
conn = Faraday::Connection.new('http://example.com')
expect(conn.proxy.host).to eq('proxy.com')
end
end
it 'allows when ip address is not in no proxy list but url is' do
with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'localhost' do
conn = Faraday::Connection.new('http://127.0.0.1')
expect(conn.proxy).to be_nil
end
end
it 'allows when url is not in no proxy list but ip address is' do
with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => '127.0.0.1' do
conn = Faraday::Connection.new('http://localhost')
expect(conn.proxy).to be_nil
end
end
it 'allows in multi element no proxy list' do
with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example0.com,example.com,example1.com' do
expect(Faraday::Connection.new('http://example0.com').proxy).to be_nil
expect(Faraday::Connection.new('http://example.com').proxy).to be_nil
expect(Faraday::Connection.new('http://example1.com').proxy).to be_nil
expect(Faraday::Connection.new('http://example2.com').proxy.host).to eq('proxy.com')
end
end
it 'test proxy requires uri' do
expect { conn.proxy = { uri: :bad_uri, user: 'rick' } }.to raise_error(ArgumentError)
end
it 'uses env http_proxy' do
with_env 'http_proxy' => 'http://proxy.com' do
conn = Faraday.new
expect(conn.instance_variable_get(:@manual_proxy)).to be_falsey
expect(conn.proxy_for_request('http://google.co.uk').host).to eq('proxy.com')
end
end
it 'uses processes no_proxy before http_proxy' do
with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'google.co.uk' do
conn = Faraday.new
expect(conn.instance_variable_get(:@manual_proxy)).to be_falsey
expect(conn.proxy_for_request('http://google.co.uk')).to be_nil
end
end
it 'uses env https_proxy' do
with_env 'https_proxy' => 'https://proxy.com' do
conn = Faraday.new
expect(conn.instance_variable_get(:@manual_proxy)).to be_falsey
expect(conn.proxy_for_request('https://google.co.uk').host).to eq('proxy.com')
end
end
it 'uses processes no_proxy before https_proxy' do
with_env 'https_proxy' => 'https://proxy.com', 'no_proxy' => 'google.co.uk' do
conn = Faraday.new
expect(conn.instance_variable_get(:@manual_proxy)).to be_falsey
expect(conn.proxy_for_request('https://google.co.uk')).to be_nil
end
end
it 'gives priority to manually set proxy' do
with_env 'https_proxy' => 'https://proxy.com', 'no_proxy' => 'google.co.uk' do
conn = Faraday.new
conn.proxy = 'http://proxy2.com'
expect(conn.instance_variable_get(:@manual_proxy)).to be_truthy
expect(conn.proxy_for_request('https://google.co.uk').host).to eq('proxy2.com')
end
end
it 'ignores env proxy if set that way' do
with_env_proxy_disabled do
with_env 'http_proxy' => 'http://duncan.proxy.com:80' do
expect(conn.proxy).to be_nil
end
end
end
context 'performing a request' do
let(:url) { 'http://example.com' }
let(:conn) do
Faraday.new do |f|
f.adapter :test do |stubs|
stubs.get(url) do
[200, {}, 'ok']
end
end
end
end
it 'dynamically checks proxy' do
with_env 'http_proxy' => 'http://proxy.com:80' do
expect(conn.proxy.uri.host).to eq('proxy.com')
conn.get(url) do |req|
expect(req.options.proxy.uri.host).to eq('proxy.com')
end
end
conn.get(url)
expect(conn.instance_variable_get(:@temp_proxy)).to be_nil
end
it 'dynamically check no proxy' do
with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do
expect(conn.proxy.uri.host).to eq('proxy.com')
conn.get('http://example.com') do |req|
expect(req.options.proxy).to be_nil
end
end
end
end
end
describe '#dup' do
subject { conn.dup }
let(:url) { 'http://httpbingo.org/foo' }
let(:options) do
{
ssl: { verify: :none },
headers: { 'content-type' => 'text/plain' },
params: { 'a' => '1' },
request: { timeout: 5 }
}
end
it { expect(subject.build_exclusive_url).to eq(conn.build_exclusive_url) }
it { expect(subject.headers['content-type']).to eq('text/plain') }
it { expect(subject.params['a']).to eq('1') }
context 'after manual changes' do
before do
subject.headers['content-length'] = 12
subject.params['b'] = '2'
subject.options[:open_timeout] = 10
end
it { expect(subject.builder.handlers.size).to eq(1) }
it { expect(conn.builder.handlers.size).to eq(1) }
it { expect(conn.headers.key?('content-length')).to be_falsey }
it { expect(conn.params.key?('b')).to be_falsey }
it { expect(subject.options[:timeout]).to eq(5) }
it { expect(conn.options[:open_timeout]).to be_nil }
end
end
describe '#respond_to?' do
it { expect(Faraday.respond_to?(:get)).to be_truthy }
it { expect(Faraday.respond_to?(:post)).to be_truthy }
end
describe 'default_connection_options' do
context 'assigning a default value' do
before do
Faraday.default_connection_options = nil
Faraday.default_connection_options.request.timeout = 10
end
it_behaves_like 'default connection options'
end
context 'assigning a hash' do
before { Faraday.default_connection_options = { request: { timeout: 10 } } }
it_behaves_like 'default connection options'
end
context 'preserving a user_agent assigned via default_conncetion_options' do
around do |example|
old = Faraday.default_connection_options
Faraday.default_connection_options = { headers: { user_agent: 'My Agent 1.2' } }
example.run
Faraday.default_connection_options = old
end
context 'when url is a Hash' do
let(:conn) { Faraday.new(url: 'http://example.co', headers: { 'CustomHeader' => 'CustomValue' }) }
it { expect(conn.headers).to eq('CustomHeader' => 'CustomValue', 'User-Agent' => 'My Agent 1.2') }
end
context 'when url is a String' do
let(:conn) { Faraday.new('http://example.co', headers: { 'CustomHeader' => 'CustomValue' }) }
it { expect(conn.headers).to eq('CustomHeader' => 'CustomValue', 'User-Agent' => 'My Agent 1.2') }
end
end
end
describe 'request params' do
context 'with simple url' do
let(:url) { 'http://example.com' }
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
before do
conn.adapter(:test, stubs)
stubs.get('http://example.com?a=a&p=3') do
[200, {}, 'ok']
end
end
after { stubs.verify_stubbed_calls }
it 'test_overrides_request_params' do
conn.get('?p=2&a=a', p: 3)
end
it 'test_overrides_request_params_block' do
conn.get('?p=1&a=a', p: 2) do |req|
req.params[:p] = 3
end
end
it 'test_overrides_request_params_block_url' do
conn.get(nil, p: 2) do |req|
req.url('?p=1&a=a', 'p' => 3)
end
end
end
context 'with url and extra params' do
let(:url) { 'http://example.com?a=1&b=2' }
let(:options) { { params: { c: 3 } } }
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
before do
conn.adapter(:test, stubs)
end
it 'merges connection and request params' do
expected = 'http://example.com?a=1&b=2&c=3&limit=5&page=1'
stubs.get(expected) { [200, {}, 'ok'] }
conn.get('?page=1', limit: 5)
stubs.verify_stubbed_calls
end
it 'allows to override all params' do
expected = 'http://example.com?b=b'
stubs.get(expected) { [200, {}, 'ok'] }
conn.get('?p=1&a=a', p: 2) do |req|
expect(req.params[:a]).to eq('a')
expect(req.params['c']).to eq(3)
expect(req.params['p']).to eq(2)
req.params = { b: 'b' }
expect(req.params['b']).to eq('b')
end
stubs.verify_stubbed_calls
end
it 'allows to set params_encoder for single request' do
encoder = CustomEncoder.new
expected = 'http://example.com/?A-1,B-2,C-3,FEELING-BLUE'
stubs.get(expected) { [200, {}, 'ok'] }
conn.get('/', a: 1, b: 2, c: 3, feeling: 'blue') do |req|
req.options.params_encoder = encoder
end
stubs.verify_stubbed_calls
end
end
context 'with default params encoder' do
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
before do
conn.adapter(:test, stubs)
stubs.get('http://example.com?color%5B%5D=blue&color%5B%5D=red') do
[200, {}, 'ok']
end
end
after { stubs.verify_stubbed_calls }
it 'supports array params in url' do
conn.get('http://example.com?color[]=blue&color[]=red')
end
it 'supports array params in params' do
conn.get('http://example.com', color: %w[blue red])
end
end
context 'with flat params encoder' do
let(:options) { { request: { params_encoder: Faraday::FlatParamsEncoder } } }
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
before do
conn.adapter(:test, stubs)
stubs.get('http://example.com?color=blue&color=red') do
[200, {}, 'ok']
end
end
after { stubs.verify_stubbed_calls }
it 'supports array params in params' do
conn.get('http://example.com', color: %w[blue red])
end
context 'with array param in url' do
let(:url) { 'http://example.com?color[]=blue&color[]=red' }
it do
conn.get('/')
end
end
end
end
end
@@ -0,0 +1,85 @@
# frozen_string_literal: true
RSpec.describe Faraday::Error do
describe '.initialize' do
subject { described_class.new(exception, response) }
let(:response) { nil }
context 'with exception only' do
let(:exception) { RuntimeError.new('test') }
it { expect(subject.wrapped_exception).to eq(exception) }
it { expect(subject.response).to be_nil }
it { expect(subject.message).to eq(exception.message) }
it { expect(subject.backtrace).to eq(exception.backtrace) }
it { expect(subject.inspect).to eq('#<Faraday::Error wrapped=#<RuntimeError: test>>') }
it { expect(subject.response_status).to be_nil }
it { expect(subject.response_headers).to be_nil }
it { expect(subject.response_body).to be_nil }
end
context 'with response hash' do
let(:exception) { { status: 400 } }
it { expect(subject.wrapped_exception).to be_nil }
it { expect(subject.response).to eq(exception) }
it { expect(subject.message).to eq('the server responded with status 400') }
it { expect(subject.inspect).to eq('#<Faraday::Error response={:status=>400}>') }
it { expect(subject.response_status).to eq(400) }
it { expect(subject.response_headers).to be_nil }
it { expect(subject.response_body).to be_nil }
end
context 'with string' do
let(:exception) { 'custom message' }
it { expect(subject.wrapped_exception).to be_nil }
it { expect(subject.response).to be_nil }
it { expect(subject.message).to eq('custom message') }
it { expect(subject.inspect).to eq('#<Faraday::Error #<Faraday::Error: custom message>>') }
it { expect(subject.response_status).to be_nil }
it { expect(subject.response_headers).to be_nil }
it { expect(subject.response_body).to be_nil }
end
context 'with anything else #to_s' do
let(:exception) { %w[error1 error2] }
it { expect(subject.wrapped_exception).to be_nil }
it { expect(subject.response).to be_nil }
it { expect(subject.message).to eq('["error1", "error2"]') }
it { expect(subject.inspect).to eq('#<Faraday::Error #<Faraday::Error: ["error1", "error2"]>>') }
it { expect(subject.response_status).to be_nil }
it { expect(subject.response_headers).to be_nil }
it { expect(subject.response_body).to be_nil }
end
context 'with exception string and response hash' do
let(:exception) { 'custom message' }
let(:response) { { status: 400 } }
it { expect(subject.wrapped_exception).to be_nil }
it { expect(subject.response).to eq(response) }
it { expect(subject.message).to eq('custom message') }
it { expect(subject.inspect).to eq('#<Faraday::Error response={:status=>400}>') }
it { expect(subject.response_status).to eq(400) }
it { expect(subject.response_headers).to be_nil }
it { expect(subject.response_body).to be_nil }
end
context 'with exception and response object' do
let(:exception) { RuntimeError.new('test') }
let(:body) { { test: 'test' } }
let(:headers) { { 'Content-Type' => 'application/json' } }
let(:response) { Faraday::Response.new(status: 400, response_headers: headers, response_body: body) }
it { expect(subject.wrapped_exception).to eq(exception) }
it { expect(subject.response).to eq(response) }
it { expect(subject.message).to eq(exception.message) }
it { expect(subject.backtrace).to eq(exception.backtrace) }
it { expect(subject.response_status).to eq(400) }
it { expect(subject.response_headers).to eq(headers) }
it { expect(subject.response_body).to eq(body) }
end
end
end
@@ -0,0 +1,31 @@
# frozen_string_literal: true
RSpec.describe Faraday::MiddlewareRegistry do
before do
stub_const('CustomMiddleware', custom_middleware_klass)
end
let(:custom_middleware_klass) { Class.new(Faraday::Middleware) }
let(:dummy) { Class.new { extend Faraday::MiddlewareRegistry } }
after { dummy.unregister_middleware(:custom) }
it 'allows to register with constant' do
dummy.register_middleware(custom: custom_middleware_klass)
expect(dummy.lookup_middleware(:custom)).to eq(custom_middleware_klass)
end
it 'allows to register with symbol' do
dummy.register_middleware(custom: :CustomMiddleware)
expect(dummy.lookup_middleware(:custom)).to eq(custom_middleware_klass)
end
it 'allows to register with string' do
dummy.register_middleware(custom: 'CustomMiddleware')
expect(dummy.lookup_middleware(:custom)).to eq(custom_middleware_klass)
end
it 'allows to register with Proc' do
dummy.register_middleware(custom: -> { custom_middleware_klass })
expect(dummy.lookup_middleware(:custom)).to eq(custom_middleware_klass)
end
end
@@ -0,0 +1,70 @@
# frozen_string_literal: true
RSpec.describe Faraday::Middleware do
subject { described_class.new(app) }
let(:app) { double }
describe 'options' do
context 'when options are passed to the middleware' do
subject { described_class.new(app, options) }
let(:options) { { field: 'value' } }
it 'accepts options when initialized' do
expect(subject.options[:field]).to eq('value')
end
end
end
describe '#on_request' do
subject do
Class.new(described_class) do
def on_request(env)
# do nothing
end
end.new(app)
end
it 'is called by #call' do
expect(app).to receive(:call).and_return(app)
expect(app).to receive(:on_complete)
is_expected.to receive(:call).and_call_original
is_expected.to receive(:on_request)
subject.call(double)
end
end
describe '#on_error' do
subject do
Class.new(described_class) do
def on_error(error)
# do nothing
end
end.new(app)
end
it 'is called by #call' do
expect(app).to receive(:call).and_raise(Faraday::ConnectionFailed)
is_expected.to receive(:call).and_call_original
is_expected.to receive(:on_error)
expect { subject.call(double) }.to raise_error(Faraday::ConnectionFailed)
end
end
describe '#close' do
context "with app that doesn't support \#close" do
it 'should issue warning' do
is_expected.to receive(:warn)
subject.close
end
end
context "with app that supports \#close" do
it 'should issue warning' do
expect(app).to receive(:close)
is_expected.to_not receive(:warn)
subject.close
end
end
end
end
@@ -0,0 +1,76 @@
# frozen_string_literal: true
RSpec.describe Faraday::Env do
subject(:env) { described_class.new }
it 'allows to access members' do
expect(env.method).to be_nil
env.method = :get
expect(env.method).to eq(:get)
end
it 'allows to access symbol non members' do
expect(env[:custom]).to be_nil
env[:custom] = :boom
expect(env[:custom]).to eq(:boom)
end
it 'allows to access string non members' do
expect(env['custom']).to be_nil
env['custom'] = :boom
expect(env['custom']).to eq(:boom)
end
it 'ignores false when fetching' do
ssl = Faraday::SSLOptions.new
ssl.verify = false
expect(ssl.fetch(:verify, true)).to be_falsey
end
it 'handle verify_hostname when fetching' do
ssl = Faraday::SSLOptions.new
ssl.verify_hostname = true
expect(ssl.fetch(:verify_hostname, false)).to be_truthy
end
it 'retains custom members' do
env[:foo] = 'custom 1'
env[:bar] = :custom2
env2 = Faraday::Env.from(env)
env2[:baz] = 'custom 3'
expect(env2[:foo]).to eq('custom 1')
expect(env2[:bar]).to eq(:custom2)
expect(env[:baz]).to be_nil
end
describe '#body' do
subject(:env) { described_class.from(body: { foo: 'bar' }) }
context 'when response is not finished yet' do
it 'returns the request body' do
expect(env.body).to eq(foo: 'bar')
end
end
context 'when response is finished' do
before do
env.status = 200
env.body = { bar: 'foo' }
env.response = Faraday::Response.new(env)
end
it 'returns the response body' do
expect(env.body).to eq(bar: 'foo')
end
it 'allows to access request_body' do
expect(env.request_body).to eq(foo: 'bar')
end
it 'allows to access response_body' do
expect(env.response_body).to eq(bar: 'foo')
end
end
end
end
@@ -0,0 +1,297 @@
# frozen_string_literal: true
RSpec.describe Faraday::Options do
SubOptions = Class.new(Faraday::Options.new(:sub_a, :sub_b))
ParentOptions = Faraday::Options.new(:a, :b, :c) do
options c: SubOptions
end
describe '#merge' do
it 'merges options with hashes' do
options = ParentOptions.new(1)
expect(options.a).to eq(1)
expect(options.b).to be_nil
dup = options.merge a: 2, b: 3
expect(dup.a).to eq(2)
expect(dup.b).to eq(3)
expect(options.a).to eq(1)
expect(options.b).to be_nil
end
it 'deeply merges two options' do
sub_opts1 = SubOptions.from(sub_a: 3)
sub_opts2 = SubOptions.from(sub_b: 4)
opt1 = ParentOptions.from(a: 1, c: sub_opts1)
opt2 = ParentOptions.from(b: 2, c: sub_opts2)
merged = opt1.merge(opt2)
expected_sub_opts = SubOptions.from(sub_a: 3, sub_b: 4)
expected = ParentOptions.from(a: 1, b: 2, c: expected_sub_opts)
expect(merged).to eq(expected)
end
it 'deeply merges options with hashes' do
sub_opts1 = SubOptions.from(sub_a: 3)
sub_opts2 = { sub_b: 4 }
opt1 = ParentOptions.from(a: 1, c: sub_opts1)
opt2 = { b: 2, c: sub_opts2 }
merged = opt1.merge(opt2)
expected_sub_opts = SubOptions.from(sub_a: 3, sub_b: 4)
expected = ParentOptions.from(a: 1, b: 2, c: expected_sub_opts)
expect(merged).to eq(expected)
end
it 'deeply merges options with nil' do
sub_opts = SubOptions.new(3, 4)
options = ParentOptions.new(1, 2, sub_opts)
expect(options.a).to eq(1)
expect(options.b).to eq(2)
expect(options.c.sub_a).to eq(3)
expect(options.c.sub_b).to eq(4)
options2 = ParentOptions.from(b: 5, c: nil)
merged = options.merge(options2)
expect(merged.b).to eq(5)
expect(merged.c).to eq(sub_opts)
end
it 'deeply merges options with options having nil sub-options' do
options = ParentOptions.from(a: 1)
sub_opts = SubOptions.new(3, 4)
options2 = ParentOptions.from(b: 2, c: sub_opts)
expect(options.a).to eq(1)
expect(options2.b).to eq(2)
expect(options2.c.sub_a).to eq(3)
expect(options2.c.sub_b).to eq(4)
merged = options.merge(options2)
expect(merged.c).to eq(sub_opts)
end
describe '#dup' do
it 'duplicate options but not sub-options' do
sub_opts = SubOptions.from(sub_a: 3)
opts = ParentOptions.from(b: 1, c: sub_opts)
duped = opts.dup
duped.b = 2
duped.c.sub_a = 4
expect(opts.b).to eq(1)
expect(opts.c.sub_a).to eq(4)
end
end
describe '#deep_dup' do
it 'duplicate options and also suboptions' do
sub_opts = SubOptions.from(sub_a: 3)
opts = ParentOptions.from(b: 1, c: sub_opts)
duped = opts.deep_dup
duped.b = 2
duped.c.sub_a = 4
expect(opts.b).to eq(1)
expect(opts.c.sub_a).to eq(3)
end
end
describe '#clear' do
it 'clears the options' do
options = SubOptions.new(1)
expect(options.empty?).not_to be_truthy
options.clear
expect(options.empty?).to be_truthy
end
end
describe '#empty?' do
it 'returns true only if all options are nil' do
options = SubOptions.new
expect(options.empty?).to be_truthy
options.sub_a = 1
expect(options.empty?).not_to be_truthy
options.delete(:sub_a)
expect(options.empty?).to be_truthy
end
end
describe '#each_key' do
it 'allows to iterate through keys' do
options = ParentOptions.new(1, 2, 3)
enum = options.each_key
expect(enum.next.to_sym).to eq(:a)
expect(enum.next.to_sym).to eq(:b)
expect(enum.next.to_sym).to eq(:c)
end
end
describe '#key?' do
it 'returns true if the key exists and is not nil' do
options = SubOptions.new
expect(options.key?(:sub_a)).not_to be_truthy
options.sub_a = 1
expect(options.key?(:sub_a)).to be_truthy
end
end
describe '#each_value' do
it 'allows to iterate through values' do
options = ParentOptions.new(1, 2, 3)
enum = options.each_value
expect(enum.next).to eq(1)
expect(enum.next).to eq(2)
expect(enum.next).to eq(3)
end
end
describe '#value?' do
it 'returns true if any key has that value' do
options = SubOptions.new
expect(options.value?(1)).not_to be_truthy
options.sub_a = 1
expect(options.value?(1)).to be_truthy
end
end
describe '#update' do
it 'updates options from hashes' do
options = ParentOptions.new(1)
expect(options.a).to eq(1)
expect(options.b).to be_nil
updated = options.update a: 2, b: 3
expect(options.a).to eq(2)
expect(options.b).to eq(3)
expect(updated).to eq(options)
end
end
describe '#delete' do
it 'allows to remove value for key' do
options = ParentOptions.new(1)
expect(options.a).to eq(1)
expect(options.delete(:a)).to eq(1)
expect(options.a).to be_nil
end
end
describe '#from' do
it { expect { ParentOptions.from invalid: 1 }.to raise_error(NoMethodError) }
it 'works with options' do
options = ParentOptions.new(1)
value = ParentOptions.from(options)
expect(value.a).to eq(1)
expect(value.b).to be_nil
end
it 'works with options with sub object' do
sub = SubOptions.new(1)
options = ParentOptions.from a: 1, c: sub
expect(options).to be_a_kind_of(ParentOptions)
expect(options.a).to eq(1)
expect(options.b).to be_nil
expect(options.c).to be_a_kind_of(SubOptions)
expect(options.c.sub_a).to eq(1)
end
it 'works with hash' do
options = ParentOptions.from a: 1
expect(options).to be_a_kind_of(ParentOptions)
expect(options.a).to eq(1)
expect(options.b).to be_nil
end
it 'works with hash with sub object' do
options = ParentOptions.from a: 1, c: { sub_a: 1 }
expect(options).to be_a_kind_of(ParentOptions)
expect(options.a).to eq(1)
expect(options.b).to be_nil
expect(options.c).to be_a_kind_of(SubOptions)
expect(options.c.sub_a).to eq(1)
end
it 'works with deep hash' do
hash = { b: 1 }
options = ParentOptions.from a: hash
expect(options.a[:b]).to eq(1)
hash[:b] = 2
expect(options.a[:b]).to eq(1)
options.a[:b] = 3
expect(hash[:b]).to eq(2)
expect(options.a[:b]).to eq(3)
end
it 'works with nil' do
options = ParentOptions.from(nil)
expect(options).to be_a_kind_of(ParentOptions)
expect(options.a).to be_nil
expect(options.b).to be_nil
end
it 'respects inheritance' do
subclass = Class.new(ParentOptions)
options = subclass.from(c: { sub_a: 'hello' })
expect(options.c).to be_a_kind_of(SubOptions)
expect(options.c.sub_a).to eq('hello')
end
end
describe '#memoized' do
subject(:options_class) { Class.new(ParentOptions) }
it 'requires block' do
expect { options_class.memoized(:a) }.to raise_error(ArgumentError)
end
it 'accepts block' do
options_class.memoized(:a) { :foo }
expect(options_class.new.a).to eql(:foo)
end
end
describe '#fetch' do
subject { SubOptions.new }
context 'when the fetched key has no value' do
it 'uses falsey default' do
expect(subject.fetch(:sub_a, false) { |_| :blah }).to be_falsey
end
it 'accepts block' do
expect(subject.fetch(:sub_a) { |k| "yo #{k.inspect}" }).to eq('yo :sub_a')
end
it 'needs a default if key is missing' do
expect { subject.fetch(:sub_a) }.to raise_error(Faraday::Options.fetch_error_class)
end
end
context 'when the fetched key has a value' do
before do
subject.sub_a = 1
end
it 'grabs value' do
expect(subject.fetch(:sub_a, false) { |_| :blah }).to eq(1)
end
it 'works with key' do
expect(subject.fetch(:sub_a)).to eq(1)
end
end
end
end
end
@@ -0,0 +1,52 @@
# frozen_string_literal: true
RSpec.describe Faraday::ProxyOptions do
describe '#from' do
it 'works with string' do
options = Faraday::ProxyOptions.from 'http://user:pass@example.org'
expect(options.user).to eq('user')
expect(options.password).to eq('pass')
expect(options.uri).to be_a_kind_of(URI)
expect(options.path).to eq('')
expect(options.port).to eq(80)
expect(options.host).to eq('example.org')
expect(options.scheme).to eq('http')
expect(options.inspect).to match('#<Faraday::ProxyOptions uri=')
end
it 'defaults to http' do
options = Faraday::ProxyOptions.from 'example.org'
expect(options.port).to eq(80)
expect(options.host).to eq('example.org')
expect(options.scheme).to eq('http')
end
it 'works with nil' do
options = Faraday::ProxyOptions.from nil
expect(options).to be_a_kind_of(Faraday::ProxyOptions)
expect(options.inspect).to eq('#<Faraday::ProxyOptions (empty)>')
end
it 'works with no auth' do
proxy = Faraday::ProxyOptions.from 'http://example.org'
expect(proxy.user).to be_nil
expect(proxy.password).to be_nil
end
it 'treats empty string as nil' do
proxy = nil
proxy_string = proxy.to_s # => empty string
options = Faraday::ProxyOptions.from proxy_string
expect(options).to be_a_kind_of(Faraday::ProxyOptions)
expect(options.inspect).to eq('#<Faraday::ProxyOptions (empty)>')
end
end
it 'allows hash access' do
proxy = Faraday::ProxyOptions.from 'http://a%40b:pw%20d@example.org'
expect(proxy.user).to eq('a@b')
expect(proxy[:user]).to eq('a@b')
expect(proxy.password).to eq('pw d')
expect(proxy[:password]).to eq('pw d')
end
end
@@ -0,0 +1,19 @@
# frozen_string_literal: true
RSpec.describe Faraday::RequestOptions do
subject(:options) { Faraday::RequestOptions.new }
it 'allows to set the request proxy' do
expect(options.proxy).to be_nil
expect { options[:proxy] = { booya: 1 } }.to raise_error(NoMethodError)
options[:proxy] = { user: 'user' }
expect(options.proxy).to be_a_kind_of(Faraday::ProxyOptions)
expect(options.proxy.user).to eq('user')
options.proxy = nil
expect(options.proxy).to be_nil
expect(options.inspect).to eq('#<Faraday::RequestOptions (empty)>')
end
end
@@ -0,0 +1,42 @@
# frozen_string_literal: true
require 'rack/utils'
RSpec.describe Faraday::FlatParamsEncoder do
it_behaves_like 'a params encoder'
it 'decodes arrays' do
query = 'a=one&a=two&a=three'
expected = { 'a' => %w[one two three] }
expect(subject.decode(query)).to eq(expected)
end
it 'decodes boolean values' do
query = 'a=true&b=false'
expected = { 'a' => 'true', 'b' => 'false' }
expect(subject.decode(query)).to eq(expected)
end
it 'encodes boolean values' do
params = { a: true, b: false }
expect(subject.encode(params)).to eq('a=true&b=false')
end
it 'encodes boolean values in array' do
params = { a: [true, false] }
expect(subject.encode(params)).to eq('a=true&a=false')
end
it 'encodes empty array in hash' do
params = { a: [] }
expect(subject.encode(params)).to eq('a=')
end
it 'encodes unsorted when asked' do
params = { b: false, a: true }
expect(subject.encode(params)).to eq('a=true&b=false')
Faraday::FlatParamsEncoder.sort_params = false
expect(subject.encode(params)).to eq('b=false&a=true')
Faraday::FlatParamsEncoder.sort_params = true
end
end
@@ -0,0 +1,150 @@
# frozen_string_literal: true
require 'rack/utils'
RSpec.describe Faraday::NestedParamsEncoder do
it_behaves_like 'a params encoder'
it 'decodes arrays' do
query = 'a[1]=one&a[2]=two&a[3]=three'
expected = { 'a' => %w[one two three] }
expect(subject.decode(query)).to eq(expected)
end
it 'decodes hashes' do
query = 'a[b1]=one&a[b2]=two&a[b][c]=foo'
expected = { 'a' => { 'b1' => 'one', 'b2' => 'two', 'b' => { 'c' => 'foo' } } }
expect(subject.decode(query)).to eq(expected)
end
it 'decodes nested arrays rack compat' do
query = 'a[][one]=1&a[][two]=2&a[][one]=3&a[][two]=4'
expected = Rack::Utils.parse_nested_query(query)
expect(subject.decode(query)).to eq(expected)
end
it 'decodes nested array mixed types' do
query = 'a[][one]=1&a[]=2&a[]=&a[]'
expected = Rack::Utils.parse_nested_query(query)
expect(subject.decode(query)).to eq(expected)
end
it 'decodes nested ignores invalid array' do
query = '[][a]=1&b=2'
expected = { 'a' => '1', 'b' => '2' }
expect(subject.decode(query)).to eq(expected)
end
it 'decodes nested ignores repeated array notation' do
query = 'a[][][]=1'
expected = { 'a' => ['1'] }
expect(subject.decode(query)).to eq(expected)
end
it 'decodes nested ignores malformed keys' do
query = '=1&[]=2'
expected = {}
expect(subject.decode(query)).to eq(expected)
end
it 'decodes nested subkeys dont have to be in brackets' do
query = 'a[b]c[d]e=1'
expected = { 'a' => { 'b' => { 'c' => { 'd' => { 'e' => '1' } } } } }
expect(subject.decode(query)).to eq(expected)
end
it 'decodes nested final value overrides any type' do
query = 'a[b][c]=1&a[b]=2'
expected = { 'a' => { 'b' => '2' } }
expect(subject.decode(query)).to eq(expected)
end
it 'encodes rack compat' do
params = { a: [{ one: '1', two: '2' }, '3', ''] }
result = Faraday::Utils.unescape(Faraday::NestedParamsEncoder.encode(params)).split('&')
expected = Rack::Utils.build_nested_query(params).split('&')
expect(result).to match_array(expected)
end
it 'encodes empty string array value' do
expected = 'baz=&foo%5Bbar%5D='
result = Faraday::NestedParamsEncoder.encode(foo: { bar: '' }, baz: '')
expect(result).to eq(expected)
end
it 'encodes nil array value' do
expected = 'baz&foo%5Bbar%5D'
result = Faraday::NestedParamsEncoder.encode(foo: { bar: nil }, baz: nil)
expect(result).to eq(expected)
end
it 'encodes empty array value' do
expected = 'baz%5B%5D&foo%5Bbar%5D%5B%5D'
result = Faraday::NestedParamsEncoder.encode(foo: { bar: [] }, baz: [])
expect(result).to eq(expected)
end
it 'encodes boolean values' do
params = { a: true, b: false }
expect(subject.encode(params)).to eq('a=true&b=false')
end
it 'encodes boolean values in array' do
params = { a: [true, false] }
expect(subject.encode(params)).to eq('a%5B%5D=true&a%5B%5D=false')
end
it 'encodes unsorted when asked' do
params = { b: false, a: true }
expect(subject.encode(params)).to eq('a=true&b=false')
Faraday::NestedParamsEncoder.sort_params = false
expect(subject.encode(params)).to eq('b=false&a=true')
Faraday::NestedParamsEncoder.sort_params = true
end
it 'encodes arrays indices when asked' do
params = { a: [0, 1, 2] }
expect(subject.encode(params)).to eq('a%5B%5D=0&a%5B%5D=1&a%5B%5D=2')
Faraday::NestedParamsEncoder.array_indices = true
expect(subject.encode(params)).to eq('a%5B0%5D=0&a%5B1%5D=1&a%5B2%5D=2')
Faraday::NestedParamsEncoder.array_indices = false
end
shared_examples 'a wrong decoding' do
it do
expect { subject.decode(query) }.to raise_error(TypeError) do |e|
expect(e.message).to eq(error_message)
end
end
end
context 'when expecting hash but getting string' do
let(:query) { 'a=1&a[b]=2' }
let(:error_message) { "expected Hash (got String) for param `a'" }
it_behaves_like 'a wrong decoding'
end
context 'when expecting hash but getting array' do
let(:query) { 'a[]=1&a[b]=2' }
let(:error_message) { "expected Hash (got Array) for param `a'" }
it_behaves_like 'a wrong decoding'
end
context 'when expecting nested hash but getting non nested' do
let(:query) { 'a[b]=1&a[b][c]=2' }
let(:error_message) { "expected Hash (got String) for param `b'" }
it_behaves_like 'a wrong decoding'
end
context 'when expecting array but getting hash' do
let(:query) { 'a[b]=1&a[]=2' }
let(:error_message) { "expected Array (got Hash) for param `a'" }
it_behaves_like 'a wrong decoding'
end
context 'when expecting array but getting string' do
let(:query) { 'a=1&a[]=2' }
let(:error_message) { "expected Array (got String) for param `a'" }
it_behaves_like 'a wrong decoding'
end
end
@@ -0,0 +1,317 @@
# frozen_string_literal: true
RSpec.describe Faraday::RackBuilder do
# mock handler classes
(Handler = Struct.new(:app)).class_eval do
def call(env)
env[:request_headers]['X-Middleware'] ||= ''
env[:request_headers]['X-Middleware'] += ":#{self.class.name.split('::').last}"
app.call(env)
end
end
class Apple < Handler
end
class Orange < Handler
end
class Banana < Handler
end
subject { conn.builder }
before { Faraday.default_adapter = :test }
after { Faraday.default_adapter = nil }
context 'with default stack' do
let(:conn) { Faraday::Connection.new }
it { expect(subject[0]).to eq(Faraday::Request.lookup_middleware(:url_encoded)) }
it { expect(subject.adapter).to eq(Faraday::Adapter.lookup_middleware(Faraday.default_adapter)) }
end
context 'with custom empty block' do
let(:conn) { Faraday::Connection.new {} }
it { expect(subject[0]).to be_nil }
it { expect(subject.adapter).to eq(Faraday::Adapter.lookup_middleware(Faraday.default_adapter)) }
end
context 'with custom adapter only' do
let(:conn) do
Faraday::Connection.new do |builder|
builder.adapter :test do |stub|
stub.get('/') { |_| [200, {}, ''] }
end
end
end
it { expect(subject[0]).to be_nil }
it { expect(subject.adapter).to eq(Faraday::Adapter.lookup_middleware(:test)) }
end
context 'with custom handler and adapter' do
let(:conn) do
Faraday::Connection.new do |builder|
builder.use Apple
builder.adapter :test do |stub|
stub.get('/') { |_| [200, {}, ''] }
end
end
end
it 'locks the stack after making a request' do
expect(subject.locked?).to be_falsey
conn.get('/')
expect(subject.locked?).to be_truthy
expect { subject.use(Orange) }.to raise_error(Faraday::RackBuilder::StackLocked)
end
it 'dup stack is unlocked' do
expect(subject.locked?).to be_falsey
subject.lock!
expect(subject.locked?).to be_truthy
dup = subject.dup
expect(dup).to eq(subject)
expect(dup.locked?).to be_falsey
end
it 'allows to compare handlers' do
expect(subject.handlers.first).to eq(Faraday::RackBuilder::Handler.new(Apple))
end
end
context 'when having a single handler' do
let(:conn) { Faraday::Connection.new {} }
before { subject.use(Apple) }
it { expect(subject.handlers).to eq([Apple]) }
it 'allows use' do
subject.use(Orange)
expect(subject.handlers).to eq([Apple, Orange])
end
it 'allows insert_before' do
subject.insert_before(Apple, Orange)
expect(subject.handlers).to eq([Orange, Apple])
end
it 'allows insert_after' do
subject.insert_after(Apple, Orange)
expect(subject.handlers).to eq([Apple, Orange])
end
it 'raises an error trying to use an unregistered symbol' do
expect { subject.use(:apple) }.to raise_error(Faraday::Error) do |err|
expect(err.message).to eq(':apple is not registered on Faraday::Middleware')
end
end
end
context 'when having two handlers' do
let(:conn) { Faraday::Connection.new {} }
before do
subject.use(Apple)
subject.use(Orange)
end
it 'allows insert_before' do
subject.insert_before(Orange, Banana)
expect(subject.handlers).to eq([Apple, Banana, Orange])
end
it 'allows insert_after' do
subject.insert_after(Apple, Banana)
expect(subject.handlers).to eq([Apple, Banana, Orange])
end
it 'allows to swap handlers' do
subject.swap(Apple, Banana)
expect(subject.handlers).to eq([Banana, Orange])
end
it 'allows to delete a handler' do
subject.delete(Apple)
expect(subject.handlers).to eq([Orange])
end
end
context 'when adapter is added with named options' do
after { Faraday.default_adapter_options = {} }
let(:conn) { Faraday::Connection.new {} }
let(:cat_adapter) do
Class.new(Faraday::Adapter) do
attr_accessor :name
def initialize(app, name:)
super(app)
@name = name
end
end
end
let(:cat) { subject.adapter.build }
it 'adds a handler to construct adapter with named options' do
Faraday.default_adapter = cat_adapter
Faraday.default_adapter_options = { name: 'Chloe' }
expect { cat }.to_not output(
/warning: Using the last argument as keyword parameters is deprecated/
).to_stderr
expect(cat.name).to eq 'Chloe'
end
end
context 'when middleware is added with named arguments' do
let(:conn) { Faraday::Connection.new {} }
let(:dog_middleware) do
Class.new(Faraday::Middleware) do
attr_accessor :name
def initialize(app, name:)
super(app)
@name = name
end
end
end
let(:dog) do
subject.handlers.find { |handler| handler == dog_middleware }.build
end
it 'adds a handler to construct middleware with options passed to use' do
subject.use dog_middleware, name: 'Rex'
expect { dog }.to_not output(
/warning: Using the last argument as keyword parameters is deprecated/
).to_stderr
expect(dog.name).to eq('Rex')
end
end
context 'when a middleware is added with named arguments' do
let(:conn) { Faraday::Connection.new {} }
let(:cat_request) do
Class.new(Faraday::Middleware) do
attr_accessor :name
def initialize(app, name:)
super(app)
@name = name
end
end
end
let(:cat) do
subject.handlers.find { |handler| handler == cat_request }.build
end
it 'adds a handler to construct request adapter with options passed to request' do
Faraday::Request.register_middleware cat_request: cat_request
subject.request :cat_request, name: 'Felix'
expect { cat }.to_not output(
/warning: Using the last argument as keyword parameters is deprecated/
).to_stderr
expect(cat.name).to eq('Felix')
end
end
context 'when a middleware is added with named arguments' do
let(:conn) { Faraday::Connection.new {} }
let(:fish_response) do
Class.new(Faraday::Middleware) do
attr_accessor :name
def initialize(app, name:)
super(app)
@name = name
end
end
end
let(:fish) do
subject.handlers.find { |handler| handler == fish_response }.build
end
it 'adds a handler to construct response adapter with options passed to response' do
Faraday::Response.register_middleware fish_response: fish_response
subject.response :fish_response, name: 'Bubbles'
expect { fish }.to_not output(
/warning: Using the last argument as keyword parameters is deprecated/
).to_stderr
expect(fish.name).to eq('Bubbles')
end
end
context 'when a plain adapter is added with named arguments' do
let(:conn) { Faraday::Connection.new {} }
let(:rabbit_adapter) do
Class.new(Faraday::Adapter) do
attr_accessor :name
def initialize(app, name:)
super(app)
@name = name
end
end
end
let(:rabbit) do
subject.adapter.build
end
it 'adds a handler to construct adapter with options passed to adapter' do
Faraday::Adapter.register_middleware rabbit_adapter: rabbit_adapter
subject.adapter :rabbit_adapter, name: 'Thumper'
expect { rabbit }.to_not output(
/warning: Using the last argument as keyword parameters is deprecated/
).to_stderr
expect(rabbit.name).to eq('Thumper')
end
end
context 'when handlers are directly added or updated' do
let(:conn) { Faraday::Connection.new {} }
let(:rock_handler) do
Class.new do
attr_accessor :name
def initialize(_app, name:)
@name = name
end
end
end
let(:rock) do
subject.handlers.find { |handler| handler == rock_handler }.build
end
it 'adds a handler to construct adapter with options passed to insert' do
subject.insert 0, rock_handler, name: 'Stony'
expect { rock }.to_not output(
/warning: Using the last argument as keyword parameters is deprecated/
).to_stderr
expect(rock.name).to eq('Stony')
end
it 'adds a handler with options passed to insert_after' do
subject.insert_after 0, rock_handler, name: 'Rocky'
expect { rock }.to_not output(
/warning: Using the last argument as keyword parameters is deprecated/
).to_stderr
expect(rock.name).to eq('Rocky')
end
it 'adds a handler with options passed to swap' do
subject.insert 0, rock_handler, name: 'Flint'
subject.swap 0, rock_handler, name: 'Chert'
expect { rock }.to_not output(
/warning: Using the last argument as keyword parameters is deprecated/
).to_stderr
expect(rock.name).to eq('Chert')
end
end
end
@@ -0,0 +1,118 @@
# frozen_string_literal: true
RSpec.describe Faraday::Request::Authorization do
let(:conn) do
Faraday.new do |b|
b.request :authorization, auth_type, *auth_config
b.adapter :test do |stub|
stub.get('/auth-echo') do |env|
[200, {}, env[:request_headers]['Authorization']]
end
end
end
end
shared_examples 'does not interfere with existing authentication' do
context 'and request already has an authentication header' do
let(:response) { conn.get('/auth-echo', nil, authorization: 'OAuth oauth_token') }
it 'does not interfere with existing authorization' do
expect(response.body).to eq('OAuth oauth_token')
end
end
end
let(:response) { conn.get('/auth-echo') }
describe 'basic_auth' do
let(:auth_type) { :basic }
context 'when passed correct params' do
let(:auth_config) { %w[aladdin opensesame] }
it { expect(response.body).to eq('Basic YWxhZGRpbjpvcGVuc2VzYW1l') }
include_examples 'does not interfere with existing authentication'
end
context 'when passed very long values' do
let(:auth_config) { ['A' * 255, ''] }
it { expect(response.body).to eq("Basic #{'QUFB' * 85}Og==") }
include_examples 'does not interfere with existing authentication'
end
end
describe 'authorization' do
let(:auth_type) { :Bearer }
context 'when passed a string' do
let(:auth_config) { ['custom'] }
it { expect(response.body).to eq('Bearer custom') }
include_examples 'does not interfere with existing authentication'
end
context 'when passed a proc' do
let(:auth_config) { [-> { 'custom_from_proc' }] }
it { expect(response.body).to eq('Bearer custom_from_proc') }
include_examples 'does not interfere with existing authentication'
end
context 'when passed a callable' do
let(:callable) { double('Callable Authorizer', call: 'custom_from_callable') }
let(:auth_config) { [callable] }
it { expect(response.body).to eq('Bearer custom_from_callable') }
include_examples 'does not interfere with existing authentication'
end
context 'with an argument' do
let(:response) { conn.get('/auth-echo', nil, 'middle' => 'crunchy surprise') }
context 'when passed a proc' do
let(:auth_config) { [proc { |env| "proc #{env.request_headers['middle']}" }] }
it { expect(response.body).to eq('Bearer proc crunchy surprise') }
include_examples 'does not interfere with existing authentication'
end
context 'when passed a lambda' do
let(:auth_config) { [->(env) { "lambda #{env.request_headers['middle']}" }] }
it { expect(response.body).to eq('Bearer lambda crunchy surprise') }
include_examples 'does not interfere with existing authentication'
end
context 'when passed a callable with an argument' do
let(:callable) do
Class.new do
def call(env)
"callable #{env.request_headers['middle']}"
end
end.new
end
let(:auth_config) { [callable] }
it { expect(response.body).to eq('Bearer callable crunchy surprise') }
include_examples 'does not interfere with existing authentication'
end
end
context 'when passed too many arguments' do
let(:auth_config) { %w[baz foo] }
it { expect { response }.to raise_error(ArgumentError) }
include_examples 'does not interfere with existing authentication'
end
end
end
@@ -0,0 +1,74 @@
# frozen_string_literal: true
RSpec.describe Faraday::Request::Instrumentation do
class FakeInstrumenter
attr_reader :instrumentations
def initialize
@instrumentations = []
end
def instrument(name, env)
@instrumentations << [name, env]
yield
end
end
let(:config) { {} }
let(:options) { Faraday::Request::Instrumentation::Options.from config }
let(:instrumenter) { FakeInstrumenter.new }
let(:conn) do
Faraday.new do |f|
f.request :instrumentation, config.merge(instrumenter: instrumenter)
f.adapter :test do |stub|
stub.get '/' do
[200, {}, 'ok']
end
end
end
end
it { expect(options.name).to eq('request.faraday') }
it 'defaults to ActiveSupport::Notifications' do
res = options.instrumenter
rescue NameError => e
expect(e.to_s).to match('ActiveSupport')
else
expect(res).to eq(ActiveSupport::Notifications)
end
it 'instruments with default name' do
expect(instrumenter.instrumentations.size).to eq(0)
res = conn.get '/'
expect(res.body).to eq('ok')
expect(instrumenter.instrumentations.size).to eq(1)
name, env = instrumenter.instrumentations.first
expect(name).to eq('request.faraday')
expect(env[:url].path).to eq('/')
end
context 'with custom name' do
let(:config) { { name: 'custom' } }
it { expect(options.name).to eq('custom') }
it 'instruments with custom name' do
expect(instrumenter.instrumentations.size).to eq(0)
res = conn.get '/'
expect(res.body).to eq('ok')
expect(instrumenter.instrumentations.size).to eq(1)
name, env = instrumenter.instrumentations.first
expect(name).to eq('custom')
expect(env[:url].path).to eq('/')
end
end
context 'with custom instrumenter' do
let(:config) { { instrumenter: :custom } }
it { expect(options.instrumenter).to eq(:custom) }
end
end
@@ -0,0 +1,199 @@
# frozen_string_literal: true
RSpec.describe Faraday::Request::Json do
let(:middleware) { described_class.new(->(env) { Faraday::Response.new(env) }) }
def process(body, content_type = nil)
env = { body: body, request_headers: Faraday::Utils::Headers.new }
env[:request_headers]['content-type'] = content_type if content_type
middleware.call(Faraday::Env.from(env)).env
end
def result_body
result[:body]
end
def result_type
result[:request_headers]['content-type']
end
context 'no body' do
let(:result) { process(nil) }
it "doesn't change body" do
expect(result_body).to be_nil
end
it "doesn't add content type" do
expect(result_type).to be_nil
end
end
context 'empty body' do
let(:result) { process('') }
it "doesn't change body" do
expect(result_body).to be_empty
end
it "doesn't add content type" do
expect(result_type).to be_nil
end
end
context 'string body' do
let(:result) { process('{"a":1}') }
it "doesn't change body" do
expect(result_body).to eq('{"a":1}')
end
it 'adds content type' do
expect(result_type).to eq('application/json')
end
end
context 'object body' do
let(:result) { process(a: 1) }
it 'encodes body' do
expect(result_body).to eq('{"a":1}')
end
it 'adds content type' do
expect(result_type).to eq('application/json')
end
end
context 'empty object body' do
let(:result) { process({}) }
it 'encodes body' do
expect(result_body).to eq('{}')
end
end
context 'true body' do
let(:result) { process(true) }
it 'encodes body' do
expect(result_body).to eq('true')
end
it 'adds content type' do
expect(result_type).to eq('application/json')
end
end
context 'false body' do
let(:result) { process(false) }
it 'encodes body' do
expect(result_body).to eq('false')
end
it 'adds content type' do
expect(result_type).to eq('application/json')
end
end
context 'object body with json type' do
let(:result) { process({ a: 1 }, 'application/json; charset=utf-8') }
it 'encodes body' do
expect(result_body).to eq('{"a":1}')
end
it "doesn't change content type" do
expect(result_type).to eq('application/json; charset=utf-8')
end
end
context 'object body with vendor json type' do
let(:result) { process({ a: 1 }, 'application/vnd.myapp.v1+json; charset=utf-8') }
it 'encodes body' do
expect(result_body).to eq('{"a":1}')
end
it "doesn't change content type" do
expect(result_type).to eq('application/vnd.myapp.v1+json; charset=utf-8')
end
end
context 'object body with incompatible type' do
let(:result) { process({ a: 1 }, 'application/xml; charset=utf-8') }
it "doesn't change body" do
expect(result_body).to eq(a: 1)
end
it "doesn't change content type" do
expect(result_type).to eq('application/xml; charset=utf-8')
end
end
context 'with encoder' do
let(:encoder) do
double('Encoder').tap do |e|
allow(e).to receive(:dump) { |s, opts| JSON.generate(s, opts) }
end
end
let(:result) { process(a: 1) }
context 'when encoder is passed as object' do
let(:middleware) { described_class.new(->(env) { Faraday::Response.new(env) }, { encoder: encoder }) }
it 'calls specified JSON encoder\'s dump method' do
expect(encoder).to receive(:dump).with({ a: 1 })
result
end
it 'encodes body' do
expect(result_body).to eq('{"a":1}')
end
it 'adds content type' do
expect(result_type).to eq('application/json')
end
end
context 'when encoder is passed as an object-method pair' do
let(:middleware) { described_class.new(->(env) { Faraday::Response.new(env) }, { encoder: [encoder, :dump] }) }
it 'calls specified JSON encoder' do
expect(encoder).to receive(:dump).with({ a: 1 })
result
end
it 'encodes body' do
expect(result_body).to eq('{"a":1}')
end
it 'adds content type' do
expect(result_type).to eq('application/json')
end
end
context 'when encoder is not passed' do
let(:middleware) { described_class.new(->(env) { Faraday::Response.new(env) }) }
it 'calls JSON.generate' do
expect(JSON).to receive(:generate).with({ a: 1 })
result
end
it 'encodes body' do
expect(result_body).to eq('{"a":1}')
end
it 'adds content type' do
expect(result_type).to eq('application/json')
end
end
end
end
@@ -0,0 +1,93 @@
# frozen_string_literal: true
require 'stringio'
RSpec.describe Faraday::Request::UrlEncoded do
let(:conn) do
Faraday.new do |b|
b.request :url_encoded
b.adapter :test do |stub|
stub.post('/echo') do |env|
posted_as = env[:request_headers]['Content-Type']
body = env[:body]
if body.respond_to?(:read)
body = body.read
end
[200, { 'Content-Type' => posted_as }, body]
end
end
end
end
it 'does nothing without payload' do
response = conn.post('/echo')
expect(response.headers['Content-Type']).to be_nil
expect(response.body.empty?).to be_truthy
end
it 'ignores custom content type' do
response = conn.post('/echo', { some: 'data' }, 'content-type' => 'application/x-foo')
expect(response.headers['Content-Type']).to eq('application/x-foo')
expect(response.body).to eq(some: 'data')
end
it 'works with no headers' do
response = conn.post('/echo', fruit: %w[apples oranges])
expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded')
expect(response.body).to eq('fruit%5B%5D=apples&fruit%5B%5D=oranges')
end
it 'works with with headers' do
response = conn.post('/echo', { 'a' => 123 }, 'content-type' => 'application/x-www-form-urlencoded')
expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded')
expect(response.body).to eq('a=123')
end
it 'works with nested params' do
response = conn.post('/echo', user: { name: 'Mislav', web: 'mislav.net' })
expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded')
expected = { 'user' => { 'name' => 'Mislav', 'web' => 'mislav.net' } }
expect(Faraday::Utils.parse_nested_query(response.body)).to eq(expected)
end
it 'works with non nested params' do
response = conn.post('/echo', dimensions: %w[date location]) do |req|
req.options.params_encoder = Faraday::FlatParamsEncoder
end
expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded')
expected = { 'dimensions' => %w[date location] }
expect(Faraday::Utils.parse_query(response.body)).to eq(expected)
expect(response.body).to eq('dimensions=date&dimensions=location')
end
it 'works with unicode' do
err = capture_warnings do
response = conn.post('/echo', str: 'eé cç aã aâ')
expect(response.body).to eq('str=e%C3%A9+c%C3%A7+a%C3%A3+a%C3%A2')
end
expect(err.empty?).to be_truthy
end
it 'works with nested keys' do
response = conn.post('/echo', 'a' => { 'b' => { 'c' => ['d'] } })
expect(response.body).to eq('a%5Bb%5D%5Bc%5D%5B%5D=d')
end
it 'works with files' do
response = conn.post('/echo', StringIO.new('str=apple'))
expect(response.body).to eq('str=apple')
end
context 'customising default_space_encoding' do
around do |example|
Faraday::Utils.default_space_encoding = '%20'
example.run
Faraday::Utils.default_space_encoding = nil
end
it 'uses the custom character to encode spaces' do
response = conn.post('/echo', str: 'apple banana')
expect(response.body).to eq('str=apple%20banana')
end
end
end
@@ -0,0 +1,110 @@
# frozen_string_literal: true
RSpec.describe Faraday::Request do
let(:conn) do
Faraday.new(url: 'http://httpbingo.org/api',
headers: { 'Mime-Version' => '1.0' },
request: { oauth: { consumer_key: 'anonymous' } })
end
let(:http_method) { :get }
let(:block) { nil }
subject { conn.build_request(http_method, &block) }
context 'when nothing particular is configured' do
it { expect(subject.http_method).to eq(:get) }
it { expect(subject.to_env(conn).ssl.verify).to be_falsey }
it { expect(subject.to_env(conn).ssl.verify_hostname).to be_falsey }
end
context 'when HTTP method is post' do
let(:http_method) { :post }
it { expect(subject.http_method).to eq(:post) }
end
context 'when setting the url on setup with a URI' do
let(:block) { proc { |req| req.url URI.parse('foo.json?a=1') } }
it { expect(subject.path).to eq(URI.parse('foo.json')) }
it { expect(subject.params).to eq('a' => '1') }
it { expect(subject.to_env(conn).url.to_s).to eq('http://httpbingo.org/api/foo.json?a=1') }
end
context 'when setting the url on setup with a string path and params' do
let(:block) { proc { |req| req.url 'foo.json', 'a' => 1 } }
it { expect(subject.path).to eq('foo.json') }
it { expect(subject.params).to eq('a' => 1) }
it { expect(subject.to_env(conn).url.to_s).to eq('http://httpbingo.org/api/foo.json?a=1') }
end
context 'when setting the url on setup with a path including params' do
let(:block) { proc { |req| req.url 'foo.json?b=2&a=1#qqq' } }
it { expect(subject.path).to eq('foo.json') }
it { expect(subject.params).to eq('a' => '1', 'b' => '2') }
it { expect(subject.to_env(conn).url.to_s).to eq('http://httpbingo.org/api/foo.json?a=1&b=2') }
end
context 'when setting a header on setup with []= syntax' do
let(:block) { proc { |req| req['Server'] = 'Faraday' } }
let(:headers) { subject.to_env(conn).request_headers }
it { expect(subject.headers['Server']).to eq('Faraday') }
it { expect(headers['mime-version']).to eq('1.0') }
it { expect(headers['server']).to eq('Faraday') }
end
context 'when setting the body on setup' do
let(:block) { proc { |req| req.body = 'hi' } }
it { expect(subject.body).to eq('hi') }
it { expect(subject.to_env(conn).body).to eq('hi') }
end
context 'with global request options set' do
let(:env_request) { subject.to_env(conn).request }
before do
conn.options.timeout = 3
conn.options.open_timeout = 5
conn.ssl.verify = false
conn.proxy = 'http://proxy.com'
end
it { expect(subject.options.timeout).to eq(3) }
it { expect(subject.options.open_timeout).to eq(5) }
it { expect(env_request.timeout).to eq(3) }
it { expect(env_request.open_timeout).to eq(5) }
context 'and per-request options set' do
let(:block) do
proc do |req|
req.options.timeout = 10
req.options.boundary = 'boo'
req.options.oauth[:consumer_secret] = 'xyz'
req.options.context = {
foo: 'foo',
bar: 'bar'
}
end
end
it { expect(subject.options.timeout).to eq(10) }
it { expect(subject.options.open_timeout).to eq(5) }
it { expect(env_request.timeout).to eq(10) }
it { expect(env_request.open_timeout).to eq(5) }
it { expect(env_request.boundary).to eq('boo') }
it { expect(env_request.context).to eq(foo: 'foo', bar: 'bar') }
it do
oauth_expected = { consumer_secret: 'xyz', consumer_key: 'anonymous' }
expect(env_request.oauth).to eq(oauth_expected)
end
end
end
it 'supports marshal serialization' do
expect(Marshal.load(Marshal.dump(subject))).to eq(subject)
end
end
@@ -0,0 +1,189 @@
# frozen_string_literal: true
RSpec.describe Faraday::Response::Json, type: :response do
let(:options) { {} }
let(:headers) { {} }
let(:middleware) do
described_class.new(lambda { |env|
Faraday::Response.new(env)
}, **options)
end
def process(body, content_type = 'application/json', options = {})
env = {
body: body, request: options,
request_headers: Faraday::Utils::Headers.new,
response_headers: Faraday::Utils::Headers.new(headers)
}
env[:response_headers]['content-type'] = content_type if content_type
yield(env) if block_given?
middleware.call(Faraday::Env.from(env))
end
context 'no type matching' do
it "doesn't change nil body" do
expect(process(nil).body).to be_nil
end
it 'nullifies empty body' do
expect(process('').body).to be_nil
end
it 'parses json body' do
response = process('{"a":1}')
expect(response.body).to eq('a' => 1)
expect(response.env[:raw_body]).to be_nil
end
end
context 'with preserving raw' do
let(:options) { { preserve_raw: true } }
it 'parses json body' do
response = process('{"a":1}')
expect(response.body).to eq('a' => 1)
expect(response.env[:raw_body]).to eq('{"a":1}')
end
end
context 'with default regexp type matching' do
it 'parses json body of correct type' do
response = process('{"a":1}', 'application/x-json')
expect(response.body).to eq('a' => 1)
end
it 'ignores json body of incorrect type' do
response = process('{"a":1}', 'text/json-xml')
expect(response.body).to eq('{"a":1}')
end
end
context 'with array type matching' do
let(:options) { { content_type: %w[a/b c/d] } }
it 'parses json body of correct type' do
expect(process('{"a":1}', 'a/b').body).to be_a(Hash)
expect(process('{"a":1}', 'c/d').body).to be_a(Hash)
end
it 'ignores json body of incorrect type' do
expect(process('{"a":1}', 'a/d').body).not_to be_a(Hash)
end
end
it 'chokes on invalid json' do
expect { process('{!') }.to raise_error(Faraday::ParsingError)
end
it 'includes the response on the ParsingError instance' do
process('{') { |env| env[:response] = Faraday::Response.new }
raise 'Parsing should have failed.'
rescue Faraday::ParsingError => e
expect(e.response).to be_a(Faraday::Response)
end
context 'HEAD responses' do
it "nullifies the body if it's only one space" do
response = process(' ')
expect(response.body).to be_nil
end
it "nullifies the body if it's two spaces" do
response = process(' ')
expect(response.body).to be_nil
end
end
context 'JSON options' do
let(:body) { '{"a": 1}' }
let(:result) { { a: 1 } }
let(:options) do
{
parser_options: {
symbolize_names: true
}
}
end
it 'passes relevant options to JSON parse' do
expect(::JSON).to receive(:parse)
.with(body, options[:parser_options])
.and_return(result)
response = process(body)
expect(response.body).to eq(result)
end
end
context 'with decoder' do
let(:decoder) do
double('Decoder').tap do |e|
allow(e).to receive(:load) { |s, opts| JSON.parse(s, opts) }
end
end
let(:body) { '{"a": 1}' }
let(:result) { { a: 1 } }
context 'when decoder is passed as object' do
let(:options) do
{
parser_options: {
decoder: decoder,
option: :option_value,
symbolize_names: true
}
}
end
it 'passes relevant options to specified decoder\'s load method' do
expect(decoder).to receive(:load)
.with(body, { option: :option_value, symbolize_names: true })
.and_return(result)
response = process(body)
expect(response.body).to eq(result)
end
end
context 'when decoder is passed as an object-method pair' do
let(:options) do
{
parser_options: {
decoder: [decoder, :load],
option: :option_value,
symbolize_names: true
}
}
end
it 'passes relevant options to specified decoder\'s method' do
expect(decoder).to receive(:load)
.with(body, { option: :option_value, symbolize_names: true })
.and_return(result)
response = process(body)
expect(response.body).to eq(result)
end
end
context 'when decoder is not passed' do
let(:options) do
{
parser_options: {
symbolize_names: true
}
}
end
it 'passes relevant options to JSON parse' do
expect(JSON).to receive(:parse)
.with(body, { symbolize_names: true })
.and_return(result)
response = process(body)
expect(response.body).to eq(result)
end
end
end
end
@@ -0,0 +1,258 @@
# frozen_string_literal: true
require 'stringio'
require 'logger'
RSpec.describe Faraday::Response::Logger do
let(:string_io) { StringIO.new }
let(:logger) { Logger.new(string_io) }
let(:logger_options) { {} }
let(:conn) do
rubbles = ['Barney', 'Betty', 'Bam Bam']
Faraday.new do |b|
b.response :logger, logger, logger_options do |logger|
logger.filter(/(soylent green is) (.+)/, '\1 tasty')
logger.filter(/(api_key:).*"(.+)."/, '\1[API_KEY]')
logger.filter(/(password)=(.+)/, '\1=[HIDDEN]')
end
b.adapter :test do |stubs|
stubs.get('/hello') { [200, { 'Content-Type' => 'text/html' }, 'hello'] }
stubs.post('/ohai') { [200, { 'Content-Type' => 'text/html' }, 'fred'] }
stubs.post('/ohyes') { [200, { 'Content-Type' => 'text/html' }, 'pebbles'] }
stubs.get('/rubbles') { [200, { 'Content-Type' => 'application/json' }, rubbles] }
stubs.get('/filtered_body') { [200, { 'Content-Type' => 'text/html' }, 'soylent green is people'] }
stubs.get('/filtered_headers') { [200, { 'Content-Type' => 'text/html' }, 'headers response'] }
stubs.get('/filtered_params') { [200, { 'Content-Type' => 'text/html' }, 'params response'] }
stubs.get('/filtered_url') { [200, { 'Content-Type' => 'text/html' }, 'url response'] }
stubs.get('/connection_failed') { raise Faraday::ConnectionFailed, 'Failed to open TCP connection' }
end
end
end
before do
logger.level = Logger::DEBUG
end
it 'still returns output' do
resp = conn.get '/hello', nil, accept: 'text/html'
expect(resp.body).to eq('hello')
end
context 'without configuration' do
let(:conn) do
Faraday.new do |b|
b.response :logger
b.adapter :test do |stubs|
stubs.get('/hello') { [200, { 'Content-Type' => 'text/html' }, 'hello'] }
end
end
end
it 'defaults to stdout' do
expect(Logger).to receive(:new).with($stdout).and_return(Logger.new(nil))
conn.get('/hello')
end
end
context 'with default formatter' do
let(:formatter) { instance_double(Faraday::Logging::Formatter, request: true, response: true, filter: []) }
before { allow(Faraday::Logging::Formatter).to receive(:new).and_return(formatter) }
it 'delegates logging to the formatter' do
expect(formatter).to receive(:request).with(an_instance_of(Faraday::Env))
expect(formatter).to receive(:response).with(an_instance_of(Faraday::Env))
conn.get '/hello'
end
context 'when no route' do
it 'delegates logging to the formatter' do
expect(formatter).to receive(:request).with(an_instance_of(Faraday::Env))
expect(formatter).to receive(:exception).with(an_instance_of(Faraday::Adapter::Test::Stubs::NotFound))
expect { conn.get '/noroute' }.to raise_error(Faraday::Adapter::Test::Stubs::NotFound)
end
end
end
context 'with custom formatter' do
let(:formatter_class) do
Class.new(Faraday::Logging::Formatter) do
def request(_env)
info 'Custom log formatter request'
end
def response(_env)
info 'Custom log formatter response'
end
end
end
let(:logger_options) { { formatter: formatter_class } }
it 'logs with custom formatter' do
conn.get '/hello'
expect(string_io.string).to match('Custom log formatter request')
expect(string_io.string).to match('Custom log formatter response')
end
end
it 'logs method and url' do
conn.get '/hello', nil, accept: 'text/html'
expect(string_io.string).to match('GET http:/hello')
end
it 'logs status' do
conn.get '/hello', nil, accept: 'text/html'
expect(string_io.string).to match('Status 200')
end
it 'does not log error message by default' do
expect { conn.get '/noroute' }.to raise_error(Faraday::Adapter::Test::Stubs::NotFound)
expect(string_io.string).not_to match(%(no stubbed request for get http:/noroute))
end
it 'logs request headers by default' do
conn.get '/hello', nil, accept: 'text/html'
expect(string_io.string).to match(%(Accept: "text/html))
end
it 'logs response headers by default' do
conn.get '/hello', nil, accept: 'text/html'
expect(string_io.string).to match(%(Content-Type: "text/html))
end
it 'does not log request body by default' do
conn.post '/ohai', 'name=Unagi', accept: 'text/html'
expect(string_io.string).not_to match(%(name=Unagi))
end
it 'does not log response body by default' do
conn.post '/ohai', 'name=Toro', accept: 'text/html'
expect(string_io.string).not_to match(%(fred))
end
it 'logs filter headers' do
conn.headers = { 'api_key' => 'ABC123' }
conn.get '/filtered_headers', nil, accept: 'text/html'
expect(string_io.string).to match(%(api_key:))
expect(string_io.string).to match(%([API_KEY]))
expect(string_io.string).not_to match(%(ABC123))
end
it 'logs filter url' do
conn.get '/filtered_url?password=hunter2', nil, accept: 'text/html'
expect(string_io.string).to match(%([HIDDEN]))
expect(string_io.string).not_to match(%(hunter2))
end
context 'when not logging request headers' do
let(:logger_options) { { headers: { request: false } } }
it 'does not log request headers if option is false' do
conn.get '/hello', nil, accept: 'text/html'
expect(string_io.string).not_to match(%(Accept: "text/html))
end
end
context 'when not logging response headers' do
let(:logger_options) { { headers: { response: false } } }
it 'does not log response headers if option is false' do
conn.get '/hello', nil, accept: 'text/html'
expect(string_io.string).not_to match(%(Content-Type: "text/html))
end
end
context 'when logging request body' do
let(:logger_options) { { bodies: { request: true } } }
it 'log only request body' do
conn.post '/ohyes', 'name=Tamago', accept: 'text/html'
expect(string_io.string).to match(%(name=Tamago))
expect(string_io.string).not_to match(%(pebbles))
end
end
context 'when logging response body' do
let(:logger_options) { { bodies: { response: true } } }
it 'log only response body' do
conn.post '/ohyes', 'name=Hamachi', accept: 'text/html'
expect(string_io.string).to match(%(pebbles))
expect(string_io.string).not_to match(%(name=Hamachi))
end
end
context 'when logging request and response bodies' do
let(:logger_options) { { bodies: true } }
it 'log request and response body' do
conn.post '/ohyes', 'name=Ebi', accept: 'text/html'
expect(string_io.string).to match(%(name=Ebi))
expect(string_io.string).to match(%(pebbles))
end
it 'log response body object' do
conn.get '/rubbles', nil, accept: 'text/html'
expect(string_io.string).to match(%([\"Barney\", \"Betty\", \"Bam Bam\"]\n))
end
it 'logs filter body' do
conn.get '/filtered_body', nil, accept: 'text/html'
expect(string_io.string).to match(%(soylent green is))
expect(string_io.string).to match(%(tasty))
expect(string_io.string).not_to match(%(people))
end
end
context 'when logging errors' do
let(:logger_options) { { errors: true } }
it 'logs error message' do
expect { conn.get '/noroute' }.to raise_error(Faraday::Adapter::Test::Stubs::NotFound)
expect(string_io.string).to match(%(no stubbed request for get http:/noroute))
end
end
context 'when logging headers and errors' do
let(:logger_options) { { headers: true, errors: true } }
it 'logs error message' do
expect { conn.get '/connection_failed' }.to raise_error(Faraday::ConnectionFailed)
expect(string_io.string).to match(%(Failed to open TCP connection))
end
end
context 'when using log_level' do
let(:logger_options) { { bodies: true, log_level: :debug } }
it 'logs request/request body on the specified level (debug)' do
logger.level = Logger::DEBUG
conn.post '/ohyes', 'name=Ebi', accept: 'text/html'
expect(string_io.string).to match(%(name=Ebi))
expect(string_io.string).to match(%(pebbles))
end
it 'logs headers on the debug level' do
logger.level = Logger::DEBUG
conn.get '/hello', nil, accept: 'text/html'
expect(string_io.string).to match(%(Content-Type: "text/html))
end
it 'does not log request/response body on the info level' do
logger.level = Logger::INFO
conn.post '/ohyes', 'name=Ebi', accept: 'text/html'
expect(string_io.string).not_to match(%(name=Ebi))
expect(string_io.string).not_to match(%(pebbles))
end
it 'does not log headers on the info level' do
logger.level = Logger::INFO
conn.get '/hello', nil, accept: 'text/html'
expect(string_io.string).not_to match(%(Content-Type: "text/html))
end
end
end
@@ -0,0 +1,211 @@
# frozen_string_literal: true
RSpec.describe Faraday::Response::RaiseError do
let(:conn) do
Faraday.new do |b|
b.response :raise_error
b.adapter :test do |stub|
stub.get('ok') { [200, { 'Content-Type' => 'text/html' }, '<body></body>'] }
stub.get('bad-request') { [400, { 'X-Reason' => 'because' }, 'keep looking'] }
stub.get('unauthorized') { [401, { 'X-Reason' => 'because' }, 'keep looking'] }
stub.get('forbidden') { [403, { 'X-Reason' => 'because' }, 'keep looking'] }
stub.get('not-found') { [404, { 'X-Reason' => 'because' }, 'keep looking'] }
stub.get('proxy-error') { [407, { 'X-Reason' => 'because' }, 'keep looking'] }
stub.get('request-timeout') { [408, { 'X-Reason' => 'because' }, 'keep looking'] }
stub.get('conflict') { [409, { 'X-Reason' => 'because' }, 'keep looking'] }
stub.get('unprocessable-entity') { [422, { 'X-Reason' => 'because' }, 'keep looking'] }
stub.get('too-many-requests') { [429, { 'X-Reason' => 'because' }, 'keep looking'] }
stub.get('4xx') { [499, { 'X-Reason' => 'because' }, 'keep looking'] }
stub.get('nil-status') { [nil, { 'X-Reason' => 'nil' }, 'fail'] }
stub.get('server-error') { [500, { 'X-Error' => 'bailout' }, 'fail'] }
end
end
end
it 'raises no exception for 200 responses' do
expect { conn.get('ok') }.not_to raise_error
end
it 'raises Faraday::BadRequestError for 400 responses' do
expect { conn.get('bad-request') }.to raise_error(Faraday::BadRequestError) do |ex|
expect(ex.message).to eq('the server responded with status 400')
expect(ex.response[:headers]['X-Reason']).to eq('because')
expect(ex.response[:status]).to eq(400)
expect(ex.response_status).to eq(400)
expect(ex.response_body).to eq('keep looking')
expect(ex.response_headers['X-Reason']).to eq('because')
end
end
it 'raises Faraday::UnauthorizedError for 401 responses' do
expect { conn.get('unauthorized') }.to raise_error(Faraday::UnauthorizedError) do |ex|
expect(ex.message).to eq('the server responded with status 401')
expect(ex.response[:headers]['X-Reason']).to eq('because')
expect(ex.response[:status]).to eq(401)
expect(ex.response_status).to eq(401)
expect(ex.response_body).to eq('keep looking')
expect(ex.response_headers['X-Reason']).to eq('because')
end
end
it 'raises Faraday::ForbiddenError for 403 responses' do
expect { conn.get('forbidden') }.to raise_error(Faraday::ForbiddenError) do |ex|
expect(ex.message).to eq('the server responded with status 403')
expect(ex.response[:headers]['X-Reason']).to eq('because')
expect(ex.response[:status]).to eq(403)
expect(ex.response_status).to eq(403)
expect(ex.response_body).to eq('keep looking')
expect(ex.response_headers['X-Reason']).to eq('because')
end
end
it 'raises Faraday::ResourceNotFound for 404 responses' do
expect { conn.get('not-found') }.to raise_error(Faraday::ResourceNotFound) do |ex|
expect(ex.message).to eq('the server responded with status 404')
expect(ex.response[:headers]['X-Reason']).to eq('because')
expect(ex.response[:status]).to eq(404)
expect(ex.response_status).to eq(404)
expect(ex.response_body).to eq('keep looking')
expect(ex.response_headers['X-Reason']).to eq('because')
end
end
it 'raises Faraday::ProxyAuthError for 407 responses' do
expect { conn.get('proxy-error') }.to raise_error(Faraday::ProxyAuthError) do |ex|
expect(ex.message).to eq('407 "Proxy Authentication Required"')
expect(ex.response[:headers]['X-Reason']).to eq('because')
expect(ex.response[:status]).to eq(407)
expect(ex.response_status).to eq(407)
expect(ex.response_body).to eq('keep looking')
expect(ex.response_headers['X-Reason']).to eq('because')
end
end
it 'raises Faraday::RequestTimeoutError for 408 responses' do
expect { conn.get('request-timeout') }.to raise_error(Faraday::RequestTimeoutError) do |ex|
expect(ex.message).to eq('the server responded with status 408')
expect(ex.response[:headers]['X-Reason']).to eq('because')
expect(ex.response[:status]).to eq(408)
expect(ex.response_status).to eq(408)
expect(ex.response_body).to eq('keep looking')
expect(ex.response_headers['X-Reason']).to eq('because')
end
end
it 'raises Faraday::ConflictError for 409 responses' do
expect { conn.get('conflict') }.to raise_error(Faraday::ConflictError) do |ex|
expect(ex.message).to eq('the server responded with status 409')
expect(ex.response[:headers]['X-Reason']).to eq('because')
expect(ex.response[:status]).to eq(409)
expect(ex.response_status).to eq(409)
expect(ex.response_body).to eq('keep looking')
expect(ex.response_headers['X-Reason']).to eq('because')
end
end
it 'raises Faraday::UnprocessableEntityError for 422 responses' do
expect { conn.get('unprocessable-entity') }.to raise_error(Faraday::UnprocessableEntityError) do |ex|
expect(ex.message).to eq('the server responded with status 422')
expect(ex.response[:headers]['X-Reason']).to eq('because')
expect(ex.response[:status]).to eq(422)
expect(ex.response_status).to eq(422)
expect(ex.response_body).to eq('keep looking')
expect(ex.response_headers['X-Reason']).to eq('because')
end
end
it 'raises Faraday::TooManyRequestsError for 429 responses' do
expect { conn.get('too-many-requests') }.to raise_error(Faraday::TooManyRequestsError) do |ex|
expect(ex.message).to eq('the server responded with status 429')
expect(ex.response[:headers]['X-Reason']).to eq('because')
expect(ex.response[:status]).to eq(429)
expect(ex.response_status).to eq(429)
expect(ex.response_body).to eq('keep looking')
expect(ex.response_headers['X-Reason']).to eq('because')
end
end
it 'raises Faraday::NilStatusError for nil status in response' do
expect { conn.get('nil-status') }.to raise_error(Faraday::NilStatusError) do |ex|
expect(ex.message).to eq('http status could not be derived from the server response')
expect(ex.response[:headers]['X-Reason']).to eq('nil')
expect(ex.response[:status]).to be_nil
expect(ex.response_status).to be_nil
expect(ex.response_body).to eq('fail')
expect(ex.response_headers['X-Reason']).to eq('nil')
end
end
it 'raises Faraday::ClientError for other 4xx responses' do
expect { conn.get('4xx') }.to raise_error(Faraday::ClientError) do |ex|
expect(ex.message).to eq('the server responded with status 499')
expect(ex.response[:headers]['X-Reason']).to eq('because')
expect(ex.response[:status]).to eq(499)
expect(ex.response_status).to eq(499)
expect(ex.response_body).to eq('keep looking')
expect(ex.response_headers['X-Reason']).to eq('because')
end
end
it 'raises Faraday::ServerError for 500 responses' do
expect { conn.get('server-error') }.to raise_error(Faraday::ServerError) do |ex|
expect(ex.message).to eq('the server responded with status 500')
expect(ex.response[:headers]['X-Error']).to eq('bailout')
expect(ex.response[:status]).to eq(500)
expect(ex.response_status).to eq(500)
expect(ex.response_body).to eq('fail')
expect(ex.response_headers['X-Error']).to eq('bailout')
end
end
describe 'request info' do
let(:conn) do
Faraday.new do |b|
b.response :raise_error, **middleware_options
b.adapter :test do |stub|
stub.post(url, request_body, request_headers) do
[400, { 'X-Reason' => 'because' }, 'keep looking']
end
end
end
end
let(:middleware_options) { {} }
let(:request_body) { JSON.generate({ 'item' => 'sth' }) }
let(:request_headers) { { 'Authorization' => 'Basic 123' } }
let(:url_path) { 'request' }
let(:query_params) { 'full=true' }
let(:url) { "#{url_path}?#{query_params}" }
subject(:perform_request) do
conn.post url do |req|
req.headers['Authorization'] = 'Basic 123'
req.body = request_body
end
end
it 'returns the request info in the exception' do
expect { perform_request }.to raise_error(Faraday::BadRequestError) do |ex|
expect(ex.response[:request][:method]).to eq(:post)
expect(ex.response[:request][:url]).to eq(URI("http:/#{url}"))
expect(ex.response[:request][:url_path]).to eq("/#{url_path}")
expect(ex.response[:request][:params]).to eq({ 'full' => 'true' })
expect(ex.response[:request][:headers]).to match(a_hash_including(request_headers))
expect(ex.response[:request][:body]).to eq(request_body)
end
end
context 'when the include_request option is set to false' do
let(:middleware_options) { { include_request: false } }
it 'does not include request info in the exception' do
expect { perform_request }.to raise_error(Faraday::BadRequestError) do |ex|
expect(ex.response.keys).to contain_exactly(
:status,
:headers,
:body
)
end
end
end
end
end
@@ -0,0 +1,77 @@
# frozen_string_literal: true
RSpec.describe Faraday::Response do
subject { Faraday::Response.new(env) }
let(:env) do
Faraday::Env.from(status: 404, body: 'yikes', url: Faraday::Utils.URI('https://lostisland.github.io/faraday'),
response_headers: { 'Content-Type' => 'text/plain' })
end
it { expect(subject.finished?).to be_truthy }
it { expect { subject.finish({}) }.to raise_error(RuntimeError) }
it { expect(subject.success?).to be_falsey }
it { expect(subject.status).to eq(404) }
it { expect(subject.body).to eq('yikes') }
it { expect(subject.headers['Content-Type']).to eq('text/plain') }
it { expect(subject['content-type']).to eq('text/plain') }
describe '#apply_request' do
before { subject.apply_request(body: 'a=b', method: :post) }
it { expect(subject.body).to eq('yikes') }
it { expect(subject.env[:method]).to eq(:post) }
end
describe '#to_hash' do
let(:hash) { subject.to_hash }
it { expect(hash).to be_a(Hash) }
it { expect(hash[:status]).to eq(subject.status) }
it { expect(hash[:response_headers]).to eq(subject.headers) }
it { expect(hash[:body]).to eq(subject.body) }
it { expect(hash[:url]).to eq(subject.env.url) }
end
describe 'marshal serialization support' do
subject { Faraday::Response.new }
let(:loaded) { Marshal.load(Marshal.dump(subject)) }
before do
subject.on_complete {}
subject.finish(env.merge(params: 'moo'))
end
it { expect(loaded.env[:params]).to be_nil }
it { expect(loaded.env[:body]).to eq(env[:body]) }
it { expect(loaded.env[:response_headers]).to eq(env[:response_headers]) }
it { expect(loaded.env[:status]).to eq(env[:status]) }
it { expect(loaded.env[:url]).to eq(env[:url]) }
end
describe '#on_complete' do
subject { Faraday::Response.new }
it 'parse body on finish' do
subject.on_complete { |env| env[:body] = env[:body].upcase }
subject.finish(env)
expect(subject.body).to eq('YIKES')
end
it 'can access response body in on_complete callback' do
subject.on_complete { |env| env[:body] = subject.body.upcase }
subject.finish(env)
expect(subject.body).to eq('YIKES')
end
it 'can access response body in on_complete callback' do
callback_env = nil
subject.on_complete { |env| callback_env = env }
subject.finish({})
expect(subject.env).to eq(callback_env)
end
end
end
@@ -0,0 +1,100 @@
# frozen_string_literal: true
RSpec.describe Faraday::Utils::Headers do
subject { Faraday::Utils::Headers.new }
context 'when Content-Type is set to application/json' do
before { subject['Content-Type'] = 'application/json' }
it { expect(subject.keys).to eq(['Content-Type']) }
it { expect(subject['Content-Type']).to eq('application/json') }
it { expect(subject['CONTENT-TYPE']).to eq('application/json') }
it { expect(subject['content-type']).to eq('application/json') }
it { is_expected.to include('content-type') }
end
context 'when Content-Type is set to application/xml' do
before { subject['Content-Type'] = 'application/xml' }
it { expect(subject.keys).to eq(['Content-Type']) }
it { expect(subject['Content-Type']).to eq('application/xml') }
it { expect(subject['CONTENT-TYPE']).to eq('application/xml') }
it { expect(subject['content-type']).to eq('application/xml') }
it { is_expected.to include('content-type') }
end
describe '#fetch' do
before { subject['Content-Type'] = 'application/json' }
it { expect(subject.fetch('Content-Type')).to eq('application/json') }
it { expect(subject.fetch('CONTENT-TYPE')).to eq('application/json') }
it { expect(subject.fetch(:content_type)).to eq('application/json') }
it { expect(subject.fetch('invalid', 'default')).to eq('default') }
it { expect(subject.fetch('invalid', false)).to eq(false) }
it { expect(subject.fetch('invalid', nil)).to be_nil }
it { expect(subject.fetch('Invalid') { |key| "#{key} key" }).to eq('Invalid key') }
it 'calls a block when provided' do
block_called = false
expect(subject.fetch('content-type') { block_called = true }).to eq('application/json')
expect(block_called).to be_falsey
end
it 'raises an error if key not found' do
expected_error = defined?(KeyError) ? KeyError : IndexError
expect { subject.fetch('invalid') }.to raise_error(expected_error)
end
end
describe '#delete' do
before do
subject['Content-Type'] = 'application/json'
@deleted = subject.delete('content-type')
end
it { expect(@deleted).to eq('application/json') }
it { expect(subject.size).to eq(0) }
it { is_expected.not_to include('content-type') }
it { expect(subject.delete('content-type')).to be_nil }
end
describe '#parse' do
context 'when response headers leave http status line out' do
let(:headers) { "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n" }
before { subject.parse(headers) }
it { expect(subject.keys).to eq(%w[Content-Type]) }
it { expect(subject['Content-Type']).to eq('text/html') }
it { expect(subject['content-type']).to eq('text/html') }
end
context 'when response headers values include a colon' do
let(:headers) { "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nLocation: http://httpbingo.org/\r\n\r\n" }
before { subject.parse(headers) }
it { expect(subject['location']).to eq('http://httpbingo.org/') }
end
context 'when response headers include a blank line' do
let(:headers) { "HTTP/1.1 200 OK\r\n\r\nContent-Type: text/html\r\n\r\n" }
before { subject.parse(headers) }
it { expect(subject['content-type']).to eq('text/html') }
end
context 'when response headers include already stored keys' do
let(:headers) { "HTTP/1.1 200 OK\r\nX-Numbers: 123\r\n\r\n" }
before do
h = subject
h[:x_numbers] = 8
h.parse(headers)
end
it do
expect(subject[:x_numbers]).to eq('8, 123')
end
end
end
end
@@ -0,0 +1,118 @@
# frozen_string_literal: true
RSpec.describe Faraday::Utils do
describe 'headers parsing' do
let(:multi_response_headers) do
"HTTP/1.x 500 OK\r\nContent-Type: text/html; charset=UTF-8\r\n" \
"HTTP/1.x 200 OK\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n"
end
it 'parse headers for aggregated responses' do
headers = Faraday::Utils::Headers.new
headers.parse(multi_response_headers)
result = headers.to_hash
expect(result['Content-Type']).to eq('application/json; charset=UTF-8')
end
end
describe 'URI parsing' do
let(:url) { 'http://example.com/abc' }
it 'escapes safe buffer' do
str = FakeSafeBuffer.new('$32,000.00')
expect(Faraday::Utils.escape(str)).to eq('%2432%2C000.00')
end
it 'parses with default parser' do
with_default_uri_parser(nil) do
uri = normalize(url)
expect(uri.host).to eq('example.com')
end
end
it 'parses with URI' do
with_default_uri_parser(::URI) do
uri = normalize(url)
expect(uri.host).to eq('example.com')
end
end
it 'parses with block' do
with_default_uri_parser(->(u) { "booya#{'!' * u.size}" }) do
expect(normalize(url)).to eq('booya!!!!!!!!!!!!!!!!!!!!!!')
end
end
it 'replaces headers hash' do
headers = Faraday::Utils::Headers.new('authorization' => 't0ps3cr3t!')
expect(headers).to have_key('authorization')
headers.replace('content-type' => 'text/plain')
expect(headers).not_to have_key('authorization')
end
end
describe '.deep_merge!' do
let(:connection_options) { Faraday::ConnectionOptions.new }
let(:url) do
{
url: 'http://example.com/abc',
headers: { 'Mime-Version' => '1.0' },
request: { oauth: { consumer_key: 'anonymous' } },
ssl: { version: '2' }
}
end
it 'recursively merges the headers' do
connection_options.headers = { user_agent: 'My Agent 1.0' }
deep_merge = Faraday::Utils.deep_merge!(connection_options, url)
expect(deep_merge.headers).to eq('Mime-Version' => '1.0', user_agent: 'My Agent 1.0')
end
context 'when a target hash has an Options Struct value' do
let(:request) do
{
params_encoder: nil,
proxy: nil,
bind: nil,
timeout: nil,
open_timeout: nil,
read_timeout: nil,
write_timeout: nil,
boundary: nil,
oauth: { consumer_key: 'anonymous' },
context: nil,
on_data: nil
}
end
let(:ssl) do
{
verify: nil,
ca_file: nil,
ca_path: nil,
verify_mode: nil,
cert_store: nil,
client_cert: nil,
client_key: nil,
certificate: nil,
private_key: nil,
verify_depth: nil,
version: '2',
min_version: nil,
max_version: nil,
verify_hostname: nil
}
end
it 'does not overwrite an Options Struct value' do
deep_merge = Faraday::Utils.deep_merge!(connection_options, url)
expect(deep_merge.request.to_h).to eq(request)
expect(deep_merge.ssl.to_h).to eq(ssl)
end
end
end
end
@@ -0,0 +1,41 @@
# frozen_string_literal: true
RSpec.describe Faraday do
it 'has a version number' do
expect(Faraday::VERSION).not_to be nil
end
context 'proxies to default_connection' do
let(:mock_connection) { double('Connection') }
before do
Faraday.default_connection = mock_connection
end
it 'proxies methods that exist on the default_connection' do
expect(mock_connection).to receive(:this_should_be_proxied)
Faraday.this_should_be_proxied
end
it 'uses method_missing on Faraday if there is no proxyable method' do
expected_message =
if RUBY_VERSION >= '3.3'
"undefined method `this_method_does_not_exist' for module Faraday"
else
"undefined method `this_method_does_not_exist' for Faraday:Module"
end
expect { Faraday.this_method_does_not_exist }.to raise_error(NoMethodError, expected_message)
end
it 'proxied methods can be accessed' do
allow(mock_connection).to receive(:this_should_be_proxied)
expect(Faraday.method(:this_should_be_proxied)).to be_a(Method)
end
after do
Faraday.default_connection = nil
end
end
end
+133
View File
@@ -0,0 +1,133 @@
# frozen_string_literal: true
# This file was generated by the `rspec --init` command. Conventionally, all
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
# The generated `.rspec` file contains `--require spec_helper` which will cause
# this file to always be loaded, without a need to explicitly require it in any
# files.
#
# Given that it is always loaded, you are encouraged to keep this file as
# light-weight as possible. Requiring heavyweight dependencies from this file
# will add to the boot time of your test suite on EVERY test run, even for an
# individual file that may not need all of that loaded. Instead, consider making
# a separate helper file that requires the additional dependencies and performs
# the additional setup, and require it from the spec files that actually need
# it.
#
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
require 'simplecov'
require 'coveralls'
require 'webmock/rspec'
WebMock.disable_net_connect!(allow_localhost: true)
SimpleCov.formatters = [SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter]
SimpleCov.start do
add_filter '/spec/'
minimum_coverage 84
minimum_coverage_by_file 26
end
require 'faraday'
require 'pry'
# Ensure all /lib files are loaded
# so they will be included in the test coverage report.
Dir['./lib/**/*.rb'].each { |file| require file }
# Load all Rspec support files
Dir['./spec/support/**/*.rb'].each { |file| require file }
RSpec.configure do |config|
# rspec-expectations config goes here. You can use an alternate
# assertion/expectation library such as wrong or the stdlib/minitest
# assertions if you prefer.
config.expect_with :rspec do |expectations|
# This option will default to `true` in RSpec 4. It makes the `description`
# and `failure_message` of custom matchers include text for helper methods
# defined using `chain`, e.g.:
# be_bigger_than(2).and_smaller_than(4).description
# # => "be bigger than 2 and smaller than 4"
# ...rather than:
# # => "be bigger than 2"
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
# rspec-mocks config goes here. You can use an alternate test double
# library (such as bogus or mocha) by changing the `mock_with` option here.
config.mock_with :rspec do |mocks|
# Prevents you from mocking or stubbing a method that does not exist on
# a real object. This is generally recommended, and will default to
# `true` in RSpec 4.
mocks.verify_partial_doubles = true
end
# This option will default to `:apply_to_host_groups` in RSpec 4 (and will
# have no way to turn it off -- the option exists only for backwards
# compatibility in RSpec 3). It causes shared context metadata to be
# inherited by the metadata hash of host groups and examples, rather than
# triggering implicit auto-inclusion in groups with matching metadata.
config.shared_context_metadata_behavior = :apply_to_host_groups
# This allows you to limit a spec run to individual examples or groups
# you care about by tagging them with `:focus` metadata. When nothing
# is tagged with `:focus`, all examples get run. RSpec also provides
# aliases for `it`, `describe`, and `context` that include `:focus`
# metadata: `fit`, `fdescribe` and `fcontext`, respectively.
# config.filter_run_when_matching :focus
# Allows RSpec to persist some state between runs in order to support
# the `--only-failures` and `--next-failure` CLI options. We recommend
# you configure your source control system to ignore this file.
# config.example_status_persistence_file_path = "spec/examples.txt"
# Limits the available syntax to the non-monkey patched syntax that is
# recommended. For more details, see:
# - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
# - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
# - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
# config.disable_monkey_patching!
# This setting enables warnings. It's recommended, but in some cases may
# be too noisy due to issues in dependencies.
# config.warnings = true
# Many RSpec users commonly either run the entire suite or an individual
# file, and it's useful to allow more verbose output when running an
# individual spec file.
# if config.files_to_run.one?
# # Use the documentation formatter for detailed output,
# # unless a formatter has already been configured
# # (e.g. via a command-line flag).
# config.default_formatter = "doc"
# end
# Print the 10 slowest examples and example groups at the
# end of the spec run, to help surface which specs are running
# particularly slow.
# config.profile_examples = 10
# Run specs in random order to surface order dependencies. If you find an
# order dependency and want to debug it, you can fix the order by providing
# the seed, which is printed after each run.
# --seed 1234
config.order = :random
# Seed global randomization in this process using the `--seed` CLI option.
# Setting this allows you to use `--seed` to deterministically reproduce
# test failures related to randomization by passing the same `--seed` value
# as the one that triggered the failure.
Kernel.srand config.seed
config.include Faraday::HelperMethods
end
# Extends RSpec DocumentationFormatter to hide skipped tests.
module FormatterOverrides
def example_pending(_arg); end
def dump_pending(_arg); end
RSpec::Core::Formatters::DocumentationFormatter.prepend self
end
@@ -0,0 +1,14 @@
# frozen_string_literal: true
# Allows to disable WebMock stubs
module DisablingStub
def disable
@disabled = true
end
def disabled?
@disabled
end
WebMock::RequestStub.prepend self
end
@@ -0,0 +1,15 @@
# frozen_string_literal: true
# emulates ActiveSupport::SafeBuffer#gsub
FakeSafeBuffer = Struct.new(:string) do
def to_s
self
end
def gsub(regex)
string.gsub(regex) do
match, = Regexp.last_match(0), '' =~ /a/ # rubocop:disable Performance/StringInclude
yield(match)
end
end
end
@@ -0,0 +1,96 @@
# frozen_string_literal: true
module Faraday
module HelperMethods
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def features(*features)
@features = features
end
def on_feature(name)
yield if block_given? && feature?(name)
end
def feature?(name)
if @features.nil?
superclass.feature?(name) if superclass.respond_to?(:feature?)
elsif @features.include?(name)
true
end
end
def method_with_body?(method)
METHODS_WITH_BODY.include?(method.to_s)
end
end
def ssl_mode?
ENV['SSL'] == 'yes'
end
def normalize(url)
Faraday::Utils::URI(url)
end
def with_default_uri_parser(parser)
old_parser = Faraday::Utils.default_uri_parser
begin
Faraday::Utils.default_uri_parser = parser
yield
ensure
Faraday::Utils.default_uri_parser = old_parser
end
end
def with_env(new_env)
old_env = {}
new_env.each do |key, value|
old_env[key] = ENV.fetch(key, false)
ENV[key] = value
end
begin
yield
ensure
old_env.each do |key, value|
value == false ? ENV.delete(key) : ENV[key] = value
end
end
end
def with_env_proxy_disabled
Faraday.ignore_env_proxy = true
begin
yield
ensure
Faraday.ignore_env_proxy = false
end
end
def capture_warnings
old = $stderr
$stderr = StringIO.new
begin
yield
$stderr.string
ensure
$stderr = old
end
end
def method_with_body?(method)
self.class.method_with_body?(method)
end
def big_string
kb = 1024
(32..126).map(&:chr).cycle.take(50 * kb).join
end
end
end
@@ -0,0 +1,105 @@
# frozen_string_literal: true
shared_examples 'an adapter' do |**options|
before { skip } if options[:skip]
context 'with SSL enabled' do
before { ENV['SSL'] = 'yes' }
include_examples 'adapter examples', options
end
context 'with SSL disabled' do
before { ENV['SSL'] = 'no' }
include_examples 'adapter examples', options
end
end
shared_examples 'adapter examples' do |**options|
include Faraday::StreamingResponseChecker
let(:adapter) { described_class.name.split('::').last }
let(:conn_options) { { headers: { 'X-Faraday-Adapter' => adapter } }.merge(options[:conn_options] || {}) }
let(:adapter_options) do
return [] unless options[:adapter_options]
if options[:adapter_options].is_a?(Array)
options[:adapter_options]
else
[options[:adapter_options]]
end
end
let(:protocol) { ssl_mode? ? 'https' : 'http' }
let(:remote) { "#{protocol}://example.com" }
let(:stub_remote) { remote }
let(:conn) do
conn_options[:ssl] ||= {}
conn_options[:ssl][:ca_file] ||= ENV.fetch('SSL_FILE', nil)
conn_options[:ssl][:verify_hostname] ||= ENV['SSL_VERIFY_HOSTNAME'] == 'yes'
Faraday.new(remote, conn_options) do |conn|
conn.request :url_encoded
conn.response :raise_error
conn.adapter described_class, *adapter_options
end
end
let!(:request_stub) { stub_request(http_method, stub_remote) }
after do
expect(request_stub).to have_been_requested unless request_stub.disabled?
end
describe '#delete' do
let(:http_method) { :delete }
it_behaves_like 'a request method', :delete
end
describe '#get' do
let(:http_method) { :get }
it_behaves_like 'a request method', :get
end
describe '#head' do
let(:http_method) { :head }
it_behaves_like 'a request method', :head
end
describe '#options' do
let(:http_method) { :options }
it_behaves_like 'a request method', :options
end
describe '#patch' do
let(:http_method) { :patch }
it_behaves_like 'a request method', :patch
end
describe '#post' do
let(:http_method) { :post }
it_behaves_like 'a request method', :post
end
describe '#put' do
let(:http_method) { :put }
it_behaves_like 'a request method', :put
end
on_feature :trace_method do
describe '#trace' do
let(:http_method) { :trace }
it_behaves_like 'a request method', :trace
end
end
end
@@ -0,0 +1,18 @@
# frozen_string_literal: true
shared_examples 'a params encoder' do
it 'escapes safe buffer' do
monies = FakeSafeBuffer.new('$32,000.00')
expect(subject.encode('a' => monies)).to eq('a=%2432%2C000.00')
end
it 'raises type error for empty string' do
expect { subject.encode('') }.to raise_error(TypeError) do |error|
expect(error.message).to eq("Can't convert String into Hash.")
end
end
it 'encodes nil' do
expect(subject.encode('a' => nil)).to eq('a')
end
end
@@ -0,0 +1,263 @@
# frozen_string_literal: true
shared_examples 'proxy examples' do
it 'handles requests with proxy' do
res = conn.public_send(http_method, '/')
expect(res.status).to eq(200)
end
it 'handles proxy failures' do
request_stub.to_return(status: 407)
expect { conn.public_send(http_method, '/') }.to raise_error(Faraday::ProxyAuthError)
end
end
shared_examples 'a request method' do |http_method|
let(:query_or_body) { method_with_body?(http_method) ? :body : :query }
let(:response) { conn.public_send(http_method, '/') }
unless http_method == :head && feature?(:skip_response_body_on_head)
it 'retrieves the response body' do
res_body = 'test'
request_stub.to_return(body: res_body)
expect(conn.public_send(http_method, '/').body).to eq(res_body)
end
end
it 'handles headers with multiple values' do
request_stub.to_return(headers: { 'Set-Cookie' => 'name=value' })
expect(response.headers['set-cookie']).to eq('name=value')
end
it 'retrieves the response headers' do
request_stub.to_return(headers: { 'Content-Type' => 'text/plain' })
expect(response.headers['Content-Type']).to match(%r{text/plain})
expect(response.headers['content-type']).to match(%r{text/plain})
end
it 'sends user agent' do
request_stub.with(headers: { 'User-Agent' => 'Agent Faraday' })
conn.public_send(http_method, '/', nil, user_agent: 'Agent Faraday')
end
it 'represents empty body response as blank string' do
expect(response.body).to eq('')
end
it 'handles connection error' do
request_stub.disable
expect { conn.public_send(http_method, 'http://localhost:4') }.to raise_error(Faraday::ConnectionFailed)
end
on_feature :local_socket_binding do
it 'binds local socket' do
stub_request(http_method, 'http://example.com')
host = '1.2.3.4'
port = 1234
conn_options[:request] = { bind: { host: host, port: port } }
conn.public_send(http_method, '/')
expect(conn.options[:bind][:host]).to eq(host)
expect(conn.options[:bind][:port]).to eq(port)
end
end
# context 'when wrong ssl certificate is provided' do
# let(:ca_file_path) { 'tmp/faraday-different-ca-cert.crt' }
# before { conn_options.merge!(ssl: { ca_file: ca_file_path }) }
#
# it do
# expect { conn.public_send(http_method, '/') }.to raise_error(Faraday::SSLError) # do |ex|
# expect(ex.message).to include?('certificate')
# end
# end
# end
on_feature :request_body_on_query_methods do
it 'sends request body' do
request_stub.with({ body: 'test' })
res = if query_or_body == :body
conn.public_send(http_method, '/', 'test')
else
conn.public_send(http_method, '/') do |req|
req.body = 'test'
end
end
expect(res.env.request_body).to eq('test')
end
end
it 'sends url encoded parameters' do
payload = { name: 'zack' }
request_stub.with({ query_or_body => payload })
res = conn.public_send(http_method, '/', payload)
if query_or_body == :query
expect(res.env.request_body).to be_nil
else
expect(res.env.request_body).to eq('name=zack')
end
end
it 'sends url encoded nested parameters' do
payload = { name: { first: 'zack' } }
request_stub.with({ query_or_body => payload })
conn.public_send(http_method, '/', payload)
end
# TODO: This needs reimplementation: see https://github.com/lostisland/faraday/issues/718
# Should raise Faraday::TimeoutError
it 'supports timeout option' do
conn_options[:request] = { timeout: 1 }
request_stub.to_timeout
exc = adapter == 'NetHttp' ? Faraday::ConnectionFailed : Faraday::TimeoutError
expect { conn.public_send(http_method, '/') }.to raise_error(exc)
end
# TODO: This needs reimplementation: see https://github.com/lostisland/faraday/issues/718
# Should raise Faraday::ConnectionFailed
it 'supports open_timeout option' do
conn_options[:request] = { open_timeout: 1 }
request_stub.to_timeout
exc = adapter == 'NetHttp' ? Faraday::ConnectionFailed : Faraday::TimeoutError
expect { conn.public_send(http_method, '/') }.to raise_error(exc)
end
on_feature :reason_phrase_parse do
it 'parses the reason phrase' do
request_stub.to_return(status: [200, 'OK'])
expect(response.reason_phrase).to eq('OK')
end
end
on_feature :compression do
# Accept-Encoding header not sent for HEAD requests as body is not expected in the response.
unless http_method == :head
it 'handles gzip compression' do
request_stub.with(headers: { 'Accept-Encoding' => /\bgzip\b/ })
conn.public_send(http_method, '/')
end
it 'handles deflate compression' do
request_stub.with(headers: { 'Accept-Encoding' => /\bdeflate\b/ })
conn.public_send(http_method, '/')
end
end
end
on_feature :streaming do
describe 'streaming' do
let(:streamed) { [] }
context 'when response is empty' do
it 'handles streaming' do
env = nil
conn.public_send(http_method, '/') do |req|
req.options.on_data = proc do |chunk, size, block_env|
streamed << [chunk, size]
env ||= block_env
end
end
expect(streamed).to eq([['', 0]])
# TODO: enable this after updating all existing adapters to the new streaming API
# expect(env).to be_a(Faraday::Env)
# expect(env.status).to eq(200)
end
end
context 'when response contains big data' do
before { request_stub.to_return(body: big_string) }
it 'handles streaming' do
env = nil
response = conn.public_send(http_method, '/') do |req|
req.options.on_data = proc do |chunk, size, block_env|
streamed << [chunk, size]
env ||= block_env
end
end
expect(response.body).to eq('')
check_streaming_response(streamed, chunk_size: 16 * 1024)
# TODO: enable this after updating all existing adapters to the new streaming API
# expect(env).to be_a(Faraday::Env)
# expect(env.status).to eq(200)
end
end
end
end
on_feature :parallel do
context 'with parallel setup' do
before do
@resp1 = nil
@resp2 = nil
@payload1 = { a: '1' }
@payload2 = { b: '2' }
request_stub
.with({ query_or_body => @payload1 })
.to_return(body: @payload1.to_json)
stub_request(http_method, remote)
.with({ query_or_body => @payload2 })
.to_return(body: @payload2.to_json)
conn.in_parallel do
@resp1 = conn.public_send(http_method, '/', @payload1)
@resp2 = conn.public_send(http_method, '/', @payload2)
expect(conn.in_parallel?).to be_truthy
expect(@resp1.body).to be_nil
expect(@resp2.body).to be_nil
end
expect(conn.in_parallel?).to be_falsey
end
it 'handles parallel requests status' do
expect(@resp1&.status).to eq(200)
expect(@resp2&.status).to eq(200)
end
unless http_method == :head && feature?(:skip_response_body_on_head)
it 'handles parallel requests body' do
expect(@resp1&.body).to eq(@payload1.to_json)
expect(@resp2&.body).to eq(@payload2.to_json)
end
end
end
end
context 'when a proxy is provided as option' do
before do
conn_options[:proxy] = 'http://env-proxy.com:80'
end
include_examples 'proxy examples'
end
context 'when http_proxy env variable is set' do
let(:proxy_url) { 'http://env-proxy.com:80' }
around do |example|
with_env 'http_proxy' => proxy_url do
example.run
end
end
include_examples 'proxy examples'
context 'when the env proxy is ignored' do
around do |example|
with_env_proxy_disabled(&example)
end
include_examples 'proxy examples'
end
end
end
@@ -0,0 +1,35 @@
# frozen_string_literal: true
module Faraday
module StreamingResponseChecker
def check_streaming_response(streamed, options = {})
opts = {
prefix: '',
streaming?: true
}.merge(options)
expected_response = opts[:prefix] + big_string
chunks, sizes = streamed.transpose
# Check that the total size of the chunks (via the last size returned)
# is the same size as the expected_response
expect(sizes.last).to eq(expected_response.bytesize)
start_index = 0
expected_chunks = []
chunks.each do |actual_chunk|
expected_chunk = expected_response[start_index..((start_index + actual_chunk.bytesize) - 1)]
expected_chunks << expected_chunk
start_index += expected_chunk.bytesize
end
# it's easier to read a smaller portion, so we check that first
expect(expected_chunks[0][0..255]).to eq(chunks[0][0..255])
[expected_chunks, chunks].transpose.each do |expected, actual|
expect(actual).to eq(expected)
end
end
end
end