This commit is contained in:
@@ -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)
|
||||
@@ -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.
|
||||
@@ -0,0 +1,67 @@
|
||||
# [][website]
|
||||
|
||||
[](https://rubygems.org/gems/faraday)
|
||||
[](https://github.com/lostisland/faraday/actions?query=workflow%3ACI)
|
||||
[](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
|
||||
|
||||
© 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user