mirror of
https://github.com/HoneyryderChuck/httpx.git
synced 2025-07-18 00:00:50 -04:00
Compare commits
27 Commits
4292644870
...
1494ba872a
Author | SHA1 | Date | |
---|---|---|---|
|
1494ba872a | ||
|
685e6e4c7f | ||
|
085cec0c8e | ||
|
288ac05508 | ||
|
c777aa779e | ||
|
d55bfec80c | ||
|
e88956a16f | ||
|
aab30279ac | ||
|
2f9247abfb | ||
|
0d58408c58 | ||
|
3f73d2e3ce | ||
|
896914e189 | ||
|
4f587c5508 | ||
|
a9cb0a69a2 | ||
|
6baca35422 | ||
|
b4c5e75705 | ||
|
d859c3a1eb | ||
|
b7f5a3dfad | ||
|
8cd1aac99c | ||
|
f0f6b5f7e2 | ||
|
acbc22e79f | ||
|
134bef69e0 | ||
|
477c3601fc | ||
|
f0dabb9a83 | ||
|
7407adefb9 | ||
|
91bfa84c12 | ||
|
7473af6d9d |
@ -43,24 +43,6 @@ test jruby:
|
|||||||
script:
|
script:
|
||||||
./spec.sh jruby 9.0.0.0
|
./spec.sh jruby 9.0.0.0
|
||||||
allow_failure: true
|
allow_failure: true
|
||||||
test ruby 2/4:
|
|
||||||
<<: *test_settings
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
script:
|
|
||||||
./spec.sh ruby 2.4
|
|
||||||
test ruby 2/5:
|
|
||||||
<<: *test_settings
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
script:
|
|
||||||
./spec.sh ruby 2.5
|
|
||||||
test ruby 2/6:
|
|
||||||
<<: *test_settings
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
script:
|
|
||||||
./spec.sh ruby 2.6
|
|
||||||
test ruby 2/7:
|
test ruby 2/7:
|
||||||
<<: *test_settings
|
<<: *test_settings
|
||||||
script:
|
script:
|
||||||
|
@ -23,7 +23,6 @@ AllCops:
|
|||||||
- 'vendor/**/*'
|
- 'vendor/**/*'
|
||||||
- 'www/**/*'
|
- 'www/**/*'
|
||||||
- 'lib/httpx/extensions.rb'
|
- 'lib/httpx/extensions.rb'
|
||||||
- 'lib/httpx/punycode.rb'
|
|
||||||
# Do not lint ffi block, for openssl parity
|
# Do not lint ffi block, for openssl parity
|
||||||
- 'test/extensions/response_pattern_match.rb'
|
- 'test/extensions/response_pattern_match.rb'
|
||||||
|
|
||||||
|
@ -6,5 +6,4 @@ SimpleCov.start do
|
|||||||
add_filter "/integration_tests/"
|
add_filter "/integration_tests/"
|
||||||
add_filter "/regression_tests/"
|
add_filter "/regression_tests/"
|
||||||
add_filter "/lib/httpx/plugins/internal_telemetry.rb"
|
add_filter "/lib/httpx/plugins/internal_telemetry.rb"
|
||||||
add_filter "/lib/httpx/punycode.rb"
|
|
||||||
end
|
end
|
||||||
|
@ -56,13 +56,13 @@ HTTPX.delete("https://myapi.com/users/1")
|
|||||||
require "httpx"
|
require "httpx"
|
||||||
|
|
||||||
# Basic Auth
|
# Basic Auth
|
||||||
response = HTTPX.plugin(:basic_authentication).basic_authentication("username", "password").get("https://google.com")
|
response = HTTPX.plugin(:basic_auth).basic_auth("username", "password").get("https://google.com")
|
||||||
|
|
||||||
# Digest Auth
|
# Digest Auth
|
||||||
response = HTTPX.plugin(:digest_authentication).digest_authentication("username", "password").get("https://google.com")
|
response = HTTPX.plugin(:digest_auth).digest_auth("username", "password").get("https://google.com")
|
||||||
|
|
||||||
# Bearer Token Auth
|
# Bearer Token Auth
|
||||||
response = HTTPX.plugin(:authentication).authentication("eyrandomtoken").get("https://google.com")
|
response = HTTPX.plugin(:auth).authorization("eyrandomtoken").get("https://google.com")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@ -139,9 +139,14 @@ end
|
|||||||
```ruby
|
```ruby
|
||||||
require "httpx"
|
require "httpx"
|
||||||
|
|
||||||
response = HTTPX.plugin(:compression).get("https://www.google.com")
|
response = HTTPX.get("https://www.google.com")
|
||||||
puts response.headers["content-encoding"] #=> "gzip"
|
puts response.headers["content-encoding"] #=> "gzip"
|
||||||
|
puts response.to_s #=> uncompressed payload
|
||||||
|
|
||||||
|
# uncompressed request payload
|
||||||
|
HTTPX.post("https://myapi.com/users", body: super_large_text_payload)
|
||||||
|
# gzip-compressed request payload
|
||||||
|
HTTPX.post("https://myapi.com/users", headers: { "content-encoding" => %w[gzip] } body: super_large_text_payload)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Proxy
|
## Proxy
|
||||||
|
27
Gemfile
27
Gemfile
@ -8,31 +8,19 @@ gemspec
|
|||||||
gem "rake", "~> 13.0"
|
gem "rake", "~> 13.0"
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
|
gem "ddtrace"
|
||||||
gem "http-form_data", ">= 2.0.0"
|
gem "http-form_data", ">= 2.0.0"
|
||||||
gem "minitest"
|
gem "minitest"
|
||||||
gem "minitest-proveit"
|
gem "minitest-proveit"
|
||||||
gem "nokogiri"
|
gem "nokogiri"
|
||||||
gem "ruby-ntlm"
|
gem "ruby-ntlm"
|
||||||
gem "sentry-ruby" if RUBY_VERSION >= "2.4.0"
|
gem "sentry-ruby"
|
||||||
gem "spy"
|
gem "spy"
|
||||||
gem "webmock"
|
gem "webmock"
|
||||||
gem "websocket-driver"
|
gem "websocket-driver"
|
||||||
|
|
||||||
gem "net-ssh", "~> 4.2.0" if RUBY_VERSION < "2.2.0"
|
|
||||||
|
|
||||||
gem "ddtrace"
|
|
||||||
|
|
||||||
platform :mri do
|
platform :mri do
|
||||||
if RUBY_VERSION < "2.5.0"
|
gem "grpc"
|
||||||
gem "google-protobuf", "< 3.19.2"
|
|
||||||
elsif RUBY_VERSION < "2.7.0"
|
|
||||||
gem "google-protobuf", "< 3.22.0"
|
|
||||||
end
|
|
||||||
if RUBY_VERSION <= "2.6.0"
|
|
||||||
gem "grpc", "< 1.49.0"
|
|
||||||
else
|
|
||||||
gem "grpc"
|
|
||||||
end
|
|
||||||
gem "logging"
|
gem "logging"
|
||||||
gem "marcel", require: false
|
gem "marcel", require: false
|
||||||
gem "mimemagic", require: false
|
gem "mimemagic", require: false
|
||||||
@ -55,13 +43,12 @@ group :test do
|
|||||||
end
|
end
|
||||||
|
|
||||||
platform :jruby do
|
platform :jruby do
|
||||||
gem "jruby-openssl" # , git: "https://github.com/jruby/jruby-openssl.git", branch: "master"
|
|
||||||
gem "ruby-debug"
|
gem "ruby-debug"
|
||||||
end
|
end
|
||||||
|
|
||||||
gem "aws-sdk-s3"
|
gem "aws-sdk-s3"
|
||||||
gem "faraday"
|
gem "faraday"
|
||||||
gem "idnx" if RUBY_VERSION >= "2.4.0"
|
gem "idnx"
|
||||||
gem "oga"
|
gem "oga"
|
||||||
|
|
||||||
if RUBY_VERSION >= "3.0.0"
|
if RUBY_VERSION >= "3.0.0"
|
||||||
@ -72,11 +59,7 @@ group :test do
|
|||||||
end
|
end
|
||||||
|
|
||||||
group :coverage do
|
group :coverage do
|
||||||
if RUBY_VERSION < "2.5"
|
gem "simplecov"
|
||||||
gem "simplecov", "< 0.21.0"
|
|
||||||
else
|
|
||||||
gem "simplecov"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
group :assorted do
|
group :assorted do
|
||||||
|
48
LICENSE.txt
48
LICENSE.txt
@ -189,51 +189,3 @@
|
|||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
* lib/httpx/domain_name.rb
|
|
||||||
|
|
||||||
This file is derived from the implementation of punycode available at
|
|
||||||
here:
|
|
||||||
|
|
||||||
https://www.verisign.com/en_US/channel-resources/domain-registry-products/idn-sdks/index.xhtml
|
|
||||||
|
|
||||||
Copyright (C) 2000-2002 Verisign Inc., All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or
|
|
||||||
without modification, are permitted provided that the following
|
|
||||||
conditions are met:
|
|
||||||
|
|
||||||
1) Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
2) Redistributions in binary form must reproduce the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer in
|
|
||||||
the documentation and/or other materials provided with the
|
|
||||||
distribution.
|
|
||||||
|
|
||||||
3) Neither the name of the VeriSign Inc. nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived
|
|
||||||
from this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
|
||||||
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
||||||
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
||||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
|
||||||
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
|
|
||||||
AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
|
||||||
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
||||||
POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
This software is licensed under the BSD open source license. For more
|
|
||||||
information visit www.opensource.org.
|
|
||||||
|
|
||||||
Authors:
|
|
||||||
John Colosi (VeriSign)
|
|
||||||
Srikanth Veeramachaneni (VeriSign)
|
|
||||||
Nagesh Chigurupati (Verisign)
|
|
||||||
Praveen Srinivasan(Verisign)
|
|
17
README.md
17
README.md
@ -19,7 +19,7 @@ And also:
|
|||||||
|
|
||||||
* Compression (gzip, deflate, brotli)
|
* Compression (gzip, deflate, brotli)
|
||||||
* Streaming Requests
|
* Streaming Requests
|
||||||
* Authentication (Basic Auth, Digest Auth, NTLM)
|
* Auth (Basic Auth, Digest Auth, NTLM)
|
||||||
* Expect 100-continue
|
* Expect 100-continue
|
||||||
* Multipart Requests
|
* Multipart Requests
|
||||||
* Advanced Cookie handling
|
* Advanced Cookie handling
|
||||||
@ -113,7 +113,7 @@ response = HTTPX.plugin(:basic_authentication)
|
|||||||
.get("https://www.google.com")
|
.get("https://www.google.com")
|
||||||
|
|
||||||
# more complex client objects can be cached, and are thread-safe
|
# more complex client objects can be cached, and are thread-safe
|
||||||
http = HTTPX.plugin(:compression).plugin(:expect).with(headers: { "x-pvt-token" => "TOKEN"})
|
http = HTTPX.plugin(:expect).with(headers: { "x-pvt-token" => "TOKEN"})
|
||||||
http.get("https://example.com") # the above options will apply
|
http.get("https://example.com") # the above options will apply
|
||||||
http.post("https://example2.com", form: {name: "John", age: "22"}) # same, plus the form POST body
|
http.post("https://example2.com", form: {name: "John", age: "22"}) # same, plus the form POST body
|
||||||
```
|
```
|
||||||
@ -134,9 +134,9 @@ The test suite runs against [httpbin proxied over nghttp2](https://nghttp2.org/h
|
|||||||
|
|
||||||
## Supported Rubies
|
## Supported Rubies
|
||||||
|
|
||||||
All Rubies greater or equal to 2.1, and always latest JRuby and Truffleruby.
|
All Rubies greater or equal to 2.7, and always latest JRuby and Truffleruby.
|
||||||
|
|
||||||
**Note**: This gem is tested against all latest patch versions, i.e. if you're using 2.2.0 and you experience some issue, please test it against 2.2.10 (latest patch version of 2.2) before creating an issue.
|
**Note**: This gem is tested against all latest patch versions, i.e. if you're using 3.2.0 and you experience some issue, please test it against 3.2.$latest before creating an issue.
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
| | |
|
| | |
|
||||||
@ -149,15 +149,6 @@ All Rubies greater or equal to 2.1, and always latest JRuby and Truffleruby.
|
|||||||
|
|
||||||
## Caveats
|
## Caveats
|
||||||
|
|
||||||
### ALPN support
|
|
||||||
|
|
||||||
ALPN negotiation is required for "auto" HTTP/2 "https" requests. This is available in ruby since version 2.3 .
|
|
||||||
|
|
||||||
### Known bugs
|
|
||||||
|
|
||||||
* Doesn't work with ruby 2.4.0 for Windows (see [#36](https://gitlab.com/os85/httpx/issues/36)).
|
|
||||||
* Using `total_timeout` along with the `:persistent` plugin [does not work as you might expect](https://gitlab.com/os85/httpx/-/wikis/Timeouts#total_timeout).
|
|
||||||
|
|
||||||
## Versioning Policy
|
## Versioning Policy
|
||||||
|
|
||||||
Although 0.x software, `httpx` is considered API-stable and production-ready, i.e. current API or options may be subject to deprecation and emit log warnings, but can only effectively be removed in a major version change.
|
Although 0.x software, `httpx` is considered API-stable and production-ready, i.e. current API or options may be subject to deprecation and emit log warnings, but can only effectively be removed in a major version change.
|
||||||
|
50
doc/release_notes/1_0_0.md
Normal file
50
doc/release_notes/1_0_0.md
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# 1.0.0
|
||||||
|
|
||||||
|
## Breaking changes
|
||||||
|
|
||||||
|
* the minimum supported ruby version is 2.7.0 .
|
||||||
|
* The default support for IDNA 2003 has been removed. If you require this feature, install the [idnx gem](https://github.com/HoneyryderChuck/idnx), which `httpx` automatically integrates with when available.
|
||||||
|
* `:total_timeout` option has been removed (no session-wide timeout supported, use `:request_timeout`).
|
||||||
|
* `:read_timeout` and `:write_timeout` are now set to 60 seconds by default, and preferred over `:operation_timeout`;
|
||||||
|
* the exception being in the `:stream` plugin, as the response is theoretically endless (so `:read_timeout` is unset).
|
||||||
|
* The `:multipart` plugin is removed, as its functionality and API are now loaded by default.
|
||||||
|
* The `:compression` plugin is removed, as its functionality and API are now loaded by default.
|
||||||
|
* `:compression_threshold_size` was removed (formats in `"content-encoding"` request header will always encode the request body).
|
||||||
|
* the new `:compress_request_body` and `:decompress_response_body` can be set to `false` to (respectively) auto-compress passed input body, or decompress the response body.
|
||||||
|
* `:retries` plugin: the `:retry_on` condition will **not** replace default retriable error checks, it will now instead be triggered only if the retriable error checks do not find anything.
|
||||||
|
|
||||||
|
### plugins
|
||||||
|
|
||||||
|
* `:authentication` plugin becomes `:auth`.
|
||||||
|
* `.authentication` helper becomes `.authorization`.
|
||||||
|
* `:basic_authentication` plugin becomes `:basic_auth`.
|
||||||
|
* `:basic_authentication` helper is removed.
|
||||||
|
* `:digest_authentication` plugin becomes `:digest_auth`.
|
||||||
|
* `:digest_authentication` helper is removed.
|
||||||
|
* `:ntlm_authentication` plugin becomes `:ntlm_auth`.
|
||||||
|
* `:ntlm_authentication` helper is removed.
|
||||||
|
* OAuth plugin: `:oauth_authentication` helper is rename to `:oauth_auth`.
|
||||||
|
* `:compression/brotli` plugin becomes `:brotli`.
|
||||||
|
|
||||||
|
### Support removed for deprecated APIs
|
||||||
|
|
||||||
|
* The deprecated `HTTPX::Client` constant lookup has been removed (use `HTTPX::Session` instead).
|
||||||
|
* The deprecated `HTTPX.timeout({...})` function has been removed (use `HTTPX.with(timeout: {...})` instead).
|
||||||
|
* The deprecated `HTTPX.headers({...})` function has been removed (use `HTTPX.with(headers: {...})` instead).
|
||||||
|
* The deprecated `HTTPX.plugins(...)` function has been removed (use `HTTPX.plugin(...).plugin(...)...` instead).
|
||||||
|
* The deprecated `:transport_options` option, which was only valid for UNIX connections, has been removed (use `:addresses` instead).
|
||||||
|
* The deprecated `def_option(...)` function, previously used to define additional options in plugins, has been removed (use `def option_$new_option)` instead).
|
||||||
|
* The deprecated `:loop_timeout` timeout option has been removed.
|
||||||
|
* `:stream` plugin: the deprecated `HTTPX::InstanceMethods::StreamResponse` has been removed (use `HTTPX::StreamResponse` instead).
|
||||||
|
* The deprecated usage of symbols to indicate HTTP verbs (i.e. `HTTPX.request(:get, ...)` or `HTTPX.build_request(:get, ...)`) is not supported anymore (use the upcase string always, i.e. `HTTPX.request("GET", ...)` or `HTTPX.build_request("GET", ...)`, instead).
|
||||||
|
* The deprecated `HTTPX::ErrorResponse#status` method has been removed (use `HTTPX::ErrorResponse#error` instead).
|
||||||
|
|
||||||
|
### dependencies
|
||||||
|
|
||||||
|
* `:datadog` adapter only supports `ddtrace` gem 1.x or higher.
|
||||||
|
* `:faraday` adapter only supports `faraday` gem 1.x or higher.
|
||||||
|
|
||||||
|
### chore
|
||||||
|
|
||||||
|
* `:grpc` plugin: connection won't buffer requests before HTTP/2 handshake is commpleted, i.e. works the same as plain `httpx` HTTP/2 connection establishment.
|
||||||
|
* if you are relying on this, you can keep the old behavior this way: `HTTPX.plugin(:grpc, http2_settings: { wait_for_handshake: false })`.
|
@ -1,7 +1,7 @@
|
|||||||
version: '3'
|
version: '3'
|
||||||
services:
|
services:
|
||||||
httpx:
|
httpx:
|
||||||
image: jruby:9.3
|
image: jruby:9.4
|
||||||
environment:
|
environment:
|
||||||
- JRUBY_OPTS=--debug
|
- JRUBY_OPTS=--debug
|
||||||
entrypoint:
|
entrypoint:
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
version: '3'
|
|
||||||
services:
|
|
||||||
httpx:
|
|
||||||
image: ruby:2.4
|
|
||||||
environment:
|
|
||||||
- HTTPBIN_COALESCING_HOST=another
|
|
||||||
links:
|
|
||||||
- "nghttp2:another"
|
|
@ -1,8 +0,0 @@
|
|||||||
version: '3'
|
|
||||||
services:
|
|
||||||
httpx:
|
|
||||||
image: ruby:2.5
|
|
||||||
environment:
|
|
||||||
- HTTPBIN_COALESCING_HOST=another
|
|
||||||
links:
|
|
||||||
- "nghttp2:another"
|
|
@ -1,27 +0,0 @@
|
|||||||
version: '3'
|
|
||||||
services:
|
|
||||||
httpx:
|
|
||||||
image: ruby:2.6
|
|
||||||
environment:
|
|
||||||
- HTTPBIN_COALESCING_HOST=another
|
|
||||||
- HTTPX_RESOLVER_URI=https://doh/dns-query
|
|
||||||
links:
|
|
||||||
- "nghttp2:another"
|
|
||||||
depends_on:
|
|
||||||
- doh
|
|
||||||
|
|
||||||
doh:
|
|
||||||
image: registry.gitlab.com/os85/httpx/nghttp2:1
|
|
||||||
depends_on:
|
|
||||||
- doh-proxy
|
|
||||||
entrypoint:
|
|
||||||
/usr/local/bin/nghttpx
|
|
||||||
volumes:
|
|
||||||
- ./test/support/ci:/home
|
|
||||||
command:
|
|
||||||
--conf /home/doh-nghttp.conf --no-ocsp --frontend '*,443'
|
|
||||||
|
|
||||||
doh-proxy:
|
|
||||||
image: publicarray/doh-proxy
|
|
||||||
environment:
|
|
||||||
- "UNBOUND_SERVICE_HOST=127.0.0.11"
|
|
@ -1,7 +1,7 @@
|
|||||||
require "httpx"
|
require "httpx"
|
||||||
require "oga"
|
require "oga"
|
||||||
|
|
||||||
http = HTTPX.plugin(:compression).plugin(:persistent).with(timeout: { operation_timeut: 5, connect_timeout: 5})
|
http = HTTPX.plugin(:persistent).with(timeout: { operation_timeut: 5, connect_timeout: 5})
|
||||||
|
|
||||||
PAGES = (ARGV.first || 10).to_i
|
PAGES = (ARGV.first || 10).to_i
|
||||||
pages = PAGES.times.map do |page|
|
pages = PAGES.times.map do |page|
|
||||||
|
@ -33,4 +33,6 @@ Gem::Specification.new do |gem|
|
|||||||
gem.require_paths = ["lib"]
|
gem.require_paths = ["lib"]
|
||||||
|
|
||||||
gem.add_runtime_dependency "http-2-next", ">= 0.4.1"
|
gem.add_runtime_dependency "http-2-next", ">= 0.4.1"
|
||||||
|
|
||||||
|
gem.required_ruby_version = ">= 2.7.0"
|
||||||
end
|
end
|
||||||
|
@ -241,59 +241,29 @@ class DatadogTest < Minitest::Test
|
|||||||
assert span.get_metric("_dd1.sr.eausr") == sample_rate
|
assert span.get_metric("_dd1.sr.eausr") == sample_rate
|
||||||
end
|
end
|
||||||
|
|
||||||
if defined?(::DDTrace) && Gem::Version.new(::DDTrace::VERSION::STRING) >= Gem::Version.new("1.0.0")
|
def set_datadog(options = {}, &blk)
|
||||||
|
Datadog.configure do |c|
|
||||||
def set_datadog(options = {}, &blk)
|
c.tracing.instrument(:httpx, options, &blk)
|
||||||
Datadog.configure do |c|
|
|
||||||
c.tracing.instrument(:httpx, options, &blk)
|
|
||||||
end
|
|
||||||
|
|
||||||
tracer # initialize tracer patches
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def tracer
|
tracer # initialize tracer patches
|
||||||
@tracer ||= begin
|
end
|
||||||
tr = Datadog::Tracing.send(:tracer)
|
|
||||||
def tr.write(trace)
|
def tracer
|
||||||
@traces ||= []
|
@tracer ||= begin
|
||||||
@traces << trace
|
tr = Datadog::Tracing.send(:tracer)
|
||||||
end
|
def tr.write(trace)
|
||||||
tr
|
@traces ||= []
|
||||||
|
@traces << trace
|
||||||
end
|
end
|
||||||
|
tr
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def trace_with_sampling_priority(priority)
|
def trace_with_sampling_priority(priority)
|
||||||
tracer.trace("foo.bar") do
|
tracer.trace("foo.bar") do
|
||||||
tracer.active_trace.sampling_priority = priority
|
tracer.active_trace.sampling_priority = priority
|
||||||
yield
|
yield
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
|
|
||||||
def set_datadog(options = {}, &blk)
|
|
||||||
Datadog.configure do |c|
|
|
||||||
c.use(:httpx, options, &blk)
|
|
||||||
end
|
|
||||||
|
|
||||||
tracer # initialize tracer patches
|
|
||||||
end
|
|
||||||
|
|
||||||
def tracer
|
|
||||||
@tracer ||= begin
|
|
||||||
tr = Datadog.tracer
|
|
||||||
def tr.write(trace)
|
|
||||||
@spans ||= []
|
|
||||||
@spans << trace
|
|
||||||
end
|
|
||||||
tr
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def trace_with_sampling_priority(priority)
|
|
||||||
tracer.trace("foo.bar") do |span|
|
|
||||||
span.context.sampling_priority = priority
|
|
||||||
yield
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -305,11 +275,7 @@ class DatadogTest < Minitest::Test
|
|||||||
# Retrieves and sorts all spans in the current tracer instance.
|
# Retrieves and sorts all spans in the current tracer instance.
|
||||||
# This method does not cache its results.
|
# This method does not cache its results.
|
||||||
def fetch_spans
|
def fetch_spans
|
||||||
spans = if defined?(::DDTrace) && Gem::Version.new(::DDTrace::VERSION::STRING) >= Gem::Version.new("1.0.0")
|
spans = (tracer.instance_variable_get(:@traces) || []).map(&:spans)
|
||||||
(tracer.instance_variable_get(:@traces) || []).map(&:spans)
|
|
||||||
else
|
|
||||||
tracer.instance_variable_get(:@spans) || []
|
|
||||||
end
|
|
||||||
spans.flatten.sort! do |a, b|
|
spans.flatten.sort! do |a, b|
|
||||||
if a.name == b.name
|
if a.name == b.name
|
||||||
if a.resource == b.resource
|
if a.resource == b.resource
|
||||||
|
@ -1,150 +1,148 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
if RUBY_VERSION >= "2.4.0"
|
require "logger"
|
||||||
require "logger"
|
require "stringio"
|
||||||
require "stringio"
|
require "sentry-ruby"
|
||||||
require "sentry-ruby"
|
require "test_helper"
|
||||||
require "test_helper"
|
require "support/http_helpers"
|
||||||
require "support/http_helpers"
|
require "httpx/adapters/sentry"
|
||||||
require "httpx/adapters/sentry"
|
|
||||||
|
|
||||||
class SentryTest < Minitest::Test
|
class SentryTest < Minitest::Test
|
||||||
include HTTPHelpers
|
include HTTPHelpers
|
||||||
|
|
||||||
DUMMY_DSN = "http://12345:67890@sentry.localdomain/sentry/42"
|
DUMMY_DSN = "http://12345:67890@sentry.localdomain/sentry/42"
|
||||||
|
|
||||||
def test_sentry_send_yes_pii
|
def test_sentry_send_yes_pii
|
||||||
before_pii = Sentry.configuration.send_default_pii
|
before_pii = Sentry.configuration.send_default_pii
|
||||||
begin
|
begin
|
||||||
Sentry.configuration.send_default_pii = true
|
Sentry.configuration.send_default_pii = true
|
||||||
|
|
||||||
transaction = Sentry.start_transaction
|
|
||||||
Sentry.get_current_scope.set_span(transaction)
|
|
||||||
|
|
||||||
uri = build_uri("/get")
|
|
||||||
|
|
||||||
response = HTTPX.get(uri, params: { "foo" => "bar" })
|
|
||||||
|
|
||||||
verify_status(response, 200)
|
|
||||||
verify_spans(transaction, response, description: "GET #{uri}?foo=bar")
|
|
||||||
crumb = Sentry.get_current_scope.breadcrumbs.peek
|
|
||||||
assert crumb.category == "httpx"
|
|
||||||
assert crumb.data == { status: 200, method: "GET", url: "#{uri}?foo=bar" }
|
|
||||||
ensure
|
|
||||||
Sentry.configuration.send_default_pii = before_pii
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_sentry_send_no_pii
|
|
||||||
before_pii = Sentry.configuration.send_default_pii
|
|
||||||
begin
|
|
||||||
Sentry.configuration.send_default_pii = false
|
|
||||||
|
|
||||||
transaction = Sentry.start_transaction
|
|
||||||
Sentry.get_current_scope.set_span(transaction)
|
|
||||||
|
|
||||||
uri = build_uri("/get")
|
|
||||||
|
|
||||||
response = HTTPX.get(uri, params: { "foo" => "bar" })
|
|
||||||
|
|
||||||
verify_status(response, 200)
|
|
||||||
verify_spans(transaction, response, description: "GET #{uri}")
|
|
||||||
crumb = Sentry.get_current_scope.breadcrumbs.peek
|
|
||||||
assert crumb.category == "httpx"
|
|
||||||
assert crumb.data == { status: 200, method: "GET", url: uri }
|
|
||||||
ensure
|
|
||||||
Sentry.configuration.send_default_pii = before_pii
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_sentry_post_request
|
|
||||||
before_pii = Sentry.configuration.send_default_pii
|
|
||||||
begin
|
|
||||||
Sentry.configuration.send_default_pii = true
|
|
||||||
transaction = Sentry.start_transaction
|
|
||||||
Sentry.get_current_scope.set_span(transaction)
|
|
||||||
|
|
||||||
uri = build_uri("/post")
|
|
||||||
response = HTTPX.post(uri, form: { foo: "bar" })
|
|
||||||
verify_status(response, 200)
|
|
||||||
verify_spans(transaction, response, verb: "POST")
|
|
||||||
|
|
||||||
crumb = Sentry.get_current_scope.breadcrumbs.peek
|
|
||||||
assert crumb.category == "httpx"
|
|
||||||
assert crumb.data == { status: 200, method: "POST", url: uri, body: "foo=bar" }
|
|
||||||
ensure
|
|
||||||
Sentry.configuration.send_default_pii = before_pii
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_sentry_multiple_requests
|
|
||||||
transaction = Sentry.start_transaction
|
transaction = Sentry.start_transaction
|
||||||
Sentry.get_current_scope.set_span(transaction)
|
Sentry.get_current_scope.set_span(transaction)
|
||||||
|
|
||||||
responses = HTTPX.get(build_uri("/status/200"), build_uri("/status/404"))
|
uri = build_uri("/get")
|
||||||
verify_status(responses[0], 200)
|
|
||||||
verify_status(responses[1], 404)
|
|
||||||
verify_spans(transaction, *responses)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_sentry_server_error_request
|
response = HTTPX.get(uri, params: { "foo" => "bar" })
|
||||||
transaction = Sentry.start_transaction
|
|
||||||
Sentry.get_current_scope.set_span(transaction)
|
|
||||||
|
|
||||||
uri = URI("http://unexisting/")
|
verify_status(response, 200)
|
||||||
|
verify_spans(transaction, response, description: "GET #{uri}?foo=bar")
|
||||||
response = HTTPX.get(uri)
|
|
||||||
|
|
||||||
verify_error_response(response, /name or service not known/)
|
|
||||||
assert response.is_a?(HTTPX::ErrorResponse), "response should contain errors"
|
|
||||||
verify_spans(transaction, response, verb: "GET")
|
|
||||||
crumb = Sentry.get_current_scope.breadcrumbs.peek
|
crumb = Sentry.get_current_scope.breadcrumbs.peek
|
||||||
assert crumb.category == "httpx"
|
assert crumb.category == "httpx"
|
||||||
assert crumb.data == { error: "name or service not known", method: "GET", url: uri.to_s }
|
assert crumb.data == { status: 200, method: "GET", url: "#{uri}?foo=bar" }
|
||||||
end
|
ensure
|
||||||
|
Sentry.configuration.send_default_pii = before_pii
|
||||||
private
|
|
||||||
|
|
||||||
def verify_spans(transaction, *responses, verb: nil, description: nil)
|
|
||||||
assert transaction.span_recorder.spans.count == responses.size + 1
|
|
||||||
assert transaction.span_recorder.spans[0] == transaction
|
|
||||||
|
|
||||||
response_spans = transaction.span_recorder.spans[1..-1]
|
|
||||||
|
|
||||||
responses.each_with_index do |response, idx|
|
|
||||||
request_span = response_spans[idx]
|
|
||||||
assert request_span.op == "httpx.client"
|
|
||||||
assert !request_span.start_timestamp.nil?
|
|
||||||
assert !request_span.timestamp.nil?
|
|
||||||
assert request_span.start_timestamp != request_span.timestamp
|
|
||||||
assert request_span.description == (description || "#{verb || "GET"} #{response.uri}")
|
|
||||||
if response.is_a?(HTTPX::ErrorResponse)
|
|
||||||
assert request_span.data == { error: response.error.message }
|
|
||||||
else
|
|
||||||
assert request_span.data == { status: response.status }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def setup
|
|
||||||
super
|
|
||||||
|
|
||||||
mock_io = StringIO.new
|
|
||||||
mock_logger = Logger.new(mock_io)
|
|
||||||
|
|
||||||
Sentry.init do |config|
|
|
||||||
config.traces_sample_rate = 1.0
|
|
||||||
config.logger = mock_logger
|
|
||||||
config.dsn = DUMMY_DSN
|
|
||||||
config.transport.transport_class = Sentry::DummyTransport
|
|
||||||
config.breadcrumbs_logger = [:http_logger]
|
|
||||||
# so the events will be sent synchronously for testing
|
|
||||||
config.background_worker_threads = 0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def origin
|
|
||||||
"https://#{httpbin}"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_sentry_send_no_pii
|
||||||
|
before_pii = Sentry.configuration.send_default_pii
|
||||||
|
begin
|
||||||
|
Sentry.configuration.send_default_pii = false
|
||||||
|
|
||||||
|
transaction = Sentry.start_transaction
|
||||||
|
Sentry.get_current_scope.set_span(transaction)
|
||||||
|
|
||||||
|
uri = build_uri("/get")
|
||||||
|
|
||||||
|
response = HTTPX.get(uri, params: { "foo" => "bar" })
|
||||||
|
|
||||||
|
verify_status(response, 200)
|
||||||
|
verify_spans(transaction, response, description: "GET #{uri}")
|
||||||
|
crumb = Sentry.get_current_scope.breadcrumbs.peek
|
||||||
|
assert crumb.category == "httpx"
|
||||||
|
assert crumb.data == { status: 200, method: "GET", url: uri }
|
||||||
|
ensure
|
||||||
|
Sentry.configuration.send_default_pii = before_pii
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_sentry_post_request
|
||||||
|
before_pii = Sentry.configuration.send_default_pii
|
||||||
|
begin
|
||||||
|
Sentry.configuration.send_default_pii = true
|
||||||
|
transaction = Sentry.start_transaction
|
||||||
|
Sentry.get_current_scope.set_span(transaction)
|
||||||
|
|
||||||
|
uri = build_uri("/post")
|
||||||
|
response = HTTPX.post(uri, form: { foo: "bar" })
|
||||||
|
verify_status(response, 200)
|
||||||
|
verify_spans(transaction, response, verb: "POST")
|
||||||
|
|
||||||
|
crumb = Sentry.get_current_scope.breadcrumbs.peek
|
||||||
|
assert crumb.category == "httpx"
|
||||||
|
assert crumb.data == { status: 200, method: "POST", url: uri, body: "foo=bar" }
|
||||||
|
ensure
|
||||||
|
Sentry.configuration.send_default_pii = before_pii
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_sentry_multiple_requests
|
||||||
|
transaction = Sentry.start_transaction
|
||||||
|
Sentry.get_current_scope.set_span(transaction)
|
||||||
|
|
||||||
|
responses = HTTPX.get(build_uri("/status/200"), build_uri("/status/404"))
|
||||||
|
verify_status(responses[0], 200)
|
||||||
|
verify_status(responses[1], 404)
|
||||||
|
verify_spans(transaction, *responses)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_sentry_server_error_request
|
||||||
|
transaction = Sentry.start_transaction
|
||||||
|
Sentry.get_current_scope.set_span(transaction)
|
||||||
|
|
||||||
|
uri = URI("http://unexisting/")
|
||||||
|
|
||||||
|
response = HTTPX.get(uri)
|
||||||
|
|
||||||
|
verify_error_response(response, /name or service not known/)
|
||||||
|
assert response.is_a?(HTTPX::ErrorResponse), "response should contain errors"
|
||||||
|
verify_spans(transaction, response, verb: "GET")
|
||||||
|
crumb = Sentry.get_current_scope.breadcrumbs.peek
|
||||||
|
assert crumb.category == "httpx"
|
||||||
|
assert crumb.data == { error: "name or service not known", method: "GET", url: uri.to_s }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def verify_spans(transaction, *responses, verb: nil, description: nil)
|
||||||
|
assert transaction.span_recorder.spans.count == responses.size + 1
|
||||||
|
assert transaction.span_recorder.spans[0] == transaction
|
||||||
|
|
||||||
|
response_spans = transaction.span_recorder.spans[1..-1]
|
||||||
|
|
||||||
|
responses.each_with_index do |response, idx|
|
||||||
|
request_span = response_spans[idx]
|
||||||
|
assert request_span.op == "httpx.client"
|
||||||
|
assert !request_span.start_timestamp.nil?
|
||||||
|
assert !request_span.timestamp.nil?
|
||||||
|
assert request_span.start_timestamp != request_span.timestamp
|
||||||
|
assert request_span.description == (description || "#{verb || "GET"} #{response.uri}")
|
||||||
|
if response.is_a?(HTTPX::ErrorResponse)
|
||||||
|
assert request_span.data == { error: response.error.message }
|
||||||
|
else
|
||||||
|
assert request_span.data == { status: response.status }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup
|
||||||
|
super
|
||||||
|
|
||||||
|
mock_io = StringIO.new
|
||||||
|
mock_logger = Logger.new(mock_io)
|
||||||
|
|
||||||
|
Sentry.init do |config|
|
||||||
|
config.traces_sample_rate = 1.0
|
||||||
|
config.logger = mock_logger
|
||||||
|
config.dsn = DUMMY_DSN
|
||||||
|
config.transport.transport_class = Sentry::DummyTransport
|
||||||
|
config.breadcrumbs_logger = [:http_logger]
|
||||||
|
# so the events will be sent synchronously for testing
|
||||||
|
config.background_worker_threads = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def origin
|
||||||
|
"https://#{httpbin}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -53,14 +53,6 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# :nocov:
|
|
||||||
def self.const_missing(const_name)
|
|
||||||
super unless const_name == :Client
|
|
||||||
warn "DEPRECATION WARNING: the class #{self}::Client is deprecated. Use #{self}::Session instead."
|
|
||||||
Session
|
|
||||||
end
|
|
||||||
# :nocov:
|
|
||||||
|
|
||||||
extend Chainable
|
extend Chainable
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1,51 +1,24 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
if defined?(DDTrace) && DDTrace::VERSION::STRING >= "1.0.0"
|
require "datadog/tracing/contrib/integration"
|
||||||
require "datadog/tracing/contrib/integration"
|
require "datadog/tracing/contrib/configuration/settings"
|
||||||
require "datadog/tracing/contrib/configuration/settings"
|
require "datadog/tracing/contrib/patcher"
|
||||||
require "datadog/tracing/contrib/patcher"
|
|
||||||
|
|
||||||
TRACING_MODULE = Datadog::Tracing
|
module Datadog::Tracing
|
||||||
else
|
|
||||||
|
|
||||||
require "ddtrace/contrib/integration"
|
|
||||||
require "ddtrace/contrib/configuration/settings"
|
|
||||||
require "ddtrace/contrib/patcher"
|
|
||||||
|
|
||||||
TRACING_MODULE = Datadog
|
|
||||||
end
|
|
||||||
|
|
||||||
module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
|
|
||||||
module Contrib
|
module Contrib
|
||||||
module HTTPX
|
module HTTPX
|
||||||
if defined?(::DDTrace) && ::DDTrace::VERSION::STRING >= "1.0.0"
|
METADATA_MODULE = Datadog::Tracing::Metadata
|
||||||
METADATA_MODULE = TRACING_MODULE::Metadata
|
|
||||||
|
|
||||||
TYPE_OUTBOUND = TRACING_MODULE::Metadata::Ext::HTTP::TYPE_OUTBOUND
|
TYPE_OUTBOUND = Datadog::Tracing::Metadata::Ext::HTTP::TYPE_OUTBOUND
|
||||||
|
|
||||||
TAG_PEER_SERVICE = TRACING_MODULE::Metadata::Ext::TAG_PEER_SERVICE
|
TAG_PEER_SERVICE = Datadog::Tracing::Metadata::Ext::TAG_PEER_SERVICE
|
||||||
|
|
||||||
TAG_URL = TRACING_MODULE::Metadata::Ext::HTTP::TAG_URL
|
TAG_URL = Datadog::Tracing::Metadata::Ext::HTTP::TAG_URL
|
||||||
TAG_METHOD = TRACING_MODULE::Metadata::Ext::HTTP::TAG_METHOD
|
TAG_METHOD = Datadog::Tracing::Metadata::Ext::HTTP::TAG_METHOD
|
||||||
TAG_TARGET_HOST = TRACING_MODULE::Metadata::Ext::NET::TAG_TARGET_HOST
|
TAG_TARGET_HOST = Datadog::Tracing::Metadata::Ext::NET::TAG_TARGET_HOST
|
||||||
TAG_TARGET_PORT = TRACING_MODULE::Metadata::Ext::NET::TAG_TARGET_PORT
|
TAG_TARGET_PORT = Datadog::Tracing::Metadata::Ext::NET::TAG_TARGET_PORT
|
||||||
|
|
||||||
TAG_STATUS_CODE = TRACING_MODULE::Metadata::Ext::HTTP::TAG_STATUS_CODE
|
TAG_STATUS_CODE = Datadog::Tracing::Metadata::Ext::HTTP::TAG_STATUS_CODE
|
||||||
|
|
||||||
else
|
|
||||||
|
|
||||||
METADATA_MODULE = Datadog
|
|
||||||
|
|
||||||
TYPE_OUTBOUND = TRACING_MODULE::Ext::HTTP::TYPE_OUTBOUND
|
|
||||||
TAG_PEER_SERVICE = TRACING_MODULE::Ext::Integration::TAG_PEER_SERVICE
|
|
||||||
TAG_URL = TRACING_MODULE::Ext::HTTP::URL
|
|
||||||
TAG_METHOD = TRACING_MODULE::Ext::HTTP::METHOD
|
|
||||||
TAG_TARGET_HOST = TRACING_MODULE::Ext::NET::TARGET_HOST
|
|
||||||
TAG_TARGET_PORT = TRACING_MODULE::Ext::NET::TARGET_PORT
|
|
||||||
TAG_STATUS_CODE = Datadog::Ext::HTTP::STATUS_CODE
|
|
||||||
PROPAGATOR = TRACING_MODULE::HTTPPropagator
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
# HTTPX Datadog Plugin
|
# HTTPX Datadog Plugin
|
||||||
#
|
#
|
||||||
@ -64,14 +37,18 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
return unless tracing_enabled?
|
return unless Datadog::Tracing.enabled?
|
||||||
|
|
||||||
@request.on(:response, &method(:finish))
|
@request.on(:response, &method(:finish))
|
||||||
|
|
||||||
verb = @request.verb
|
verb = @request.verb
|
||||||
uri = @request.uri
|
uri = @request.uri
|
||||||
|
|
||||||
@span = build_span
|
@span = Datadog::Tracing.trace(
|
||||||
|
SPAN_REQUEST,
|
||||||
|
service: service_name(@request.uri.host, configuration, Datadog.configuration_for(self)),
|
||||||
|
span_type: TYPE_OUTBOUND
|
||||||
|
)
|
||||||
|
|
||||||
@span.resource = verb
|
@span.resource = verb
|
||||||
|
|
||||||
@ -86,7 +63,8 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
|
|||||||
# Tag as an external peer service
|
# Tag as an external peer service
|
||||||
@span.set_tag(TAG_PEER_SERVICE, @span.service)
|
@span.set_tag(TAG_PEER_SERVICE, @span.service)
|
||||||
|
|
||||||
propagate_headers if @configuration[:distributed_tracing]
|
Datadog::Tracing::Propagation::HTTP.inject!(Datadog::Tracing.active_trace,
|
||||||
|
@request.headers) if @configuration[:distributed_tracing]
|
||||||
|
|
||||||
# Set analytics sample rate
|
# Set analytics sample rate
|
||||||
if Contrib::Analytics.enabled?(@configuration[:analytics_enabled])
|
if Contrib::Analytics.enabled?(@configuration[:analytics_enabled])
|
||||||
@ -113,48 +91,8 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
if defined?(::DDTrace) && ::DDTrace::VERSION::STRING >= "1.0.0"
|
def configuration
|
||||||
|
@configuration ||= Datadog.configuration.tracing[:httpx, @request.uri.host]
|
||||||
def build_span
|
|
||||||
TRACING_MODULE.trace(
|
|
||||||
SPAN_REQUEST,
|
|
||||||
service: service_name(@request.uri.host, configuration, Datadog.configuration_for(self)),
|
|
||||||
span_type: TYPE_OUTBOUND
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def propagate_headers
|
|
||||||
TRACING_MODULE::Propagation::HTTP.inject!(TRACING_MODULE.active_trace, @request.headers)
|
|
||||||
end
|
|
||||||
|
|
||||||
def configuration
|
|
||||||
@configuration ||= Datadog.configuration.tracing[:httpx, @request.uri.host]
|
|
||||||
end
|
|
||||||
|
|
||||||
def tracing_enabled?
|
|
||||||
TRACING_MODULE.enabled?
|
|
||||||
end
|
|
||||||
else
|
|
||||||
def build_span
|
|
||||||
service_name = configuration[:split_by_domain] ? @request.uri.host : configuration[:service_name]
|
|
||||||
configuration[:tracer].trace(
|
|
||||||
SPAN_REQUEST,
|
|
||||||
service: service_name,
|
|
||||||
span_type: TYPE_OUTBOUND
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def propagate_headers
|
|
||||||
Datadog::HTTPPropagator.inject!(@span.context, @request.headers)
|
|
||||||
end
|
|
||||||
|
|
||||||
def configuration
|
|
||||||
@configuration ||= Datadog.configuration[:httpx, @request.uri.host]
|
|
||||||
end
|
|
||||||
|
|
||||||
def tracing_enabled?
|
|
||||||
configuration[:tracer].enabled
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -179,7 +117,7 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
|
|||||||
module Configuration
|
module Configuration
|
||||||
# Default settings for httpx
|
# Default settings for httpx
|
||||||
#
|
#
|
||||||
class Settings < TRACING_MODULE::Contrib::Configuration::Settings
|
class Settings < Datadog::Tracing::Contrib::Configuration::Settings
|
||||||
DEFAULT_ERROR_HANDLER = lambda do |response|
|
DEFAULT_ERROR_HANDLER = lambda do |response|
|
||||||
Datadog::Ext::HTTP::ERROR_RANGE.cover?(response.status)
|
Datadog::Ext::HTTP::ERROR_RANGE.cover?(response.status)
|
||||||
end
|
end
|
||||||
@ -203,10 +141,10 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
|
|||||||
o.lazy
|
o.lazy
|
||||||
end
|
end
|
||||||
|
|
||||||
if defined?(TRACING_MODULE::Contrib::SpanAttributeSchema)
|
if defined?(Datadog::Tracing::Contrib::SpanAttributeSchema)
|
||||||
option :service_name do |o|
|
option :service_name do |o|
|
||||||
o.default do
|
o.default do
|
||||||
TRACING_MODULE::Contrib::SpanAttributeSchema.fetch_service_name(
|
Datadog::Tracing::Contrib::SpanAttributeSchema.fetch_service_name(
|
||||||
"DD_TRACE_HTTPX_SERVICE_NAME",
|
"DD_TRACE_HTTPX_SERVICE_NAME",
|
||||||
"httpx"
|
"httpx"
|
||||||
)
|
)
|
||||||
@ -231,7 +169,7 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
|
|||||||
# Patcher enables patching of 'httpx' with datadog components.
|
# Patcher enables patching of 'httpx' with datadog components.
|
||||||
#
|
#
|
||||||
module Patcher
|
module Patcher
|
||||||
include TRACING_MODULE::Contrib::Patcher
|
include Datadog::Tracing::Contrib::Patcher
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
@ -254,7 +192,6 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
|
|||||||
class Integration
|
class Integration
|
||||||
include Contrib::Integration
|
include Contrib::Integration
|
||||||
|
|
||||||
# MINIMUM_VERSION = Gem::Version.new('0.11.0')
|
|
||||||
MINIMUM_VERSION = Gem::Version.new("0.10.2")
|
MINIMUM_VERSION = Gem::Version.new("0.10.2")
|
||||||
|
|
||||||
register_as :httpx
|
register_as :httpx
|
||||||
@ -271,14 +208,8 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
|
|||||||
super && version >= MINIMUM_VERSION
|
super && version >= MINIMUM_VERSION
|
||||||
end
|
end
|
||||||
|
|
||||||
if defined?(::DDTrace) && ::DDTrace::VERSION::STRING >= "1.0.0"
|
def new_configuration
|
||||||
def new_configuration
|
Configuration::Settings.new
|
||||||
Configuration::Settings.new
|
|
||||||
end
|
|
||||||
else
|
|
||||||
def default_configuration
|
|
||||||
Configuration::Settings.new
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def patcher
|
def patcher
|
||||||
|
@ -7,38 +7,11 @@ require "faraday"
|
|||||||
module Faraday
|
module Faraday
|
||||||
class Adapter
|
class Adapter
|
||||||
class HTTPX < Faraday::Adapter
|
class HTTPX < Faraday::Adapter
|
||||||
# :nocov:
|
|
||||||
SSL_ERROR = if defined?(Faraday::SSLError)
|
|
||||||
Faraday::SSLError
|
|
||||||
else
|
|
||||||
Faraday::Error::SSLError
|
|
||||||
end
|
|
||||||
|
|
||||||
CONNECTION_FAILED_ERROR = if defined?(Faraday::ConnectionFailed)
|
|
||||||
Faraday::ConnectionFailed
|
|
||||||
else
|
|
||||||
Faraday::Error::ConnectionFailed
|
|
||||||
end
|
|
||||||
# :nocov:
|
|
||||||
|
|
||||||
unless Faraday::RequestOptions.method_defined?(:stream_response?)
|
|
||||||
module RequestOptionsExtensions
|
|
||||||
refine Faraday::RequestOptions do
|
|
||||||
def stream_response?
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
using RequestOptionsExtensions
|
|
||||||
end
|
|
||||||
|
|
||||||
module RequestMixin
|
module RequestMixin
|
||||||
using ::HTTPX::HashExtensions
|
|
||||||
|
|
||||||
def build_connection(env)
|
def build_connection(env)
|
||||||
return @connection if defined?(@connection)
|
return @connection if defined?(@connection)
|
||||||
|
|
||||||
@connection = ::HTTPX.plugin(:compression).plugin(:persistent).plugin(ReasonPlugin)
|
@connection = ::HTTPX.plugin(:persistent).plugin(ReasonPlugin)
|
||||||
@connection = @connection.with(@connection_options) unless @connection_options.empty?
|
@connection = @connection.with(@connection_options) unless @connection_options.empty?
|
||||||
connection_opts = options_from_env(env)
|
connection_opts = options_from_env(env)
|
||||||
|
|
||||||
@ -70,7 +43,7 @@ module Faraday
|
|||||||
def connect(env, &blk)
|
def connect(env, &blk)
|
||||||
connection(env, &blk)
|
connection(env, &blk)
|
||||||
rescue ::HTTPX::TLSError => e
|
rescue ::HTTPX::TLSError => e
|
||||||
raise SSL_ERROR, e
|
raise Faraday::SSLError, e
|
||||||
rescue Errno::ECONNABORTED,
|
rescue Errno::ECONNABORTED,
|
||||||
Errno::ECONNREFUSED,
|
Errno::ECONNREFUSED,
|
||||||
Errno::ECONNRESET,
|
Errno::ECONNRESET,
|
||||||
@ -79,9 +52,7 @@ module Faraday
|
|||||||
Errno::ENETUNREACH,
|
Errno::ENETUNREACH,
|
||||||
Errno::EPIPE,
|
Errno::EPIPE,
|
||||||
::HTTPX::ConnectionError => e
|
::HTTPX::ConnectionError => e
|
||||||
raise CONNECTION_FAILED_ERROR, e
|
raise Faraday::ConnectionFailed, e
|
||||||
rescue ::HTTPX::TimeoutError => e
|
|
||||||
raise Faraday::TimeoutError, e
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_request(env)
|
def build_request(env)
|
||||||
@ -159,24 +130,13 @@ module Faraday
|
|||||||
end
|
end
|
||||||
|
|
||||||
module ReasonPlugin
|
module ReasonPlugin
|
||||||
if RUBY_VERSION < "2.5"
|
def self.load_dependencies(*)
|
||||||
def self.load_dependencies(*)
|
require "net/http/status"
|
||||||
require "webrick"
|
|
||||||
end
|
|
||||||
else
|
|
||||||
def self.load_dependencies(*)
|
|
||||||
require "net/http/status"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
module ResponseMethods
|
module ResponseMethods
|
||||||
if RUBY_VERSION < "2.5"
|
def reason
|
||||||
def reason
|
Net::HTTP::STATUS_CODES.fetch(@status)
|
||||||
WEBrick::HTTPStatus::StatusMessage.fetch(@status)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
def reason
|
|
||||||
Net::HTTP::STATUS_CODES.fetch(@status)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -261,10 +221,7 @@ module Faraday
|
|||||||
|
|
||||||
# from Faraday::Adapter#request_timeout
|
# from Faraday::Adapter#request_timeout
|
||||||
def request_timeout(type, options)
|
def request_timeout(type, options)
|
||||||
key = Faraday::Adapter::TIMEOUT_KEYS.fetch(type) do
|
key = Faraday::Adapter::TIMEOUT_KEYS[type]
|
||||||
msg = "Expected :read, :write, :open. Got #{type.inspect} :("
|
|
||||||
raise ArgumentError, msg
|
|
||||||
end
|
|
||||||
options[key] || options[:timeout]
|
options[key] || options[:timeout]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,13 +2,8 @@
|
|||||||
|
|
||||||
module WebMock
|
module WebMock
|
||||||
module HttpLibAdapters
|
module HttpLibAdapters
|
||||||
if RUBY_VERSION < "2.5"
|
require "net/http/status"
|
||||||
require "webrick/httpstatus"
|
HTTP_REASONS = Net::HTTP::STATUS_CODES
|
||||||
HTTP_REASONS = WEBrick::HTTPStatus::StatusMessage
|
|
||||||
else
|
|
||||||
require "net/http/status"
|
|
||||||
HTTP_REASONS = Net::HTTP::STATUS_CODES
|
|
||||||
end
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# HTTPX plugin for webmock.
|
# HTTPX plugin for webmock.
|
||||||
|
@ -98,29 +98,11 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# :nocov:
|
def parse_altsvc_origin(alt_proto, alt_origin)
|
||||||
if RUBY_VERSION < "2.2"
|
alt_scheme = parse_altsvc_scheme(alt_proto) or return
|
||||||
def parse_altsvc_origin(alt_proto, alt_origin)
|
alt_origin = alt_origin[1..-2] if alt_origin.start_with?("\"") && alt_origin.end_with?("\"")
|
||||||
alt_scheme = parse_altsvc_scheme(alt_proto) or return
|
|
||||||
|
|
||||||
alt_origin = alt_origin[1..-2] if alt_origin.start_with?("\"") && alt_origin.end_with?("\"")
|
URI.parse("#{alt_scheme}://#{alt_origin}")
|
||||||
if alt_origin.start_with?(":")
|
|
||||||
alt_origin = "#{alt_scheme}://dummy#{alt_origin}"
|
|
||||||
uri = URI.parse(alt_origin)
|
|
||||||
uri.host = nil
|
|
||||||
uri
|
|
||||||
else
|
|
||||||
URI.parse("#{alt_scheme}://#{alt_origin}")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
def parse_altsvc_origin(alt_proto, alt_origin)
|
|
||||||
alt_scheme = parse_altsvc_scheme(alt_proto) or return
|
|
||||||
alt_origin = alt_origin[1..-2] if alt_origin.start_with?("\"") && alt_origin.end_with?("\"")
|
|
||||||
|
|
||||||
URI.parse("#{alt_scheme}://#{alt_origin}")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
# :nocov:
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -27,18 +27,6 @@ module HTTPX
|
|||||||
branch(default_options).request(*args, **options)
|
branch(default_options).request(*args, **options)
|
||||||
end
|
end
|
||||||
|
|
||||||
# :nocov:
|
|
||||||
def timeout(**args)
|
|
||||||
warn ":#{__method__} is deprecated, use :with_timeout instead"
|
|
||||||
with(timeout: args)
|
|
||||||
end
|
|
||||||
|
|
||||||
def headers(headers)
|
|
||||||
warn ":#{__method__} is deprecated, use :with_headers instead"
|
|
||||||
with(headers: headers)
|
|
||||||
end
|
|
||||||
# :nocov:
|
|
||||||
|
|
||||||
def accept(type)
|
def accept(type)
|
||||||
with(headers: { "accept" => String(type) })
|
with(headers: { "accept" => String(type) })
|
||||||
end
|
end
|
||||||
@ -54,17 +42,6 @@ module HTTPX
|
|||||||
klass.plugin(pl, options, &blk).new
|
klass.plugin(pl, options, &blk).new
|
||||||
end
|
end
|
||||||
|
|
||||||
# deprecated
|
|
||||||
# :nocov:
|
|
||||||
def plugins(pls)
|
|
||||||
warn ":#{__method__} is deprecated, use :plugin instead"
|
|
||||||
klass = is_a?(S) ? self.class : Session
|
|
||||||
klass = Class.new(klass)
|
|
||||||
klass.instance_variable_set(:@default_options, klass.default_options.merge(default_options))
|
|
||||||
klass.plugins(pls).new
|
|
||||||
end
|
|
||||||
# :nocov:
|
|
||||||
|
|
||||||
def with(options, &blk)
|
def with(options, &blk)
|
||||||
branch(default_options.merge(options), &blk)
|
branch(default_options.merge(options), &blk)
|
||||||
end
|
end
|
||||||
|
@ -33,7 +33,6 @@ module HTTPX
|
|||||||
include Callbacks
|
include Callbacks
|
||||||
|
|
||||||
using URIExtensions
|
using URIExtensions
|
||||||
using NumericExtensions
|
|
||||||
|
|
||||||
require "httpx/connection/http2"
|
require "httpx/connection/http2"
|
||||||
require "httpx/connection/http1"
|
require "httpx/connection/http1"
|
||||||
@ -70,7 +69,6 @@ module HTTPX
|
|||||||
|
|
||||||
@inflight = 0
|
@inflight = 0
|
||||||
@keep_alive_timeout = @options.timeout[:keep_alive_timeout]
|
@keep_alive_timeout = @options.timeout[:keep_alive_timeout]
|
||||||
@total_timeout = @options.timeout[:total_timeout]
|
|
||||||
|
|
||||||
self.addresses = @options.addresses if @options.addresses
|
self.addresses = @options.addresses if @options.addresses
|
||||||
end
|
end
|
||||||
@ -270,21 +268,6 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
|
|
||||||
def timeout
|
def timeout
|
||||||
if @total_timeout
|
|
||||||
return @total_timeout unless @connected_at
|
|
||||||
|
|
||||||
elapsed_time = @total_timeout - Utils.elapsed_time(@connected_at)
|
|
||||||
|
|
||||||
if elapsed_time.negative?
|
|
||||||
ex = TotalTimeoutError.new(@total_timeout, "Timed out after #{@total_timeout} seconds")
|
|
||||||
ex.set_backtrace(caller)
|
|
||||||
on_error(ex)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
return elapsed_time
|
|
||||||
end
|
|
||||||
|
|
||||||
return @timeout if defined?(@timeout)
|
return @timeout if defined?(@timeout)
|
||||||
|
|
||||||
return @options.timeout[:connect_timeout] if @state == :idle
|
return @options.timeout[:connect_timeout] if @state == :idle
|
||||||
@ -604,6 +587,9 @@ module HTTPX
|
|||||||
purge_after_closed
|
purge_after_closed
|
||||||
when :already_open
|
when :already_open
|
||||||
nextstate = :open
|
nextstate = :open
|
||||||
|
# the first check for given io readiness must still use a timeout.
|
||||||
|
# connect is the reasonable choice in such a case.
|
||||||
|
@timeout = @options.timeout[:connect_timeout]
|
||||||
send_pending
|
send_pending
|
||||||
when :active
|
when :active
|
||||||
return unless @state == :inactive
|
return unless @state == :inactive
|
||||||
@ -643,23 +629,16 @@ module HTTPX
|
|||||||
def on_error(error)
|
def on_error(error)
|
||||||
if error.instance_of?(TimeoutError)
|
if error.instance_of?(TimeoutError)
|
||||||
|
|
||||||
if @total_timeout && @connected_at &&
|
# inactive connections do not contribute to the select loop, therefore
|
||||||
Utils.elapsed_time(@connected_at) > @total_timeout
|
# they should not fail due to such errors.
|
||||||
ex = TotalTimeoutError.new(@total_timeout, "Timed out after #{@total_timeout} seconds")
|
return if @state == :inactive
|
||||||
ex.set_backtrace(error.backtrace)
|
|
||||||
error = ex
|
|
||||||
else
|
|
||||||
# inactive connections do not contribute to the select loop, therefore
|
|
||||||
# they should not fail due to such errors.
|
|
||||||
return if @state == :inactive
|
|
||||||
|
|
||||||
if @timeout
|
if @timeout
|
||||||
@timeout -= error.timeout
|
@timeout -= error.timeout
|
||||||
return unless @timeout <= 0
|
return unless @timeout <= 0
|
||||||
end
|
|
||||||
|
|
||||||
error = error.to_connection_error if connecting?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
error = error.to_connection_error if connecting?
|
||||||
end
|
end
|
||||||
handle_error(error)
|
handle_error(error)
|
||||||
reset
|
reset
|
||||||
|
@ -51,8 +51,6 @@ module HTTPX
|
|||||||
# non-canonical domain.
|
# non-canonical domain.
|
||||||
attr_reader :domain
|
attr_reader :domain
|
||||||
|
|
||||||
DOT = "." # :nodoc:
|
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def new(domain)
|
def new(domain)
|
||||||
return domain if domain.is_a?(self)
|
return domain if domain.is_a?(self)
|
||||||
@ -63,7 +61,7 @@ module HTTPX
|
|||||||
# Normalizes a _domain_ using the Punycode algorithm as necessary.
|
# Normalizes a _domain_ using the Punycode algorithm as necessary.
|
||||||
# The result will be a downcased, ASCII-only string.
|
# The result will be a downcased, ASCII-only string.
|
||||||
def normalize(domain)
|
def normalize(domain)
|
||||||
domain = domain.chomp(DOT).unicode_normalize(:nfc) unless domain.ascii_only?
|
domain = domain.chomp(".").unicode_normalize(:nfc) unless domain.ascii_only?
|
||||||
Punycode.encode_hostname(domain).downcase
|
Punycode.encode_hostname(domain).downcase
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -73,7 +71,7 @@ module HTTPX
|
|||||||
def initialize(hostname)
|
def initialize(hostname)
|
||||||
hostname = String(hostname)
|
hostname = String(hostname)
|
||||||
|
|
||||||
raise ArgumentError, "domain name must not start with a dot: #{hostname}" if hostname.start_with?(DOT)
|
raise ArgumentError, "domain name must not start with a dot: #{hostname}" if hostname.start_with?(".")
|
||||||
|
|
||||||
begin
|
begin
|
||||||
@ipaddr = IPAddr.new(hostname)
|
@ipaddr = IPAddr.new(hostname)
|
||||||
@ -84,7 +82,7 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
|
|
||||||
@hostname = DomainName.normalize(hostname)
|
@hostname = DomainName.normalize(hostname)
|
||||||
tld = if (last_dot = @hostname.rindex(DOT))
|
tld = if (last_dot = @hostname.rindex("."))
|
||||||
@hostname[(last_dot + 1)..-1]
|
@hostname[(last_dot + 1)..-1]
|
||||||
else
|
else
|
||||||
@hostname
|
@hostname
|
||||||
@ -94,7 +92,7 @@ module HTTPX
|
|||||||
@domain = if last_dot
|
@domain = if last_dot
|
||||||
# fallback - accept cookies down to second level
|
# fallback - accept cookies down to second level
|
||||||
# cf. http://www.dkim-reputation.org/regdom-libs/
|
# cf. http://www.dkim-reputation.org/regdom-libs/
|
||||||
if (penultimate_dot = @hostname.rindex(DOT, last_dot - 1))
|
if (penultimate_dot = @hostname.rindex(".", last_dot - 1))
|
||||||
@hostname[(penultimate_dot + 1)..-1]
|
@hostname[(penultimate_dot + 1)..-1]
|
||||||
else
|
else
|
||||||
@hostname
|
@hostname
|
||||||
@ -126,17 +124,12 @@ module HTTPX
|
|||||||
@domain && self <= domain && domain <= @domain
|
@domain && self <= domain && domain <= @domain
|
||||||
end
|
end
|
||||||
|
|
||||||
# def ==(other)
|
|
||||||
# other = DomainName.new(other)
|
|
||||||
# other.hostname == @hostname
|
|
||||||
# end
|
|
||||||
|
|
||||||
def <=>(other)
|
def <=>(other)
|
||||||
other = DomainName.new(other)
|
other = DomainName.new(other)
|
||||||
othername = other.hostname
|
othername = other.hostname
|
||||||
if othername == @hostname
|
if othername == @hostname
|
||||||
0
|
0
|
||||||
elsif @hostname.end_with?(othername) && @hostname[-othername.size - 1, 1] == DOT
|
elsif @hostname.end_with?(othername) && @hostname[-othername.size - 1, 1] == "."
|
||||||
# The other is higher
|
# The other is higher
|
||||||
-1
|
-1
|
||||||
else
|
else
|
||||||
|
@ -22,8 +22,6 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class TotalTimeoutError < TimeoutError; end
|
|
||||||
|
|
||||||
class ConnectTimeoutError < TimeoutError; end
|
class ConnectTimeoutError < TimeoutError; end
|
||||||
|
|
||||||
class RequestTimeoutError < TimeoutError
|
class RequestTimeoutError < TimeoutError
|
||||||
|
@ -3,96 +3,6 @@
|
|||||||
require "uri"
|
require "uri"
|
||||||
|
|
||||||
module HTTPX
|
module HTTPX
|
||||||
unless Method.method_defined?(:curry)
|
|
||||||
|
|
||||||
# Backport
|
|
||||||
#
|
|
||||||
# Ruby 2.1 and lower implement curry only for Procs.
|
|
||||||
#
|
|
||||||
# Why not using Refinements? Because they don't work for Method (tested with ruby 2.1.9).
|
|
||||||
#
|
|
||||||
module CurryMethods
|
|
||||||
# Backport for the Method#curry method, which is part of ruby core since 2.2 .
|
|
||||||
#
|
|
||||||
def curry(*args)
|
|
||||||
to_proc.curry(*args)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Method.__send__(:include, CurryMethods)
|
|
||||||
end
|
|
||||||
|
|
||||||
unless String.method_defined?(:+@)
|
|
||||||
# Backport for +"", to initialize unfrozen strings from the string literal.
|
|
||||||
#
|
|
||||||
module LiteralStringExtensions
|
|
||||||
def +@
|
|
||||||
frozen? ? dup : self
|
|
||||||
end
|
|
||||||
end
|
|
||||||
String.__send__(:include, LiteralStringExtensions)
|
|
||||||
end
|
|
||||||
|
|
||||||
unless Numeric.method_defined?(:positive?)
|
|
||||||
# Ruby 2.3 Backport (Numeric#positive?)
|
|
||||||
#
|
|
||||||
module PosMethods
|
|
||||||
def positive?
|
|
||||||
self > 0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Numeric.__send__(:include, PosMethods)
|
|
||||||
end
|
|
||||||
|
|
||||||
unless Numeric.method_defined?(:negative?)
|
|
||||||
# Ruby 2.3 Backport (Numeric#negative?)
|
|
||||||
#
|
|
||||||
module NegMethods
|
|
||||||
def negative?
|
|
||||||
self < 0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Numeric.__send__(:include, NegMethods)
|
|
||||||
end
|
|
||||||
|
|
||||||
module NumericExtensions
|
|
||||||
# Ruby 2.4 backport
|
|
||||||
refine Numeric do
|
|
||||||
def infinite?
|
|
||||||
self == Float::INFINITY
|
|
||||||
end unless Numeric.method_defined?(:infinite?)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module StringExtensions
|
|
||||||
refine String do
|
|
||||||
# Ruby 2.5 backport
|
|
||||||
def delete_suffix!(suffix)
|
|
||||||
suffix = Backports.coerce_to_str(suffix)
|
|
||||||
chomp! if frozen?
|
|
||||||
len = suffix.length
|
|
||||||
if len > 0 && index(suffix, -len)
|
|
||||||
self[-len..-1] = ''
|
|
||||||
self
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end unless String.method_defined?(:delete_suffix!)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module HashExtensions
|
|
||||||
refine Hash do
|
|
||||||
# Ruby 2.4 backport
|
|
||||||
def compact
|
|
||||||
h = {}
|
|
||||||
each do |key, value|
|
|
||||||
h[key] = value unless value == nil
|
|
||||||
end
|
|
||||||
h
|
|
||||||
end unless Hash.method_defined?(:compact)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module ArrayExtensions
|
module ArrayExtensions
|
||||||
module FilterMap
|
module FilterMap
|
||||||
refine Array do
|
refine Array do
|
||||||
@ -108,16 +18,6 @@ module HTTPX
|
|||||||
end unless Array.method_defined?(:filter_map)
|
end unless Array.method_defined?(:filter_map)
|
||||||
end
|
end
|
||||||
|
|
||||||
module Sum
|
|
||||||
refine Array do
|
|
||||||
# Ruby 2.6 backport
|
|
||||||
def sum(accumulator = 0, &block)
|
|
||||||
values = block_given? ? map(&block) : self
|
|
||||||
values.inject(accumulator, :+)
|
|
||||||
end
|
|
||||||
end unless Array.method_defined?(:sum)
|
|
||||||
end
|
|
||||||
|
|
||||||
module Intersect
|
module Intersect
|
||||||
refine Array do
|
refine Array do
|
||||||
# Ruby 3.1 backport
|
# Ruby 3.1 backport
|
||||||
@ -133,30 +33,6 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
module IOExtensions
|
|
||||||
refine IO do
|
|
||||||
# Ruby 2.3 backport
|
|
||||||
# provides a fallback for rubies where IO#wait isn't implemented,
|
|
||||||
# but IO#wait_readable and IO#wait_writable are.
|
|
||||||
def wait(timeout = nil, _mode = :read_write)
|
|
||||||
r, w = IO.select([self], [self], nil, timeout)
|
|
||||||
|
|
||||||
return unless r || w
|
|
||||||
|
|
||||||
self
|
|
||||||
end unless IO.method_defined?(:wait) && IO.instance_method(:wait).arity == 2
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module RegexpExtensions
|
|
||||||
refine(Regexp) do
|
|
||||||
# Ruby 2.4 backport
|
|
||||||
def match?(*args)
|
|
||||||
!match(*args).nil?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module URIExtensions
|
module URIExtensions
|
||||||
# uri 0.11 backport, ships with ruby 3.1
|
# uri 0.11 backport, ships with ruby 3.1
|
||||||
refine URI::Generic do
|
refine URI::Generic do
|
||||||
|
@ -6,15 +6,11 @@ module HTTPX
|
|||||||
TLSError = OpenSSL::SSL::SSLError
|
TLSError = OpenSSL::SSL::SSLError
|
||||||
|
|
||||||
class SSL < TCP
|
class SSL < TCP
|
||||||
using RegexpExtensions unless Regexp.method_defined?(:match?)
|
# rubocop:disable Style/MutableConstant
|
||||||
|
TLS_OPTIONS = { alpn_protocols: %w[h2 http/1.1].freeze }
|
||||||
TLS_OPTIONS = if OpenSSL::SSL::SSLContext.instance_methods.include?(:alpn_protocols)
|
|
||||||
{ alpn_protocols: %w[h2 http/1.1].freeze }
|
|
||||||
else
|
|
||||||
{}
|
|
||||||
end
|
|
||||||
# https://github.com/jruby/jruby-openssl/issues/284
|
# https://github.com/jruby/jruby-openssl/issues/284
|
||||||
TLS_OPTIONS[:verify_hostname] = true if RUBY_ENGINE == "jruby"
|
TLS_OPTIONS[:verify_hostname] = true if RUBY_ENGINE == "jruby"
|
||||||
|
# rubocop:enable Style/MutableConstant
|
||||||
TLS_OPTIONS.freeze
|
TLS_OPTIONS.freeze
|
||||||
|
|
||||||
attr_writer :ssl_session
|
attr_writer :ssl_session
|
||||||
@ -103,61 +99,18 @@ module HTTPX
|
|||||||
try_ssl_connect
|
try_ssl_connect
|
||||||
end
|
end
|
||||||
|
|
||||||
if RUBY_VERSION < "2.3"
|
def try_ssl_connect
|
||||||
# :nocov:
|
case @io.connect_nonblock(exception: false)
|
||||||
def try_ssl_connect
|
when :wait_readable
|
||||||
@io.connect_nonblock
|
|
||||||
@io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE && @verify_hostname
|
|
||||||
transition(:negotiated)
|
|
||||||
@interests = :w
|
|
||||||
rescue ::IO::WaitReadable
|
|
||||||
@interests = :r
|
@interests = :r
|
||||||
rescue ::IO::WaitWritable
|
return
|
||||||
|
when :wait_writable
|
||||||
@interests = :w
|
@interests = :w
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
@io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE && @verify_hostname
|
||||||
def read(_, buffer)
|
transition(:negotiated)
|
||||||
super
|
@interests = :w
|
||||||
rescue ::IO::WaitWritable
|
|
||||||
buffer.clear
|
|
||||||
0
|
|
||||||
end
|
|
||||||
|
|
||||||
def write(*)
|
|
||||||
super
|
|
||||||
rescue ::IO::WaitReadable
|
|
||||||
0
|
|
||||||
end
|
|
||||||
# :nocov:
|
|
||||||
else
|
|
||||||
def try_ssl_connect
|
|
||||||
case @io.connect_nonblock(exception: false)
|
|
||||||
when :wait_readable
|
|
||||||
@interests = :r
|
|
||||||
return
|
|
||||||
when :wait_writable
|
|
||||||
@interests = :w
|
|
||||||
return
|
|
||||||
end
|
|
||||||
@io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE && @verify_hostname
|
|
||||||
transition(:negotiated)
|
|
||||||
@interests = :w
|
|
||||||
end
|
|
||||||
|
|
||||||
# :nocov:
|
|
||||||
if OpenSSL::VERSION < "2.0.6"
|
|
||||||
def read(size, buffer)
|
|
||||||
@io.read_nonblock(size, buffer)
|
|
||||||
buffer.bytesize
|
|
||||||
rescue ::IO::WaitReadable,
|
|
||||||
::IO::WaitWritable
|
|
||||||
buffer.clear
|
|
||||||
0
|
|
||||||
rescue EOFError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
# :nocov:
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -94,84 +94,43 @@ module HTTPX
|
|||||||
retry
|
retry
|
||||||
end
|
end
|
||||||
|
|
||||||
if RUBY_VERSION < "2.3"
|
def try_connect
|
||||||
# :nocov:
|
case @io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s), exception: false)
|
||||||
def try_connect
|
when :wait_readable
|
||||||
@io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s))
|
|
||||||
rescue ::IO::WaitWritable, Errno::EALREADY
|
|
||||||
@interests = :w
|
|
||||||
rescue ::IO::WaitReadable
|
|
||||||
@interests = :r
|
@interests = :r
|
||||||
rescue Errno::EISCONN
|
return
|
||||||
transition(:connected)
|
when :wait_writable
|
||||||
@interests = :w
|
|
||||||
else
|
|
||||||
transition(:connected)
|
|
||||||
@interests = :w
|
@interests = :w
|
||||||
|
return
|
||||||
end
|
end
|
||||||
private :try_connect
|
transition(:connected)
|
||||||
|
@interests = :w
|
||||||
|
rescue Errno::EALREADY
|
||||||
|
@interests = :w
|
||||||
|
end
|
||||||
|
private :try_connect
|
||||||
|
|
||||||
def read(size, buffer)
|
def read(size, buffer)
|
||||||
@io.read_nonblock(size, buffer)
|
ret = @io.read_nonblock(size, buffer, exception: false)
|
||||||
log { "READ: #{buffer.bytesize} bytes..." }
|
if ret == :wait_readable
|
||||||
buffer.bytesize
|
|
||||||
rescue ::IO::WaitReadable
|
|
||||||
buffer.clear
|
buffer.clear
|
||||||
0
|
return 0
|
||||||
rescue EOFError
|
|
||||||
nil
|
|
||||||
end
|
end
|
||||||
|
return if ret.nil?
|
||||||
|
|
||||||
def write(buffer)
|
log { "READ: #{buffer.bytesize} bytes..." }
|
||||||
siz = @io.write_nonblock(buffer)
|
buffer.bytesize
|
||||||
log { "WRITE: #{siz} bytes..." }
|
end
|
||||||
buffer.shift!(siz)
|
|
||||||
siz
|
|
||||||
rescue ::IO::WaitWritable
|
|
||||||
0
|
|
||||||
rescue EOFError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
# :nocov:
|
|
||||||
else
|
|
||||||
def try_connect
|
|
||||||
case @io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s), exception: false)
|
|
||||||
when :wait_readable
|
|
||||||
@interests = :r
|
|
||||||
return
|
|
||||||
when :wait_writable
|
|
||||||
@interests = :w
|
|
||||||
return
|
|
||||||
end
|
|
||||||
transition(:connected)
|
|
||||||
@interests = :w
|
|
||||||
rescue Errno::EALREADY
|
|
||||||
@interests = :w
|
|
||||||
end
|
|
||||||
private :try_connect
|
|
||||||
|
|
||||||
def read(size, buffer)
|
def write(buffer)
|
||||||
ret = @io.read_nonblock(size, buffer, exception: false)
|
siz = @io.write_nonblock(buffer, exception: false)
|
||||||
if ret == :wait_readable
|
return 0 if siz == :wait_writable
|
||||||
buffer.clear
|
return if siz.nil?
|
||||||
return 0
|
|
||||||
end
|
|
||||||
return if ret.nil?
|
|
||||||
|
|
||||||
log { "READ: #{buffer.bytesize} bytes..." }
|
log { "WRITE: #{siz} bytes..." }
|
||||||
buffer.bytesize
|
|
||||||
end
|
|
||||||
|
|
||||||
def write(buffer)
|
buffer.shift!(siz)
|
||||||
siz = @io.write_nonblock(buffer, exception: false)
|
siz
|
||||||
return 0 if siz == :wait_writable
|
|
||||||
return if siz.nil?
|
|
||||||
|
|
||||||
log { "WRITE: #{siz} bytes..." }
|
|
||||||
|
|
||||||
buffer.shift!(siz)
|
|
||||||
siz
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def close
|
def close
|
||||||
|
@ -23,45 +23,19 @@ module HTTPX
|
|||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
if RUBY_VERSION < "2.3"
|
def close
|
||||||
# :nocov:
|
@io.close
|
||||||
def close
|
|
||||||
@io.close
|
|
||||||
rescue StandardError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
# :nocov:
|
|
||||||
else
|
|
||||||
def close
|
|
||||||
@io.close
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# :nocov:
|
if RUBY_ENGINE == "jruby"
|
||||||
if (RUBY_ENGINE == "truffleruby" && RUBY_ENGINE_VERSION < "21.1.0") ||
|
# In JRuby, sendmsg_nonblock is not implemented
|
||||||
RUBY_VERSION < "2.3"
|
|
||||||
def write(buffer)
|
def write(buffer)
|
||||||
siz = @io.sendmsg_nonblock(buffer.to_s, 0, Socket.sockaddr_in(@port, @host.to_s))
|
siz = @io.send(buffer.to_s, 0, @host, @port)
|
||||||
log { "WRITE: #{siz} bytes..." }
|
log { "WRITE: #{siz} bytes..." }
|
||||||
buffer.shift!(siz)
|
buffer.shift!(siz)
|
||||||
siz
|
siz
|
||||||
rescue ::IO::WaitWritable
|
|
||||||
0
|
|
||||||
rescue EOFError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def read(size, buffer)
|
|
||||||
data, _ = @io.recvfrom_nonblock(size)
|
|
||||||
buffer.replace(data)
|
|
||||||
log { "READ: #{buffer.bytesize} bytes..." }
|
|
||||||
buffer.bytesize
|
|
||||||
rescue ::IO::WaitReadable
|
|
||||||
0
|
|
||||||
rescue IOError
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
|
||||||
def write(buffer)
|
def write(buffer)
|
||||||
siz = @io.sendmsg_nonblock(buffer.to_s, 0, Socket.sockaddr_in(@port, @host.to_s), exception: false)
|
siz = @io.sendmsg_nonblock(buffer.to_s, 0, Socket.sockaddr_in(@port, @host.to_s), exception: false)
|
||||||
return 0 if siz == :wait_writable
|
return 0 if siz == :wait_writable
|
||||||
@ -72,26 +46,17 @@ module HTTPX
|
|||||||
buffer.shift!(siz)
|
buffer.shift!(siz)
|
||||||
siz
|
siz
|
||||||
end
|
end
|
||||||
|
|
||||||
def read(size, buffer)
|
|
||||||
ret = @io.recvfrom_nonblock(size, 0, buffer, exception: false)
|
|
||||||
return 0 if ret == :wait_readable
|
|
||||||
return if ret.nil?
|
|
||||||
|
|
||||||
log { "READ: #{buffer.bytesize} bytes..." }
|
|
||||||
|
|
||||||
buffer.bytesize
|
|
||||||
rescue IOError
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# In JRuby, sendmsg_nonblock is not implemented
|
def read(size, buffer)
|
||||||
def write(buffer)
|
ret = @io.recvfrom_nonblock(size, 0, buffer, exception: false)
|
||||||
siz = @io.send(buffer.to_s, 0, @host, @port)
|
return 0 if ret == :wait_readable
|
||||||
log { "WRITE: #{siz} bytes..." }
|
return if ret.nil?
|
||||||
buffer.shift!(siz)
|
|
||||||
siz
|
log { "READ: #{buffer.bytesize} bytes..." }
|
||||||
end if RUBY_ENGINE == "jruby"
|
|
||||||
# :nocov:
|
buffer.bytesize
|
||||||
|
rescue IOError
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -27,14 +27,7 @@ module HTTPX
|
|||||||
@keep_open = true
|
@keep_open = true
|
||||||
@state = :connected
|
@state = :connected
|
||||||
else
|
else
|
||||||
if @options.transport_options
|
@path = addresses.first
|
||||||
# :nocov:
|
|
||||||
warn ":transport_options is deprecated, use :addresses instead"
|
|
||||||
@path = @options.transport_options[:path]
|
|
||||||
# :nocov:
|
|
||||||
else
|
|
||||||
@path = addresses.first
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
@io ||= build_socket
|
@io ||= build_socket
|
||||||
end
|
end
|
||||||
|
@ -24,26 +24,11 @@ module HTTPX
|
|||||||
debug_stream << message
|
debug_stream << message
|
||||||
end
|
end
|
||||||
|
|
||||||
if Exception.instance_methods.include?(:full_message)
|
def log_exception(ex, level: @options.debug_level, color: nil)
|
||||||
|
return unless @options.debug
|
||||||
def log_exception(ex, level: @options.debug_level, color: nil)
|
return unless @options.debug_level >= level
|
||||||
return unless @options.debug
|
|
||||||
return unless @options.debug_level >= level
|
|
||||||
|
|
||||||
log(level: level, color: color) { ex.full_message }
|
|
||||||
end
|
|
||||||
|
|
||||||
else
|
|
||||||
|
|
||||||
def log_exception(ex, level: @options.debug_level, color: nil)
|
|
||||||
return unless @options.debug
|
|
||||||
return unless @options.debug_level >= level
|
|
||||||
|
|
||||||
message = +"#{ex.message} (#{ex.class})"
|
|
||||||
message << "\n" << ex.backtrace.join("\n") unless ex.backtrace.nil?
|
|
||||||
log(level: level, color: color) { message }
|
|
||||||
end
|
|
||||||
|
|
||||||
|
log(level: level, color: color) { ex.full_message }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -7,11 +7,10 @@ module HTTPX
|
|||||||
BUFFER_SIZE = 1 << 14
|
BUFFER_SIZE = 1 << 14
|
||||||
WINDOW_SIZE = 1 << 14 # 16K
|
WINDOW_SIZE = 1 << 14 # 16K
|
||||||
MAX_BODY_THRESHOLD_SIZE = (1 << 10) * 112 # 112K
|
MAX_BODY_THRESHOLD_SIZE = (1 << 10) * 112 # 112K
|
||||||
CONNECT_TIMEOUT = 60
|
|
||||||
OPERATION_TIMEOUT = 60
|
|
||||||
KEEP_ALIVE_TIMEOUT = 20
|
KEEP_ALIVE_TIMEOUT = 20
|
||||||
SETTINGS_TIMEOUT = 10
|
SETTINGS_TIMEOUT = 10
|
||||||
READ_TIMEOUT = WRITE_TIMEOUT = REQUEST_TIMEOUT = Float::INFINITY
|
CONNECT_TIMEOUT = READ_TIMEOUT = WRITE_TIMEOUT = 60
|
||||||
|
REQUEST_TIMEOUT = OPERATION_TIMEOUT = Float::INFINITY
|
||||||
|
|
||||||
# https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
|
# https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
|
||||||
ip_address_families = begin
|
ip_address_families = begin
|
||||||
@ -31,6 +30,9 @@ module HTTPX
|
|||||||
:ssl => {},
|
:ssl => {},
|
||||||
:http2_settings => { settings_enable_push: 0 },
|
:http2_settings => { settings_enable_push: 0 },
|
||||||
:fallback_protocol => "http/1.1",
|
:fallback_protocol => "http/1.1",
|
||||||
|
:supported_compression_formats => %w[gzip deflate],
|
||||||
|
:decompress_response_body => true,
|
||||||
|
:compress_request_body => true,
|
||||||
:timeout => {
|
:timeout => {
|
||||||
connect_timeout: CONNECT_TIMEOUT,
|
connect_timeout: CONNECT_TIMEOUT,
|
||||||
settings_timeout: SETTINGS_TIMEOUT,
|
settings_timeout: SETTINGS_TIMEOUT,
|
||||||
@ -52,7 +54,6 @@ module HTTPX
|
|||||||
:connection_class => Class.new(Connection),
|
:connection_class => Class.new(Connection),
|
||||||
:options_class => Class.new(self),
|
:options_class => Class.new(self),
|
||||||
:transport => nil,
|
:transport => nil,
|
||||||
:transport_options => nil,
|
|
||||||
:addresses => nil,
|
:addresses => nil,
|
||||||
:persistent => false,
|
:persistent => false,
|
||||||
:resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym,
|
:resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym,
|
||||||
@ -60,28 +61,6 @@ module HTTPX
|
|||||||
:ip_families => ip_address_families,
|
:ip_families => ip_address_families,
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
begin
|
|
||||||
module HashExtensions
|
|
||||||
refine Hash do
|
|
||||||
def >=(other)
|
|
||||||
Hash[other] <= self
|
|
||||||
end
|
|
||||||
|
|
||||||
def <=(other)
|
|
||||||
other = Hash[other]
|
|
||||||
return false unless size <= other.size
|
|
||||||
|
|
||||||
each do |k, v|
|
|
||||||
v2 = other.fetch(k) { return false }
|
|
||||||
return false unless v2 == v
|
|
||||||
end
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
using HashExtensions
|
|
||||||
end unless Hash.method_defined?(:>=)
|
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def new(options = {})
|
def new(options = {})
|
||||||
# let enhanced options go through
|
# let enhanced options go through
|
||||||
@ -100,38 +79,10 @@ module HTTPX
|
|||||||
|
|
||||||
attr_reader(optname)
|
attr_reader(optname)
|
||||||
end
|
end
|
||||||
|
|
||||||
def def_option(optname, *args, &block)
|
|
||||||
if args.empty? && !block
|
|
||||||
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
|
||||||
def option_#{optname}(v); v; end # def option_smth(v); v; end
|
|
||||||
OUT
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
deprecated_def_option(optname, *args, &block)
|
|
||||||
end
|
|
||||||
|
|
||||||
def deprecated_def_option(optname, layout = nil, &interpreter)
|
|
||||||
warn "DEPRECATION WARNING: using `def_option(#{optname})` for setting options is deprecated. " \
|
|
||||||
"Define module OptionsMethods and `def option_#{optname}(val)` instead."
|
|
||||||
|
|
||||||
if layout
|
|
||||||
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
|
||||||
def option_#{optname}(value) # def option_origin(v)
|
|
||||||
#{layout} # URI(v)
|
|
||||||
end # end
|
|
||||||
OUT
|
|
||||||
elsif interpreter
|
|
||||||
define_method(:"option_#{optname}") do |value|
|
|
||||||
instance_exec(value, &interpreter)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(options = {})
|
def initialize(options = {})
|
||||||
__initialize__(options)
|
do_initialize(options)
|
||||||
freeze
|
freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -142,6 +93,7 @@ module HTTPX
|
|||||||
@timeout.freeze
|
@timeout.freeze
|
||||||
@headers.freeze
|
@headers.freeze
|
||||||
@addresses.freeze
|
@addresses.freeze
|
||||||
|
@supported_compression_formats.freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
def option_origin(value)
|
def option_origin(value)
|
||||||
@ -157,14 +109,11 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
|
|
||||||
def option_timeout(value)
|
def option_timeout(value)
|
||||||
timeouts = Hash[value]
|
Hash[value]
|
||||||
|
end
|
||||||
|
|
||||||
if timeouts.key?(:loop_timeout)
|
def option_supported_compression_formats(value)
|
||||||
warn ":loop_timeout is deprecated, use :operation_timeout instead"
|
Array(value).map(&:to_s)
|
||||||
timeouts[:operation_timeout] = timeouts.delete(:loop_timeout)
|
|
||||||
end
|
|
||||||
|
|
||||||
timeouts
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def option_max_concurrent_requests(value)
|
def option_max_concurrent_requests(value)
|
||||||
@ -196,7 +145,10 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
|
|
||||||
def option_body_threshold_size(value)
|
def option_body_threshold_size(value)
|
||||||
Integer(value)
|
bytes = Integer(value)
|
||||||
|
raise TypeError, ":body_threshold_size must be positive" unless bytes.positive?
|
||||||
|
|
||||||
|
bytes
|
||||||
end
|
end
|
||||||
|
|
||||||
def option_transport(value)
|
def option_transport(value)
|
||||||
@ -218,10 +170,13 @@ module HTTPX
|
|||||||
params form json xml body ssl http2_settings
|
params form json xml body ssl http2_settings
|
||||||
request_class response_class headers_class request_body_class
|
request_class response_class headers_class request_body_class
|
||||||
response_body_class connection_class options_class
|
response_body_class connection_class options_class
|
||||||
io fallback_protocol debug debug_level transport_options resolver_class resolver_options
|
io fallback_protocol debug debug_level resolver_class resolver_options
|
||||||
|
compress_request_body decompress_response_body
|
||||||
persistent
|
persistent
|
||||||
].each do |method_name|
|
].each do |method_name|
|
||||||
def_option(method_name)
|
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
||||||
|
def option_#{method_name}(v); v; end # def option_smth(v); v; end
|
||||||
|
OUT
|
||||||
end
|
end
|
||||||
|
|
||||||
REQUEST_IVARS = %i[@params @form @xml @json @body].freeze
|
REQUEST_IVARS = %i[@params @form @xml @json @body].freeze
|
||||||
@ -270,30 +225,15 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if RUBY_VERSION > "2.4.0"
|
def initialize_dup(other)
|
||||||
def initialize_dup(other)
|
instance_variables.each do |ivar|
|
||||||
instance_variables.each do |ivar|
|
instance_variable_set(ivar, other.instance_variable_get(ivar).dup)
|
||||||
instance_variable_set(ivar, other.instance_variable_get(ivar).dup)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
def initialize_dup(other)
|
|
||||||
instance_variables.each do |ivar|
|
|
||||||
value = other.instance_variable_get(ivar)
|
|
||||||
value = case value
|
|
||||||
when Symbol, Numeric, TrueClass, FalseClass
|
|
||||||
value
|
|
||||||
else
|
|
||||||
value.dup
|
|
||||||
end
|
|
||||||
instance_variable_set(ivar, value)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def __initialize__(options = {})
|
def do_initialize(options = {})
|
||||||
defaults = DEFAULT_OPTIONS.merge(options)
|
defaults = DEFAULT_OPTIONS.merge(options)
|
||||||
defaults.each do |k, v|
|
defaults.each do |k, v|
|
||||||
next if v.nil?
|
next if v.nil?
|
||||||
|
25
lib/httpx/plugins/auth.rb
Normal file
25
lib/httpx/plugins/auth.rb
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module HTTPX
|
||||||
|
module Plugins
|
||||||
|
#
|
||||||
|
# This plugin adds a shim +authorization+ method to the session, which will fill
|
||||||
|
# the HTTP Authorization header, and another, +bearer_auth+, which fill the "Bearer " prefix
|
||||||
|
# in its value.
|
||||||
|
#
|
||||||
|
# https://gitlab.com/os85/httpx/wikis/Auth#authorization
|
||||||
|
#
|
||||||
|
module Auth
|
||||||
|
module InstanceMethods
|
||||||
|
def authorization(token)
|
||||||
|
with(headers: { "authorization" => token })
|
||||||
|
end
|
||||||
|
|
||||||
|
def bearer_auth(token)
|
||||||
|
authorization("Bearer #{token}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
register_plugin :auth, Auth
|
||||||
|
end
|
||||||
|
end
|
@ -11,10 +11,6 @@ module HTTPX
|
|||||||
@password = password
|
@password = password
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_authenticate?(authenticate)
|
|
||||||
authenticate && /Basic .*/.match?(authenticate)
|
|
||||||
end
|
|
||||||
|
|
||||||
def authenticate(*)
|
def authenticate(*)
|
||||||
"Basic #{Base64.strict_encode64("#{@user}:#{@password}")}"
|
"Basic #{Base64.strict_encode64("#{@user}:#{@password}")}"
|
||||||
end
|
end
|
@ -8,8 +8,6 @@ module HTTPX
|
|||||||
module Plugins
|
module Plugins
|
||||||
module Authentication
|
module Authentication
|
||||||
class Digest
|
class Digest
|
||||||
using RegexpExtensions unless Regexp.method_defined?(:match?)
|
|
||||||
|
|
||||||
def initialize(user, password, hashed: false, **)
|
def initialize(user, password, hashed: false, **)
|
||||||
@user = user
|
@user = user
|
||||||
@password = password
|
@password = password
|
||||||
@ -31,9 +29,8 @@ module HTTPX
|
|||||||
# discard first token, it's Digest
|
# discard first token, it's Digest
|
||||||
auth_info = authenticate[/^(\w+) (.*)/, 2]
|
auth_info = authenticate[/^(\w+) (.*)/, 2]
|
||||||
|
|
||||||
params = Hash[auth_info.split(/ *, */)
|
params = auth_info.split(/ *, */)
|
||||||
.map { |val| val.split("=") }
|
.to_h { |val| val.split("=") }.transform_values { |v| v.delete("\"") }
|
||||||
.map { |k, v| [k, v.delete("\"")] }]
|
|
||||||
nonce = params["nonce"]
|
nonce = params["nonce"]
|
||||||
nc = next_nonce
|
nc = next_nonce
|
||||||
|
|
@ -7,8 +7,6 @@ module HTTPX
|
|||||||
module Plugins
|
module Plugins
|
||||||
module Authentication
|
module Authentication
|
||||||
class Ntlm
|
class Ntlm
|
||||||
using RegexpExtensions unless Regexp.method_defined?(:match?)
|
|
||||||
|
|
||||||
def initialize(user, password, domain: nil)
|
def initialize(user, password, domain: nil)
|
||||||
@user = user
|
@user = user
|
||||||
@password = password
|
@password = password
|
@ -1,24 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module HTTPX
|
|
||||||
module Plugins
|
|
||||||
#
|
|
||||||
# This plugin adds a shim +authentication+ method to the session, which will fill
|
|
||||||
# the HTTP Authorization header.
|
|
||||||
#
|
|
||||||
# https://gitlab.com/os85/httpx/wikis/Authentication#authentication
|
|
||||||
#
|
|
||||||
module Authentication
|
|
||||||
module InstanceMethods
|
|
||||||
def authentication(token)
|
|
||||||
with(headers: { "authorization" => token })
|
|
||||||
end
|
|
||||||
|
|
||||||
def bearer_auth(token)
|
|
||||||
authentication("Bearer #{token}")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
register_plugin :authentication, Authentication
|
|
||||||
end
|
|
||||||
end
|
|
@ -146,7 +146,6 @@ module HTTPX
|
|||||||
|
|
||||||
def configure(klass)
|
def configure(klass)
|
||||||
klass.plugin(:expect)
|
klass.plugin(:expect)
|
||||||
klass.plugin(:compression)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -5,26 +5,25 @@ module HTTPX
|
|||||||
#
|
#
|
||||||
# This plugin adds helper methods to implement HTTP Basic Auth (https://tools.ietf.org/html/rfc7617)
|
# This plugin adds helper methods to implement HTTP Basic Auth (https://tools.ietf.org/html/rfc7617)
|
||||||
#
|
#
|
||||||
# https://gitlab.com/os85/httpx/wikis/Authentication#basic-authentication
|
# https://gitlab.com/os85/httpx/wikis/Authorization#basic-auth
|
||||||
#
|
#
|
||||||
module BasicAuth
|
module BasicAuth
|
||||||
class << self
|
class << self
|
||||||
def load_dependencies(_klass)
|
def load_dependencies(_klass)
|
||||||
require_relative "authentication/basic"
|
require_relative "auth/basic"
|
||||||
end
|
end
|
||||||
|
|
||||||
def configure(klass)
|
def configure(klass)
|
||||||
klass.plugin(:authentication)
|
klass.plugin(:auth)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
module InstanceMethods
|
module InstanceMethods
|
||||||
def basic_auth(user, password)
|
def basic_auth(user, password)
|
||||||
authentication(Authentication::Basic.new(user, password).authenticate)
|
authorization(Authentication::Basic.new(user, password).authenticate)
|
||||||
end
|
end
|
||||||
alias_method :basic_authentication, :basic_auth
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
register_plugin :basic_authentication, BasicAuth
|
register_plugin :basic_auth, BasicAuth
|
||||||
end
|
end
|
||||||
end
|
end
|
50
lib/httpx/plugins/brotli.rb
Normal file
50
lib/httpx/plugins/brotli.rb
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module HTTPX
|
||||||
|
module Plugins
|
||||||
|
module Brotli
|
||||||
|
class Deflater < Transcoder::Deflater
|
||||||
|
def deflate(chunk)
|
||||||
|
return unless chunk
|
||||||
|
|
||||||
|
::Brotli.deflate(chunk)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module RequestBodyClassMethods
|
||||||
|
def initialize_deflater_body(body, encoding)
|
||||||
|
return Brotli.encode(body) if encoding == "br"
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module ResponseBodyClassMethods
|
||||||
|
def initialize_inflater_by_encoding(encoding, response, **kwargs)
|
||||||
|
return Brotli.decode(response, **kwargs) if encoding == "br"
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def load_dependencies(*)
|
||||||
|
require "brotli"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.extra_options(options)
|
||||||
|
options.merge(supported_compression_formats: %w[br] + options.supported_compression_formats)
|
||||||
|
end
|
||||||
|
|
||||||
|
def encode(body)
|
||||||
|
Deflater.new(body)
|
||||||
|
end
|
||||||
|
|
||||||
|
def decode(_response, **)
|
||||||
|
::Brotli.method(:inflate)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
register_plugin :brotli, Brotli
|
||||||
|
end
|
||||||
|
end
|
@ -1,165 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module HTTPX
|
|
||||||
module Plugins
|
|
||||||
#
|
|
||||||
# This plugin adds compression support. Namely it:
|
|
||||||
#
|
|
||||||
# * Compresses the request body when passed a supported "Content-Encoding" mime-type;
|
|
||||||
# * Decompresses the response body from a supported "Content-Encoding" mime-type;
|
|
||||||
#
|
|
||||||
# It supports both *gzip* and *deflate*.
|
|
||||||
#
|
|
||||||
# https://gitlab.com/os85/httpx/wikis/Compression
|
|
||||||
#
|
|
||||||
module Compression
|
|
||||||
class << self
|
|
||||||
def configure(klass)
|
|
||||||
klass.plugin(:"compression/gzip")
|
|
||||||
klass.plugin(:"compression/deflate")
|
|
||||||
end
|
|
||||||
|
|
||||||
def extra_options(options)
|
|
||||||
options.merge(encodings: {})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module OptionsMethods
|
|
||||||
def option_compression_threshold_size(value)
|
|
||||||
bytes = Integer(value)
|
|
||||||
raise TypeError, ":compression_threshold_size must be positive" unless bytes.positive?
|
|
||||||
|
|
||||||
bytes
|
|
||||||
end
|
|
||||||
|
|
||||||
def option_encodings(value)
|
|
||||||
raise TypeError, ":encodings must be an Hash" unless value.is_a?(Hash)
|
|
||||||
|
|
||||||
value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module RequestMethods
|
|
||||||
def initialize(*)
|
|
||||||
super
|
|
||||||
# forego compression in the Range cases
|
|
||||||
if @headers.key?("range")
|
|
||||||
@headers.delete("accept-encoding")
|
|
||||||
else
|
|
||||||
@headers["accept-encoding"] ||= @options.encodings.keys
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module RequestBodyMethods
|
|
||||||
def initialize(*, options)
|
|
||||||
super
|
|
||||||
return if @body.nil?
|
|
||||||
|
|
||||||
threshold = options.compression_threshold_size
|
|
||||||
return if threshold && !unbounded_body? && @body.bytesize < threshold
|
|
||||||
|
|
||||||
@headers.get("content-encoding").each do |encoding|
|
|
||||||
next if encoding == "identity"
|
|
||||||
|
|
||||||
next unless options.encodings.key?(encoding)
|
|
||||||
|
|
||||||
@body = Encoder.new(@body, options.encodings[encoding].deflater)
|
|
||||||
end
|
|
||||||
@headers["content-length"] = @body.bytesize unless unbounded_body?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module ResponseBodyMethods
|
|
||||||
using ArrayExtensions::FilterMap
|
|
||||||
|
|
||||||
attr_reader :encodings
|
|
||||||
|
|
||||||
def initialize(*)
|
|
||||||
@encodings = []
|
|
||||||
|
|
||||||
super
|
|
||||||
|
|
||||||
return unless @headers.key?("content-encoding")
|
|
||||||
|
|
||||||
# remove encodings that we are able to decode
|
|
||||||
@headers["content-encoding"] = @headers.get("content-encoding") - @encodings
|
|
||||||
|
|
||||||
compressed_length = if @headers.key?("content-length")
|
|
||||||
@headers["content-length"].to_i
|
|
||||||
else
|
|
||||||
Float::INFINITY
|
|
||||||
end
|
|
||||||
|
|
||||||
@_inflaters = @headers.get("content-encoding").filter_map do |encoding|
|
|
||||||
next if encoding == "identity"
|
|
||||||
|
|
||||||
next unless @options.encodings.key?(encoding)
|
|
||||||
|
|
||||||
inflater = @options.encodings[encoding].inflater(compressed_length)
|
|
||||||
# do not uncompress if there is no decoder available. In fact, we can't reliably
|
|
||||||
# continue decompressing beyond that, so ignore.
|
|
||||||
break unless inflater
|
|
||||||
|
|
||||||
@encodings << encoding
|
|
||||||
inflater
|
|
||||||
end
|
|
||||||
|
|
||||||
# this can happen if the only declared encoding is "identity"
|
|
||||||
remove_instance_variable(:@_inflaters) if @_inflaters.empty?
|
|
||||||
end
|
|
||||||
|
|
||||||
def write(chunk)
|
|
||||||
return super unless defined?(@_inflaters) && !chunk.empty?
|
|
||||||
|
|
||||||
chunk = decompress(chunk)
|
|
||||||
super(chunk)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def decompress(buffer)
|
|
||||||
@_inflaters.reverse_each do |inflater|
|
|
||||||
buffer = inflater.inflate(buffer)
|
|
||||||
end
|
|
||||||
buffer
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class Encoder
|
|
||||||
attr_reader :content_type
|
|
||||||
|
|
||||||
def initialize(body, deflater)
|
|
||||||
@content_type = body.content_type
|
|
||||||
@body = body.respond_to?(:read) ? body : StringIO.new(body.to_s)
|
|
||||||
@buffer = StringIO.new("".b, File::RDWR)
|
|
||||||
@deflater = deflater
|
|
||||||
end
|
|
||||||
|
|
||||||
def each(&blk)
|
|
||||||
return enum_for(__method__) unless blk
|
|
||||||
|
|
||||||
return deflate(&blk) if @buffer.size.zero? # rubocop:disable Style/ZeroLengthPredicate
|
|
||||||
|
|
||||||
@buffer.rewind
|
|
||||||
@buffer.each(&blk)
|
|
||||||
end
|
|
||||||
|
|
||||||
def bytesize
|
|
||||||
deflate
|
|
||||||
@buffer.size
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def deflate(&blk)
|
|
||||||
return unless @buffer.size.zero? # rubocop:disable Style/ZeroLengthPredicate
|
|
||||||
|
|
||||||
@body.rewind
|
|
||||||
@deflater.deflate(@body, @buffer, chunk_size: 16_384, &blk)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
register_plugin :compression, Compression
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,54 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module HTTPX
|
|
||||||
module Plugins
|
|
||||||
module Compression
|
|
||||||
module Brotli
|
|
||||||
class << self
|
|
||||||
def load_dependencies(klass)
|
|
||||||
require "brotli"
|
|
||||||
klass.plugin(:compression)
|
|
||||||
end
|
|
||||||
|
|
||||||
def extra_options(options)
|
|
||||||
options.merge(encodings: options.encodings.merge("br" => self))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module Deflater
|
|
||||||
module_function
|
|
||||||
|
|
||||||
def deflate(raw, buffer = "".b, chunk_size: 16_384)
|
|
||||||
while (chunk = raw.read(chunk_size))
|
|
||||||
compressed = ::Brotli.deflate(chunk)
|
|
||||||
buffer << compressed
|
|
||||||
yield compressed if block_given?
|
|
||||||
end
|
|
||||||
buffer
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class Inflater
|
|
||||||
def initialize(bytesize)
|
|
||||||
@bytesize = bytesize
|
|
||||||
end
|
|
||||||
|
|
||||||
def inflate(chunk)
|
|
||||||
::Brotli.inflate(chunk)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module_function
|
|
||||||
|
|
||||||
def deflater
|
|
||||||
Deflater
|
|
||||||
end
|
|
||||||
|
|
||||||
def inflater(bytesize)
|
|
||||||
Inflater.new(bytesize)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
register_plugin :"compression/brotli", Compression::Brotli
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,54 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module HTTPX
|
|
||||||
module Plugins
|
|
||||||
module Compression
|
|
||||||
module Deflate
|
|
||||||
class << self
|
|
||||||
def load_dependencies(_klass)
|
|
||||||
require "stringio"
|
|
||||||
require "zlib"
|
|
||||||
end
|
|
||||||
|
|
||||||
def configure(klass)
|
|
||||||
klass.plugin(:"compression/gzip")
|
|
||||||
end
|
|
||||||
|
|
||||||
def extra_options(options)
|
|
||||||
options.merge(encodings: options.encodings.merge("deflate" => self))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module Deflater
|
|
||||||
module_function
|
|
||||||
|
|
||||||
def deflate(raw, buffer = "".b, chunk_size: 16_384)
|
|
||||||
deflater = Zlib::Deflate.new
|
|
||||||
while (chunk = raw.read(chunk_size))
|
|
||||||
compressed = deflater.deflate(chunk)
|
|
||||||
buffer << compressed
|
|
||||||
yield compressed if block_given?
|
|
||||||
end
|
|
||||||
last = deflater.finish
|
|
||||||
buffer << last
|
|
||||||
yield last if block_given?
|
|
||||||
buffer
|
|
||||||
ensure
|
|
||||||
deflater.close if deflater
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module_function
|
|
||||||
|
|
||||||
def deflater
|
|
||||||
Deflater
|
|
||||||
end
|
|
||||||
|
|
||||||
def inflater(bytesize)
|
|
||||||
GZIP::Inflater.new(bytesize)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
register_plugin :"compression/deflate", Compression::Deflate
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,90 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require "forwardable"
|
|
||||||
|
|
||||||
module HTTPX
|
|
||||||
module Plugins
|
|
||||||
module Compression
|
|
||||||
module GZIP
|
|
||||||
class << self
|
|
||||||
def load_dependencies(*)
|
|
||||||
require "zlib"
|
|
||||||
end
|
|
||||||
|
|
||||||
def extra_options(options)
|
|
||||||
options.merge(encodings: options.encodings.merge("gzip" => self))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class Deflater
|
|
||||||
def initialize
|
|
||||||
@compressed_chunk = "".b
|
|
||||||
end
|
|
||||||
|
|
||||||
def deflate(raw, buffer = "".b, chunk_size: 16_384)
|
|
||||||
gzip = Zlib::GzipWriter.new(self)
|
|
||||||
|
|
||||||
begin
|
|
||||||
while (chunk = raw.read(chunk_size))
|
|
||||||
gzip.write(chunk)
|
|
||||||
gzip.flush
|
|
||||||
compressed = compressed_chunk
|
|
||||||
buffer << compressed
|
|
||||||
yield compressed if block_given?
|
|
||||||
end
|
|
||||||
ensure
|
|
||||||
gzip.close
|
|
||||||
end
|
|
||||||
|
|
||||||
return unless (compressed = compressed_chunk)
|
|
||||||
|
|
||||||
buffer << compressed
|
|
||||||
yield compressed if block_given?
|
|
||||||
buffer
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def write(chunk)
|
|
||||||
@compressed_chunk << chunk
|
|
||||||
end
|
|
||||||
|
|
||||||
def compressed_chunk
|
|
||||||
@compressed_chunk.dup
|
|
||||||
ensure
|
|
||||||
@compressed_chunk.clear
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class Inflater
|
|
||||||
def initialize(bytesize)
|
|
||||||
@inflater = Zlib::Inflate.new(Zlib::MAX_WBITS + 32)
|
|
||||||
@bytesize = bytesize
|
|
||||||
@buffer = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def inflate(chunk)
|
|
||||||
buffer = @inflater.inflate(chunk)
|
|
||||||
@bytesize -= chunk.bytesize
|
|
||||||
if @bytesize <= 0
|
|
||||||
buffer << @inflater.finish
|
|
||||||
@inflater.close
|
|
||||||
end
|
|
||||||
buffer
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module_function
|
|
||||||
|
|
||||||
def deflater
|
|
||||||
Deflater.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def inflater(bytesize)
|
|
||||||
Inflater.new(bytesize)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
register_plugin :"compression/gzip", Compression::GZIP
|
|
||||||
end
|
|
||||||
end
|
|
@ -71,7 +71,7 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
|
|
||||||
module OptionsMethods
|
module OptionsMethods
|
||||||
def __initialize__(*)
|
def do_initialize(*)
|
||||||
super
|
super
|
||||||
|
|
||||||
return unless @headers.key?("cookie")
|
return unless @headers.key?("cookie")
|
||||||
|
@ -6,8 +6,6 @@ require "time"
|
|||||||
module HTTPX
|
module HTTPX
|
||||||
module Plugins::Cookies
|
module Plugins::Cookies
|
||||||
module SetCookieParser
|
module SetCookieParser
|
||||||
using(RegexpExtensions) unless Regexp.method_defined?(:match?)
|
|
||||||
|
|
||||||
# Whitespace.
|
# Whitespace.
|
||||||
RE_WSP = /[ \t]+/.freeze
|
RE_WSP = /[ \t]+/.freeze
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ module HTTPX
|
|||||||
#
|
#
|
||||||
# This plugin adds helper methods to implement HTTP Digest Auth (https://tools.ietf.org/html/rfc7616)
|
# This plugin adds helper methods to implement HTTP Digest Auth (https://tools.ietf.org/html/rfc7616)
|
||||||
#
|
#
|
||||||
# https://gitlab.com/os85/httpx/wikis/Authentication#authentication
|
# https://gitlab.com/os85/httpx/wikis/Authorization#digest-auth
|
||||||
#
|
#
|
||||||
module DigestAuth
|
module DigestAuth
|
||||||
DigestError = Class.new(Error)
|
DigestError = Class.new(Error)
|
||||||
@ -16,7 +16,7 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
|
|
||||||
def load_dependencies(*)
|
def load_dependencies(*)
|
||||||
require_relative "authentication/digest"
|
require_relative "auth/digest"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -29,11 +29,11 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
|
|
||||||
module InstanceMethods
|
module InstanceMethods
|
||||||
def digest_authentication(user, password, hashed: false)
|
def digest_auth(user, password, hashed: false)
|
||||||
with(digest: Authentication::Digest.new(user, password, hashed: hashed))
|
with(digest: Authentication::Digest.new(user, password, hashed: hashed))
|
||||||
end
|
end
|
||||||
|
|
||||||
alias_method :digest_auth, :digest_authentication
|
private
|
||||||
|
|
||||||
def send_requests(*requests)
|
def send_requests(*requests)
|
||||||
requests.flat_map do |request|
|
requests.flat_map do |request|
|
||||||
@ -57,6 +57,6 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
register_plugin :digest_authentication, DigestAuth
|
register_plugin :digest_auth, DigestAuth
|
||||||
end
|
end
|
||||||
end
|
end
|
@ -48,7 +48,27 @@ module HTTPX
|
|||||||
return response unless REDIRECT_STATUS.include?(response.status) && response.headers.key?("location")
|
return response unless REDIRECT_STATUS.include?(response.status) && response.headers.key?("location")
|
||||||
return response unless max_redirects.positive?
|
return response unless max_redirects.positive?
|
||||||
|
|
||||||
retry_request = build_redirect_request(redirect_request, response, options)
|
# build redirect request
|
||||||
|
redirect_uri = __get_location_from_response(response)
|
||||||
|
|
||||||
|
if response.status == 305 && options.respond_to?(:proxy)
|
||||||
|
# The requested resource MUST be accessed through the proxy given by
|
||||||
|
# the Location field. The Location field gives the URI of the proxy.
|
||||||
|
retry_options = options.merge(headers: redirect_request.headers,
|
||||||
|
proxy: { uri: redirect_uri },
|
||||||
|
body: redirect_request.body,
|
||||||
|
max_redirects: max_redirects - 1)
|
||||||
|
redirect_uri = redirect_request.uri
|
||||||
|
options = retry_options
|
||||||
|
else
|
||||||
|
|
||||||
|
# redirects are **ALWAYS** GET
|
||||||
|
retry_options = options.merge(headers: redirect_request.headers,
|
||||||
|
body: redirect_request.body,
|
||||||
|
max_redirects: max_redirects - 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
retry_request = build_request("GET", redirect_uri, retry_options)
|
||||||
|
|
||||||
request.redirect_request = retry_request
|
request.redirect_request = retry_request
|
||||||
|
|
||||||
@ -83,29 +103,6 @@ module HTTPX
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_redirect_request(request, response, options)
|
|
||||||
redirect_uri = __get_location_from_response(response)
|
|
||||||
max_redirects = request.max_redirects
|
|
||||||
|
|
||||||
if response.status == 305 && options.respond_to?(:proxy)
|
|
||||||
# The requested resource MUST be accessed through the proxy given by
|
|
||||||
# the Location field. The Location field gives the URI of the proxy.
|
|
||||||
retry_options = options.merge(headers: request.headers,
|
|
||||||
proxy: { uri: redirect_uri },
|
|
||||||
body: request.body,
|
|
||||||
max_redirects: max_redirects - 1)
|
|
||||||
redirect_uri = request.url
|
|
||||||
else
|
|
||||||
|
|
||||||
# redirects are **ALWAYS** GET
|
|
||||||
retry_options = options.merge(headers: request.headers,
|
|
||||||
body: request.body,
|
|
||||||
max_redirects: max_redirects - 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
build_request("GET", redirect_uri, retry_options)
|
|
||||||
end
|
|
||||||
|
|
||||||
def __get_location_from_response(response)
|
def __get_location_from_response(response)
|
||||||
location_uri = URI(response.headers["location"])
|
location_uri = URI(response.headers["location"])
|
||||||
location_uri = response.uri.merge(location_uri) if location_uri.relative?
|
location_uri = response.uri.merge(location_uri) if location_uri.relative?
|
||||||
|
@ -49,20 +49,19 @@ module HTTPX
|
|||||||
class << self
|
class << self
|
||||||
def load_dependencies(*)
|
def load_dependencies(*)
|
||||||
require "stringio"
|
require "stringio"
|
||||||
|
require "httpx/plugins/grpc/grpc_encoding"
|
||||||
require "httpx/plugins/grpc/message"
|
require "httpx/plugins/grpc/message"
|
||||||
require "httpx/plugins/grpc/call"
|
require "httpx/plugins/grpc/call"
|
||||||
end
|
end
|
||||||
|
|
||||||
def configure(klass)
|
def configure(klass)
|
||||||
klass.plugin(:persistent)
|
klass.plugin(:persistent)
|
||||||
klass.plugin(:compression)
|
|
||||||
klass.plugin(:stream)
|
klass.plugin(:stream)
|
||||||
end
|
end
|
||||||
|
|
||||||
def extra_options(options)
|
def extra_options(options)
|
||||||
options.merge(
|
options.merge(
|
||||||
fallback_protocol: "h2",
|
fallback_protocol: "h2",
|
||||||
http2_settings: { wait_for_handshake: false },
|
|
||||||
grpc_rpcs: {}.freeze,
|
grpc_rpcs: {}.freeze,
|
||||||
grpc_compression: false,
|
grpc_compression: false,
|
||||||
grpc_deadline: DEADLINE
|
grpc_deadline: DEADLINE
|
||||||
@ -108,9 +107,18 @@ module HTTPX
|
|||||||
@trailing_metadata = Hash[trailers]
|
@trailing_metadata = Hash[trailers]
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def encoders
|
module RequestBodyMethods
|
||||||
@options.encodings
|
def initialize(headers, _)
|
||||||
|
super
|
||||||
|
|
||||||
|
if (compression = headers["grpc-encoding"])
|
||||||
|
deflater_body = self.class.initialize_deflater_body(@body, compression)
|
||||||
|
@body = Transcoder::GRPCEncoding.encode(deflater_body || @body, compressed: !deflater_body.nil?)
|
||||||
|
else
|
||||||
|
@body = Transcoder::GRPCEncoding.encode(@body, compressed: false)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -233,7 +241,7 @@ module HTTPX
|
|||||||
uri.path = rpc_method
|
uri.path = rpc_method
|
||||||
|
|
||||||
headers = HEADERS.merge(
|
headers = HEADERS.merge(
|
||||||
"grpc-accept-encoding" => ["identity", *@options.encodings.keys]
|
"grpc-accept-encoding" => ["identity", *@options.supported_compression_formats]
|
||||||
)
|
)
|
||||||
unless deadline == Float::INFINITY
|
unless deadline == Float::INFINITY
|
||||||
# convert to milliseconds
|
# convert to milliseconds
|
||||||
@ -244,27 +252,13 @@ module HTTPX
|
|||||||
headers = headers.merge(metadata) if metadata
|
headers = headers.merge(metadata) if metadata
|
||||||
|
|
||||||
# prepare compressor
|
# prepare compressor
|
||||||
deflater = nil
|
|
||||||
compression = @options.grpc_compression == true ? "gzip" : @options.grpc_compression
|
compression = @options.grpc_compression == true ? "gzip" : @options.grpc_compression
|
||||||
|
|
||||||
if compression
|
headers["grpc-encoding"] = compression if compression
|
||||||
headers["grpc-encoding"] = compression
|
|
||||||
deflater = @options.encodings[compression].deflater if @options.encodings.key?(compression)
|
|
||||||
end
|
|
||||||
|
|
||||||
headers.merge!(@options.call_credentials.call) if @options.call_credentials
|
headers.merge!(@options.call_credentials.call) if @options.call_credentials
|
||||||
|
|
||||||
body = if input.respond_to?(:each)
|
build_request("POST", uri, headers: headers, body: input)
|
||||||
Enumerator.new do |y|
|
|
||||||
input.each do |message|
|
|
||||||
y << Message.encode(message, deflater: deflater)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
Message.encode(input, deflater: deflater)
|
|
||||||
end
|
|
||||||
|
|
||||||
build_request("POST", uri, headers: headers, body: body)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
82
lib/httpx/plugins/grpc/grpc_encoding.rb
Normal file
82
lib/httpx/plugins/grpc/grpc_encoding.rb
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module HTTPX
|
||||||
|
module Transcoder
|
||||||
|
module GRPCEncoding
|
||||||
|
class Deflater
|
||||||
|
extend Forwardable
|
||||||
|
|
||||||
|
attr_reader :content_type
|
||||||
|
|
||||||
|
def initialize(body, compressed:)
|
||||||
|
@content_type = body.content_type
|
||||||
|
@body = BodyReader.new(body)
|
||||||
|
@compressed = compressed
|
||||||
|
end
|
||||||
|
|
||||||
|
def bytesize
|
||||||
|
return @body.bytesize if @body.respond_to?(:bytesize)
|
||||||
|
|
||||||
|
Float::INFINITY
|
||||||
|
end
|
||||||
|
|
||||||
|
def read(length = nil, outbuf = nil)
|
||||||
|
buf = @body.read(length, outbuf)
|
||||||
|
|
||||||
|
return unless buf
|
||||||
|
|
||||||
|
compressed_flag = @compressed ? 1 : 0
|
||||||
|
|
||||||
|
buf = outbuf if outbuf
|
||||||
|
|
||||||
|
buf.prepend([compressed_flag, buf.bytesize].pack("CL>"))
|
||||||
|
buf
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Inflater
|
||||||
|
def initialize(response)
|
||||||
|
@encodings = response.headers.get("grpc-encoding")
|
||||||
|
@response = response
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(message, &blk)
|
||||||
|
data = "".b
|
||||||
|
|
||||||
|
until message.empty?
|
||||||
|
compressed, size = message.unpack("CL>")
|
||||||
|
|
||||||
|
encoded_data = message.byteslice(5..size + 5 - 1)
|
||||||
|
|
||||||
|
if compressed == 1
|
||||||
|
@encodings.reverse_each do |encoding|
|
||||||
|
decoder = @response.body.class.initialize_inflater_by_encoding(encoding, @response, bytesize: encoded_data.bytesize)
|
||||||
|
encoded_data = decoder.call(encoded_data)
|
||||||
|
|
||||||
|
blk.call(encoded_data) if blk
|
||||||
|
|
||||||
|
data << encoded_data
|
||||||
|
end
|
||||||
|
else
|
||||||
|
blk.call(encoded_data) if blk
|
||||||
|
|
||||||
|
data << encoded_data
|
||||||
|
end
|
||||||
|
|
||||||
|
message = message.byteslice((size + 5)..-1)
|
||||||
|
end
|
||||||
|
|
||||||
|
data
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.encode(*args, **kwargs)
|
||||||
|
Deflater.new(*args, **kwargs)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.decode(response)
|
||||||
|
Inflater.new(response)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -12,57 +12,25 @@ module HTTPX
|
|||||||
# decodes a unary grpc response
|
# decodes a unary grpc response
|
||||||
def unary(response)
|
def unary(response)
|
||||||
verify_status(response)
|
verify_status(response)
|
||||||
decode(response.to_s, encodings: response.headers.get("grpc-encoding"), encoders: response.encoders)
|
|
||||||
|
decoder = Transcoder::GRPCEncoding.decode(response)
|
||||||
|
|
||||||
|
decoder.call(response.to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
# lazy decodes a grpc stream response
|
# lazy decodes a grpc stream response
|
||||||
def stream(response, &block)
|
def stream(response, &block)
|
||||||
return enum_for(__method__, response) unless block
|
return enum_for(__method__, response) unless block
|
||||||
|
|
||||||
|
decoder = Transcoder::GRPCEncoding.decode(response)
|
||||||
|
|
||||||
response.each do |frame|
|
response.each do |frame|
|
||||||
decode(frame, encodings: response.headers.get("grpc-encoding"), encoders: response.encoders, &block)
|
decoder.call(frame, &block)
|
||||||
end
|
end
|
||||||
|
|
||||||
verify_status(response)
|
verify_status(response)
|
||||||
end
|
end
|
||||||
|
|
||||||
# encodes a single grpc message
|
|
||||||
def encode(bytes, deflater:)
|
|
||||||
if deflater
|
|
||||||
compressed_flag = 1
|
|
||||||
bytes = deflater.deflate(StringIO.new(bytes))
|
|
||||||
else
|
|
||||||
compressed_flag = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
"".b << [compressed_flag, bytes.bytesize].pack("CL>") << bytes.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
# decodes a single grpc message
|
|
||||||
def decode(message, encodings:, encoders:)
|
|
||||||
until message.empty?
|
|
||||||
|
|
||||||
compressed, size = message.unpack("CL>")
|
|
||||||
|
|
||||||
data = message.byteslice(5..size + 5 - 1)
|
|
||||||
if compressed == 1
|
|
||||||
encodings.reverse_each do |algo|
|
|
||||||
next unless encoders.key?(algo)
|
|
||||||
|
|
||||||
inflater = encoders[algo].inflater(size)
|
|
||||||
data = inflater.inflate(data)
|
|
||||||
size = data.bytesize
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return data unless block_given?
|
|
||||||
|
|
||||||
yield data
|
|
||||||
|
|
||||||
message = message.byteslice((size + 5)..-1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def cancel(request)
|
def cancel(request)
|
||||||
request.emit(:refuse, :client_cancellation)
|
request.emit(:refuse, :client_cancellation)
|
||||||
end
|
end
|
||||||
|
@ -1,96 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module HTTPX
|
|
||||||
module Plugins
|
|
||||||
#
|
|
||||||
# This plugin adds support for passing `http-form_data` objects (like file objects) as "multipart/form-data";
|
|
||||||
#
|
|
||||||
# HTTPX.post(URL, form: form: { image: HTTP::FormData::File.new("path/to/file")})
|
|
||||||
#
|
|
||||||
# https://gitlab.com/os85/httpx/wikis/Multipart-Uploads
|
|
||||||
#
|
|
||||||
module Multipart
|
|
||||||
MULTIPART_VALUE_COND = lambda do |value|
|
|
||||||
value.respond_to?(:read) ||
|
|
||||||
(value.respond_to?(:to_hash) &&
|
|
||||||
value.key?(:body) &&
|
|
||||||
(value.key?(:filename) || value.key?(:content_type)))
|
|
||||||
end
|
|
||||||
|
|
||||||
class << self
|
|
||||||
def normalize_keys(key, value, &block)
|
|
||||||
Transcoder.normalize_keys(key, value, MULTIPART_VALUE_COND, &block)
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_dependencies(*)
|
|
||||||
# :nocov:
|
|
||||||
begin
|
|
||||||
unless defined?(HTTP::FormData)
|
|
||||||
# in order not to break legacy code, we'll keep loading http/form_data for them.
|
|
||||||
require "http/form_data"
|
|
||||||
warn "httpx: http/form_data is no longer a requirement to use HTTPX :multipart plugin. See migration instructions under" \
|
|
||||||
"https://os85.gitlab.io/httpx/wiki/Multipart-Uploads.html#notes. \n\n" \
|
|
||||||
"If you'd like to stop seeing this message, require 'http/form_data' yourself."
|
|
||||||
end
|
|
||||||
rescue LoadError
|
|
||||||
end
|
|
||||||
# :nocov:
|
|
||||||
require "httpx/plugins/multipart/encoder"
|
|
||||||
require "httpx/plugins/multipart/decoder"
|
|
||||||
require "httpx/plugins/multipart/part"
|
|
||||||
require "httpx/plugins/multipart/mime_type_detector"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module RequestBodyMethods
|
|
||||||
private
|
|
||||||
|
|
||||||
def initialize_body(options)
|
|
||||||
return FormTranscoder.encode(options.form) if options.form
|
|
||||||
|
|
||||||
super
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module ResponseMethods
|
|
||||||
def form
|
|
||||||
decode(FormTranscoder)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module FormTranscoder
|
|
||||||
module_function
|
|
||||||
|
|
||||||
def encode(form)
|
|
||||||
if multipart?(form)
|
|
||||||
Encoder.new(form)
|
|
||||||
else
|
|
||||||
Transcoder::Form::Encoder.new(form)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def decode(response)
|
|
||||||
content_type = response.content_type.mime_type
|
|
||||||
|
|
||||||
case content_type
|
|
||||||
when "application/x-www-form-urlencoded"
|
|
||||||
Transcoder::Form.decode(response)
|
|
||||||
when "multipart/form-data"
|
|
||||||
Decoder.new(response)
|
|
||||||
else
|
|
||||||
raise Error, "invalid form mime type (#{content_type})"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def multipart?(data)
|
|
||||||
data.any? do |_, v|
|
|
||||||
MULTIPART_VALUE_COND.call(v) ||
|
|
||||||
(v.respond_to?(:to_ary) && v.to_ary.any?(&MULTIPART_VALUE_COND)) ||
|
|
||||||
(v.respond_to?(:to_hash) && v.to_hash.any? { |_, e| MULTIPART_VALUE_COND.call(e) })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
register_plugin :multipart, Multipart
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,137 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require "tempfile"
|
|
||||||
require "delegate"
|
|
||||||
|
|
||||||
module HTTPX::Plugins
|
|
||||||
module Multipart
|
|
||||||
class FilePart < SimpleDelegator
|
|
||||||
attr_reader :original_filename, :content_type
|
|
||||||
|
|
||||||
def initialize(filename, content_type)
|
|
||||||
@original_filename = filename
|
|
||||||
@content_type = content_type
|
|
||||||
@file = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
|
|
||||||
super(@file)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class Decoder
|
|
||||||
include HTTPX::Utils
|
|
||||||
|
|
||||||
CRLF = "\r\n"
|
|
||||||
BOUNDARY_RE = /;\s*boundary=([^;]+)/i.freeze
|
|
||||||
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{CRLF}/ni.freeze
|
|
||||||
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni.freeze
|
|
||||||
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{CRLF}]*)/ni.freeze
|
|
||||||
WINDOW_SIZE = 2 << 14
|
|
||||||
|
|
||||||
def initialize(response)
|
|
||||||
@boundary = begin
|
|
||||||
m = response.headers["content-type"].to_s[BOUNDARY_RE, 1]
|
|
||||||
raise Error, "no boundary declared in content-type header" unless m
|
|
||||||
|
|
||||||
m.strip
|
|
||||||
end
|
|
||||||
@buffer = "".b
|
|
||||||
@parts = {}
|
|
||||||
@intermediate_boundary = "--#{@boundary}"
|
|
||||||
@state = :idle
|
|
||||||
end
|
|
||||||
|
|
||||||
def call(response, *)
|
|
||||||
response.body.each do |chunk|
|
|
||||||
@buffer << chunk
|
|
||||||
|
|
||||||
parse
|
|
||||||
end
|
|
||||||
|
|
||||||
raise Error, "invalid or unsupported multipart format" unless @buffer.empty?
|
|
||||||
|
|
||||||
@parts
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def parse
|
|
||||||
case @state
|
|
||||||
when :idle
|
|
||||||
raise Error, "payload does not start with boundary" unless @buffer.start_with?("#{@intermediate_boundary}#{CRLF}")
|
|
||||||
|
|
||||||
@buffer = @buffer.byteslice(@intermediate_boundary.bytesize + 2..-1)
|
|
||||||
|
|
||||||
@state = :part_header
|
|
||||||
when :part_header
|
|
||||||
idx = @buffer.index("#{CRLF}#{CRLF}")
|
|
||||||
|
|
||||||
# raise Error, "couldn't parse part headers" unless idx
|
|
||||||
return unless idx
|
|
||||||
|
|
||||||
head = @buffer.byteslice(0..idx + 4 - 1)
|
|
||||||
|
|
||||||
@buffer = @buffer.byteslice(head.bytesize..-1)
|
|
||||||
|
|
||||||
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
|
||||||
if (name = head[MULTIPART_CONTENT_DISPOSITION, 1])
|
|
||||||
name = /\A"(.*)"\Z/ =~ name ? Regexp.last_match(1) : name.dup
|
|
||||||
name.gsub!(/\\(.)/, "\\1")
|
|
||||||
name
|
|
||||||
else
|
|
||||||
name = head[MULTIPART_CONTENT_ID, 1]
|
|
||||||
end
|
|
||||||
|
|
||||||
filename = HTTPX::Utils.get_filename(head)
|
|
||||||
|
|
||||||
name = filename || +"#{content_type || "text/plain"}[]" if name.nil? || name.empty?
|
|
||||||
|
|
||||||
@current = name
|
|
||||||
|
|
||||||
@parts[name] = if filename
|
|
||||||
FilePart.new(filename, content_type)
|
|
||||||
else
|
|
||||||
"".b
|
|
||||||
end
|
|
||||||
|
|
||||||
@state = :part_body
|
|
||||||
when :part_body
|
|
||||||
part = @parts[@current]
|
|
||||||
|
|
||||||
body_separator = if part.is_a?(FilePart)
|
|
||||||
"#{CRLF}#{CRLF}"
|
|
||||||
else
|
|
||||||
CRLF
|
|
||||||
end
|
|
||||||
idx = @buffer.index(body_separator)
|
|
||||||
|
|
||||||
if idx
|
|
||||||
payload = @buffer.byteslice(0..idx - 1)
|
|
||||||
@buffer = @buffer.byteslice(idx + body_separator.bytesize..-1)
|
|
||||||
part << payload
|
|
||||||
part.rewind if part.respond_to?(:rewind)
|
|
||||||
@state = :parse_boundary
|
|
||||||
else
|
|
||||||
part << @buffer
|
|
||||||
@buffer.clear
|
|
||||||
end
|
|
||||||
when :parse_boundary
|
|
||||||
raise Error, "payload does not start with boundary" unless @buffer.start_with?(@intermediate_boundary)
|
|
||||||
|
|
||||||
@buffer = @buffer.byteslice(@intermediate_boundary.bytesize..-1)
|
|
||||||
|
|
||||||
if @buffer == "--"
|
|
||||||
@buffer.clear
|
|
||||||
@state = :done
|
|
||||||
return
|
|
||||||
elsif @buffer.start_with?(CRLF)
|
|
||||||
@buffer = @buffer.byteslice(2..-1)
|
|
||||||
@state = :part_header
|
|
||||||
else
|
|
||||||
return
|
|
||||||
end
|
|
||||||
when :done
|
|
||||||
raise Error, "parsing should have been over by now"
|
|
||||||
end until @buffer.empty?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -3,12 +3,12 @@
|
|||||||
module HTTPX
|
module HTTPX
|
||||||
module Plugins
|
module Plugins
|
||||||
#
|
#
|
||||||
# https://gitlab.com/os85/httpx/wikis/Authentication#ntlm-authentication
|
# https://gitlab.com/os85/httpx/wikis/Authorization#ntlm-auth
|
||||||
#
|
#
|
||||||
module NTLMAuth
|
module NTLMAuth
|
||||||
class << self
|
class << self
|
||||||
def load_dependencies(_klass)
|
def load_dependencies(_klass)
|
||||||
require_relative "authentication/ntlm"
|
require_relative "auth/ntlm"
|
||||||
end
|
end
|
||||||
|
|
||||||
def extra_options(options)
|
def extra_options(options)
|
||||||
@ -25,11 +25,11 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
|
|
||||||
module InstanceMethods
|
module InstanceMethods
|
||||||
def ntlm_authentication(user, password, domain = nil)
|
def ntlm_auth(user, password, domain = nil)
|
||||||
with(ntlm: Authentication::Ntlm.new(user, password, domain: domain))
|
with(ntlm: Authentication::Ntlm.new(user, password, domain: domain))
|
||||||
end
|
end
|
||||||
|
|
||||||
alias_method :ntlm_auth, :ntlm_authentication
|
private
|
||||||
|
|
||||||
def send_requests(*requests)
|
def send_requests(*requests)
|
||||||
requests.flat_map do |request|
|
requests.flat_map do |request|
|
||||||
@ -55,6 +55,6 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
register_plugin :ntlm_authentication, NTLMAuth
|
register_plugin :ntlm_auth, NTLMAuth
|
||||||
end
|
end
|
||||||
end
|
end
|
@ -8,7 +8,7 @@ module HTTPX
|
|||||||
module OAuth
|
module OAuth
|
||||||
class << self
|
class << self
|
||||||
def load_dependencies(_klass)
|
def load_dependencies(_klass)
|
||||||
require_relative "authentication/basic"
|
require_relative "auth/basic"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
|
|
||||||
module InstanceMethods
|
module InstanceMethods
|
||||||
def oauth_authentication(**args)
|
def oauth_auth(**args)
|
||||||
with(oauth_session: OAuthSession.new(**args))
|
with(oauth_session: OAuthSession.new(**args))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -28,35 +28,6 @@ module HTTPX
|
|||||||
def extra_options(options)
|
def extra_options(options)
|
||||||
options.merge(supported_proxy_protocols: [])
|
options.merge(supported_proxy_protocols: [])
|
||||||
end
|
end
|
||||||
|
|
||||||
if URI::Generic.methods.include?(:use_proxy?)
|
|
||||||
def use_proxy?(*args)
|
|
||||||
URI::Generic.use_proxy?(*args)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
# https://github.com/ruby/uri/blob/ae07f956a4bea00b4f54a75bd40b8fa918103eed/lib/uri/generic.rb
|
|
||||||
def use_proxy?(hostname, addr, port, no_proxy)
|
|
||||||
hostname = hostname.downcase
|
|
||||||
dothostname = ".#{hostname}"
|
|
||||||
no_proxy.scan(/([^:,\s]+)(?::(\d+))?/) do |p_host, p_port|
|
|
||||||
if !p_port || port == p_port.to_i
|
|
||||||
if p_host.start_with?(".")
|
|
||||||
return false if hostname.end_with?(p_host.downcase)
|
|
||||||
else
|
|
||||||
return false if dothostname.end_with?(".#{p_host.downcase}")
|
|
||||||
end
|
|
||||||
if addr
|
|
||||||
begin
|
|
||||||
return false if IPAddr.new(p_host).include?(addr)
|
|
||||||
rescue IPAddr::InvalidAddressError
|
|
||||||
next
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
class Parameters
|
class Parameters
|
||||||
@ -82,7 +53,7 @@ module HTTPX
|
|||||||
|
|
||||||
auth_scheme = scheme.to_s.capitalize
|
auth_scheme = scheme.to_s.capitalize
|
||||||
|
|
||||||
require_relative "authentication/#{scheme}" unless defined?(Authentication) && Authentication.const_defined?(auth_scheme, false)
|
require_relative "auth/#{scheme}" unless defined?(Authentication) && Authentication.const_defined?(auth_scheme, false)
|
||||||
|
|
||||||
@authenticator = Authentication.const_get(auth_scheme).new(@username, @password, **extra)
|
@authenticator = Authentication.const_get(auth_scheme).new(@username, @password, **extra)
|
||||||
end
|
end
|
||||||
@ -162,8 +133,8 @@ module HTTPX
|
|||||||
no_proxy = proxy[:no_proxy]
|
no_proxy = proxy[:no_proxy]
|
||||||
no_proxy = no_proxy.join(",") if no_proxy.is_a?(Array)
|
no_proxy = no_proxy.join(",") if no_proxy.is_a?(Array)
|
||||||
|
|
||||||
return super(request, connections, options.merge(proxy: nil)) unless Proxy.use_proxy?(uri.host, next_proxy.host,
|
return super(request, connections, options.merge(proxy: nil)) unless URI::Generic.use_proxy?(uri.host, next_proxy.host,
|
||||||
next_proxy.port, no_proxy)
|
next_proxy.port, no_proxy)
|
||||||
end
|
end
|
||||||
|
|
||||||
proxy.merge(uri: next_proxy)
|
proxy.merge(uri: next_proxy)
|
||||||
|
@ -92,10 +92,6 @@ module HTTPX
|
|||||||
@options = Options.new(options)
|
@options = Options.new(options)
|
||||||
end
|
end
|
||||||
|
|
||||||
def timeout
|
|
||||||
@options.timeout[:operation_timeout]
|
|
||||||
end
|
|
||||||
|
|
||||||
def close; end
|
def close; end
|
||||||
|
|
||||||
def consume(*); end
|
def consume(*); end
|
||||||
|
@ -20,7 +20,7 @@ module HTTPX
|
|||||||
|
|
||||||
class << self
|
class << self
|
||||||
def load_dependencies(*)
|
def load_dependencies(*)
|
||||||
require_relative "../authentication/socks5"
|
require_relative "../auth/socks5"
|
||||||
end
|
end
|
||||||
|
|
||||||
def extra_options(options)
|
def extra_options(options)
|
||||||
@ -144,10 +144,6 @@ module HTTPX
|
|||||||
@options = Options.new(options)
|
@options = Options.new(options)
|
||||||
end
|
end
|
||||||
|
|
||||||
def timeout
|
|
||||||
@options.timeout[:operation_timeout]
|
|
||||||
end
|
|
||||||
|
|
||||||
def close; end
|
def close; end
|
||||||
|
|
||||||
def consume(*); end
|
def consume(*); end
|
||||||
|
@ -88,13 +88,12 @@ module HTTPX
|
|||||||
request.retries.positive? &&
|
request.retries.positive? &&
|
||||||
__repeatable_request?(request, options) &&
|
__repeatable_request?(request, options) &&
|
||||||
(
|
(
|
||||||
# rubocop:disable Style/MultilineTernaryOperator
|
|
||||||
options.retry_on ?
|
|
||||||
options.retry_on.call(response) :
|
|
||||||
(
|
(
|
||||||
response.is_a?(ErrorResponse) && __retryable_error?(response.error)
|
response.is_a?(ErrorResponse) && __retryable_error?(response.error)
|
||||||
|
) ||
|
||||||
|
(
|
||||||
|
options.retry_on && options.retry_on.call(response)
|
||||||
)
|
)
|
||||||
# rubocop:enable Style/MultilineTernaryOperator
|
|
||||||
)
|
)
|
||||||
__try_partial_retry(request, response)
|
__try_partial_retry(request, response)
|
||||||
log { "failed to get response, #{request.retries} tries to go..." }
|
log { "failed to get response, #{request.retries} tries to go..." }
|
||||||
|
@ -94,6 +94,10 @@ module HTTPX
|
|||||||
# https://gitlab.com/honeyryderchuck/httpx/wikis/Stream
|
# https://gitlab.com/honeyryderchuck/httpx/wikis/Stream
|
||||||
#
|
#
|
||||||
module Stream
|
module Stream
|
||||||
|
def self.extra_options(options)
|
||||||
|
options.merge(timeout: { read_timeout: Float::INFINITY, operation_timeout: 60 })
|
||||||
|
end
|
||||||
|
|
||||||
module InstanceMethods
|
module InstanceMethods
|
||||||
def request(*args, stream: false, **options)
|
def request(*args, stream: false, **options)
|
||||||
return super(*args, **options) unless stream
|
return super(*args, **options) unless stream
|
||||||
@ -139,12 +143,6 @@ module HTTPX
|
|||||||
super
|
super
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.const_missing(const_name)
|
|
||||||
super unless const_name == :StreamResponse
|
|
||||||
warn "DEPRECATION WARNING: the class #{self}::StreamResponse is deprecated. Use HTTPX::StreamResponse instead."
|
|
||||||
HTTPX::StreamResponse
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
register_plugin :stream, Stream
|
register_plugin :stream, Stream
|
||||||
end
|
end
|
||||||
|
@ -1,304 +1,22 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module HTTPX
|
module HTTPX
|
||||||
begin
|
module Punycode
|
||||||
require "idnx"
|
module_function
|
||||||
|
|
||||||
module Punycode
|
begin
|
||||||
module_function
|
require "idnx"
|
||||||
|
|
||||||
def encode_hostname(hostname)
|
def encode_hostname(hostname)
|
||||||
Idnx.to_punycode(hostname)
|
Idnx.to_punycode(hostname)
|
||||||
end
|
end
|
||||||
end
|
rescue LoadError
|
||||||
|
|
||||||
rescue LoadError
|
|
||||||
# :nocov:
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#--
|
|
||||||
# punycode.rb - PunyCode encoder for the Domain Name library
|
|
||||||
#
|
|
||||||
# Copyright (C) 2011-2017 Akinori MUSHA, All rights reserved.
|
|
||||||
#
|
|
||||||
# Ported from puny.c, a part of VeriSign XCode (encode/decode) IDN
|
|
||||||
# Library.
|
|
||||||
#
|
|
||||||
# Copyright (C) 2000-2002 Verisign Inc., All rights reserved.
|
|
||||||
#
|
|
||||||
# Redistribution and use in source and binary forms, with or
|
|
||||||
# without modification, are permitted provided that the following
|
|
||||||
# conditions are met:
|
|
||||||
#
|
|
||||||
# 1) Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
#
|
|
||||||
# 2) Redistributions in binary form must reproduce the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer in
|
|
||||||
# the documentation and/or other materials provided with the
|
|
||||||
# distribution.
|
|
||||||
#
|
|
||||||
# 3) Neither the name of the VeriSign Inc. nor the names of its
|
|
||||||
# contributors may be used to endorse or promote products derived
|
|
||||||
# from this software without specific prior written permission.
|
|
||||||
#
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
|
||||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
||||||
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
||||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
|
||||||
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
|
|
||||||
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
|
||||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
#
|
|
||||||
# This software is licensed under the BSD open source license. For more
|
|
||||||
# information visit www.opensource.org.
|
|
||||||
#
|
|
||||||
# Authors:
|
|
||||||
# John Colosi (VeriSign)
|
|
||||||
# Srikanth Veeramachaneni (VeriSign)
|
|
||||||
# Nagesh Chigurupati (Verisign)
|
|
||||||
# Praveen Srinivasan(Verisign)
|
|
||||||
#++
|
|
||||||
module Punycode
|
|
||||||
BASE = 36
|
|
||||||
TMIN = 1
|
|
||||||
TMAX = 26
|
|
||||||
SKEW = 38
|
|
||||||
DAMP = 700
|
|
||||||
INITIAL_BIAS = 72
|
|
||||||
INITIAL_N = 0x80
|
|
||||||
DELIMITER = "-"
|
|
||||||
|
|
||||||
MAXINT = (1 << 32) - 1
|
|
||||||
|
|
||||||
LOBASE = BASE - TMIN
|
|
||||||
CUTOFF = LOBASE * TMAX / 2
|
|
||||||
|
|
||||||
RE_NONBASIC = /[^\x00-\x7f]/.freeze
|
|
||||||
|
|
||||||
# Returns the numeric value of a basic code point (for use in
|
|
||||||
# representing integers) in the range 0 to base-1, or nil if cp
|
|
||||||
# is does not represent a value.
|
|
||||||
DECODE_DIGIT = {}.tap do |map|
|
|
||||||
# ASCII A..Z map to 0..25
|
|
||||||
# ASCII a..z map to 0..25
|
|
||||||
(0..25).each { |i| map[65 + i] = map[97 + i] = i }
|
|
||||||
# ASCII 0..9 map to 26..35
|
|
||||||
(26..35).each { |i| map[22 + i] = i }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the basic code point whose value (when used for
|
|
||||||
# representing integers) is d, which must be in the range 0 to
|
|
||||||
# BASE-1. The lowercase form is used unless flag is true, in
|
|
||||||
# which case the uppercase form is used. The behavior is
|
|
||||||
# undefined if flag is nonzero and digit d has no uppercase
|
|
||||||
# form.
|
|
||||||
ENCODE_DIGIT = proc { |d, flag|
|
|
||||||
(d + 22 + (d < 26 ? 75 : 0) - (flag ? (1 << 5) : 0)).chr
|
|
||||||
# 0..25 map to ASCII a..z or A..Z
|
|
||||||
# 26..35 map to ASCII 0..9
|
|
||||||
}
|
|
||||||
|
|
||||||
DOT = "."
|
|
||||||
PREFIX = "xn--"
|
|
||||||
|
|
||||||
# Most errors we raise are basically kind of ArgumentError.
|
|
||||||
class ArgumentError < ::ArgumentError; end
|
|
||||||
class BufferOverflowError < ArgumentError; end
|
|
||||||
|
|
||||||
module_function
|
|
||||||
|
|
||||||
# Encode a +string+ in Punycode
|
|
||||||
def encode(string)
|
|
||||||
input = string.unpack("U*")
|
|
||||||
output = +""
|
|
||||||
|
|
||||||
# Initialize the state
|
|
||||||
n = INITIAL_N
|
|
||||||
delta = 0
|
|
||||||
bias = INITIAL_BIAS
|
|
||||||
|
|
||||||
# Handle the basic code points
|
|
||||||
input.each { |cp| output << cp.chr if cp < 0x80 }
|
|
||||||
|
|
||||||
h = b = output.length
|
|
||||||
|
|
||||||
# h is the number of code points that have been handled, b is the
|
|
||||||
# number of basic code points, and out is the number of characters
|
|
||||||
# that have been output.
|
|
||||||
|
|
||||||
output << DELIMITER if b > 0
|
|
||||||
|
|
||||||
# Main encoding loop
|
|
||||||
|
|
||||||
while h < input.length
|
|
||||||
# All non-basic code points < n have been handled already. Find
|
|
||||||
# the next larger one
|
|
||||||
|
|
||||||
m = MAXINT
|
|
||||||
input.each do |cp|
|
|
||||||
m = cp if (n...m) === cp
|
|
||||||
end
|
|
||||||
|
|
||||||
# Increase delta enough to advance the decoder's <n,i> state to
|
|
||||||
# <m,0>, but guard against overflow
|
|
||||||
|
|
||||||
delta += (m - n) * (h + 1)
|
|
||||||
raise BufferOverflowError if delta > MAXINT
|
|
||||||
|
|
||||||
n = m
|
|
||||||
|
|
||||||
input.each do |cp|
|
|
||||||
# AMC-ACE-Z can use this simplified version instead
|
|
||||||
if cp < n
|
|
||||||
delta += 1
|
|
||||||
raise BufferOverflowError if delta > MAXINT
|
|
||||||
elsif cp == n
|
|
||||||
# Represent delta as a generalized variable-length integer
|
|
||||||
q = delta
|
|
||||||
k = BASE
|
|
||||||
loop do
|
|
||||||
t = k <= bias ? TMIN : k - bias >= TMAX ? TMAX : k - bias
|
|
||||||
break if q < t
|
|
||||||
|
|
||||||
q, r = (q - t).divmod(BASE - t)
|
|
||||||
output << ENCODE_DIGIT[t + r, false]
|
|
||||||
k += BASE
|
|
||||||
end
|
|
||||||
|
|
||||||
output << ENCODE_DIGIT[q, false]
|
|
||||||
|
|
||||||
# Adapt the bias
|
|
||||||
delta = h == b ? delta / DAMP : delta >> 1
|
|
||||||
delta += delta / (h + 1)
|
|
||||||
bias = 0
|
|
||||||
while delta > CUTOFF
|
|
||||||
delta /= LOBASE
|
|
||||||
bias += BASE
|
|
||||||
end
|
|
||||||
bias += (LOBASE + 1) * delta / (delta + SKEW)
|
|
||||||
|
|
||||||
delta = 0
|
|
||||||
h += 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
delta += 1
|
|
||||||
n += 1
|
|
||||||
end
|
|
||||||
|
|
||||||
output
|
|
||||||
end
|
|
||||||
|
|
||||||
# Encode a hostname using IDN/Punycode algorithms
|
|
||||||
def encode_hostname(hostname)
|
def encode_hostname(hostname)
|
||||||
hostname.match(RE_NONBASIC) || (return hostname)
|
warn "#{hostname} cannot be converted to punycode. Install the " \
|
||||||
|
"\"idnx\" gem: https://github.com/HoneyryderChuck/idnx"
|
||||||
|
|
||||||
hostname.split(DOT).map do |name|
|
hostname
|
||||||
if name.match(RE_NONBASIC)
|
|
||||||
PREFIX + encode(name)
|
|
||||||
else
|
|
||||||
name
|
|
||||||
end
|
|
||||||
end.join(DOT)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Decode a +string+ encoded in Punycode
|
|
||||||
def decode(string)
|
|
||||||
# Initialize the state
|
|
||||||
n = INITIAL_N
|
|
||||||
i = 0
|
|
||||||
bias = INITIAL_BIAS
|
|
||||||
|
|
||||||
if j = string.rindex(DELIMITER)
|
|
||||||
b = string[0...j]
|
|
||||||
|
|
||||||
b.match(RE_NONBASIC) &&
|
|
||||||
raise(ArgumentError, "Illegal character is found in basic part: #{string.inspect}")
|
|
||||||
|
|
||||||
# Handle the basic code points
|
|
||||||
|
|
||||||
output = b.unpack("U*")
|
|
||||||
u = string[(j + 1)..-1]
|
|
||||||
else
|
|
||||||
output = []
|
|
||||||
u = string
|
|
||||||
end
|
|
||||||
|
|
||||||
# Main decoding loop: Start just after the last delimiter if any
|
|
||||||
# basic code points were copied; start at the beginning
|
|
||||||
# otherwise.
|
|
||||||
|
|
||||||
input = u.unpack("C*")
|
|
||||||
input_length = input.length
|
|
||||||
h = 0
|
|
||||||
out = output.length
|
|
||||||
|
|
||||||
while h < input_length
|
|
||||||
# Decode a generalized variable-length integer into delta,
|
|
||||||
# which gets added to i. The overflow checking is easier
|
|
||||||
# if we increase i as we go, then subtract off its starting
|
|
||||||
# value at the end to obtain delta.
|
|
||||||
|
|
||||||
oldi = i
|
|
||||||
w = 1
|
|
||||||
k = BASE
|
|
||||||
|
|
||||||
loop do
|
|
||||||
(digit = DECODE_DIGIT[input[h]]) ||
|
|
||||||
raise(ArgumentError, "Illegal character is found in non-basic part: #{string.inspect}")
|
|
||||||
h += 1
|
|
||||||
i += digit * w
|
|
||||||
raise BufferOverflowError if i > MAXINT
|
|
||||||
|
|
||||||
t = k <= bias ? TMIN : k - bias >= TMAX ? TMAX : k - bias
|
|
||||||
break if digit < t
|
|
||||||
|
|
||||||
w *= BASE - t
|
|
||||||
raise BufferOverflowError if w > MAXINT
|
|
||||||
|
|
||||||
k += BASE
|
|
||||||
(h < input_length) || raise(ArgumentError, "Malformed input given: #{string.inspect}")
|
|
||||||
end
|
|
||||||
|
|
||||||
# Adapt the bias
|
|
||||||
delta = oldi == 0 ? i / DAMP : (i - oldi) >> 1
|
|
||||||
delta += delta / (out + 1)
|
|
||||||
bias = 0
|
|
||||||
while delta > CUTOFF
|
|
||||||
delta /= LOBASE
|
|
||||||
bias += BASE
|
|
||||||
end
|
|
||||||
bias += (LOBASE + 1) * delta / (delta + SKEW)
|
|
||||||
|
|
||||||
# i was supposed to wrap around from out+1 to 0, incrementing
|
|
||||||
# n each time, so we'll fix that now:
|
|
||||||
|
|
||||||
q, i = i.divmod(out + 1)
|
|
||||||
n += q
|
|
||||||
raise BufferOverflowError if n > MAXINT
|
|
||||||
|
|
||||||
# Insert n at position i of the output:
|
|
||||||
|
|
||||||
output[i, 0] = n
|
|
||||||
|
|
||||||
out += 1
|
|
||||||
i += 1
|
|
||||||
end
|
|
||||||
output.pack("U*")
|
|
||||||
end
|
|
||||||
|
|
||||||
# Decode a hostname using IDN/Punycode algorithms
|
|
||||||
def decode_hostname(hostname)
|
|
||||||
hostname.gsub(/(\A|#{Regexp.quote(DOT)})#{Regexp.quote(PREFIX)}([^#{Regexp.quote(DOT)}]*)/o) do
|
|
||||||
Regexp.last_match(1) << decode(Regexp.last_match(2))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
# :nocov:
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -19,10 +19,6 @@ module HTTPX
|
|||||||
def_delegator :@body, :empty?
|
def_delegator :@body, :empty?
|
||||||
|
|
||||||
def initialize(verb, uri, options = {})
|
def initialize(verb, uri, options = {})
|
||||||
if verb.is_a?(Symbol)
|
|
||||||
warn "DEPRECATION WARNING: Using symbols for `verb` is deprecated, and will not be supported in httpx 1.0. " \
|
|
||||||
"Use \"#{verb.to_s.upcase}\" instead."
|
|
||||||
end
|
|
||||||
@verb = verb.to_s.upcase
|
@verb = verb.to_s.upcase
|
||||||
@options = Options.new(options)
|
@options = Options.new(options)
|
||||||
@uri = Utils.to_uri(uri)
|
@uri = Utils.to_uri(uri)
|
||||||
@ -69,16 +65,6 @@ module HTTPX
|
|||||||
:w
|
:w
|
||||||
end
|
end
|
||||||
|
|
||||||
if RUBY_VERSION < "2.2"
|
|
||||||
URIParser = URI::DEFAULT_PARSER
|
|
||||||
|
|
||||||
def initialize_with_escape(verb, uri, options = {})
|
|
||||||
initialize_without_escape(verb, URIParser.escape(uri.to_s), options)
|
|
||||||
end
|
|
||||||
alias_method :initialize_without_escape, :initialize
|
|
||||||
alias_method :initialize, :initialize_with_escape
|
|
||||||
end
|
|
||||||
|
|
||||||
def merge_headers(h)
|
def merge_headers(h)
|
||||||
@headers = @headers.merge(h)
|
@headers = @headers.merge(h)
|
||||||
end
|
end
|
||||||
@ -153,100 +139,6 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
# :nocov:
|
# :nocov:
|
||||||
|
|
||||||
class Body < SimpleDelegator
|
|
||||||
class << self
|
|
||||||
def new(_, options)
|
|
||||||
return options.body if options.body.is_a?(self)
|
|
||||||
|
|
||||||
super
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(headers, options)
|
|
||||||
@headers = headers
|
|
||||||
@body = initialize_body(options)
|
|
||||||
return if @body.nil?
|
|
||||||
|
|
||||||
@headers["content-type"] ||= @body.content_type
|
|
||||||
@headers["content-length"] = @body.bytesize unless unbounded_body?
|
|
||||||
super(@body)
|
|
||||||
end
|
|
||||||
|
|
||||||
def each(&block)
|
|
||||||
return enum_for(__method__) unless block
|
|
||||||
return if @body.nil?
|
|
||||||
|
|
||||||
body = stream(@body)
|
|
||||||
if body.respond_to?(:read)
|
|
||||||
::IO.copy_stream(body, ProcIO.new(block))
|
|
||||||
elsif body.respond_to?(:each)
|
|
||||||
body.each(&block)
|
|
||||||
else
|
|
||||||
block[body.to_s]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def rewind
|
|
||||||
return if empty?
|
|
||||||
|
|
||||||
@body.rewind if @body.respond_to?(:rewind)
|
|
||||||
end
|
|
||||||
|
|
||||||
def empty?
|
|
||||||
return true if @body.nil?
|
|
||||||
return false if chunked?
|
|
||||||
|
|
||||||
@body.bytesize.zero?
|
|
||||||
end
|
|
||||||
|
|
||||||
def bytesize
|
|
||||||
return 0 if @body.nil?
|
|
||||||
|
|
||||||
@body.bytesize
|
|
||||||
end
|
|
||||||
|
|
||||||
def stream(body)
|
|
||||||
encoded = body
|
|
||||||
encoded = Transcoder::Chunker.encode(body.enum_for(:each)) if chunked?
|
|
||||||
encoded
|
|
||||||
end
|
|
||||||
|
|
||||||
def unbounded_body?
|
|
||||||
return @unbounded_body if defined?(@unbounded_body)
|
|
||||||
|
|
||||||
@unbounded_body = !@body.nil? && (chunked? || @body.bytesize == Float::INFINITY)
|
|
||||||
end
|
|
||||||
|
|
||||||
def chunked?
|
|
||||||
@headers["transfer-encoding"] == "chunked"
|
|
||||||
end
|
|
||||||
|
|
||||||
def chunk!
|
|
||||||
@headers.add("transfer-encoding", "chunked")
|
|
||||||
end
|
|
||||||
|
|
||||||
# :nocov:
|
|
||||||
def inspect
|
|
||||||
"#<HTTPX::Request::Body:#{object_id} " \
|
|
||||||
"#{unbounded_body? ? "stream" : "@bytesize=#{bytesize}"}>"
|
|
||||||
end
|
|
||||||
# :nocov:
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def initialize_body(options)
|
|
||||||
if options.body
|
|
||||||
Transcoder::Body.encode(options.body)
|
|
||||||
elsif options.form
|
|
||||||
Transcoder::Form.encode(options.form)
|
|
||||||
elsif options.json
|
|
||||||
Transcoder::JSON.encode(options.json)
|
|
||||||
elsif options.xml
|
|
||||||
Transcoder::Xml.encode(options.xml)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def transition(nextstate)
|
def transition(nextstate)
|
||||||
case nextstate
|
case nextstate
|
||||||
when :idle
|
when :idle
|
||||||
@ -284,16 +176,7 @@ module HTTPX
|
|||||||
def expects?
|
def expects?
|
||||||
@headers["expect"] == "100-continue" && @informational_status == 100 && !@response
|
@headers["expect"] == "100-continue" && @informational_status == 100 && !@response
|
||||||
end
|
end
|
||||||
|
|
||||||
class ProcIO
|
|
||||||
def initialize(block)
|
|
||||||
@block = block
|
|
||||||
end
|
|
||||||
|
|
||||||
def write(data)
|
|
||||||
@block.call(data.dup)
|
|
||||||
data.bytesize
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
require_relative "request/body"
|
||||||
|
145
lib/httpx/request/body.rb
Normal file
145
lib/httpx/request/body.rb
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module HTTPX
|
||||||
|
class Request::Body < SimpleDelegator
|
||||||
|
class << self
|
||||||
|
def new(_, options)
|
||||||
|
return options.body if options.body.is_a?(self)
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :threshold_size
|
||||||
|
|
||||||
|
def initialize(headers, options)
|
||||||
|
@headers = headers
|
||||||
|
@threshold_size = options.body_threshold_size
|
||||||
|
|
||||||
|
# forego compression in the Range cases
|
||||||
|
if @headers.key?("range")
|
||||||
|
@headers.delete("accept-encoding")
|
||||||
|
else
|
||||||
|
@headers["accept-encoding"] ||= options.supported_compression_formats
|
||||||
|
end
|
||||||
|
|
||||||
|
initialize_body(options)
|
||||||
|
|
||||||
|
return if @body.nil?
|
||||||
|
|
||||||
|
@headers["content-type"] ||= @body.content_type
|
||||||
|
@headers["content-length"] = @body.bytesize unless unbounded_body?
|
||||||
|
super(@body)
|
||||||
|
end
|
||||||
|
|
||||||
|
def each(&block)
|
||||||
|
return enum_for(__method__) unless block
|
||||||
|
return if @body.nil?
|
||||||
|
|
||||||
|
body = stream(@body)
|
||||||
|
if body.respond_to?(:read)
|
||||||
|
::IO.copy_stream(body, ProcIO.new(block))
|
||||||
|
elsif body.respond_to?(:each)
|
||||||
|
body.each(&block)
|
||||||
|
else
|
||||||
|
block[body.to_s]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def rewind
|
||||||
|
return if empty?
|
||||||
|
|
||||||
|
@body.rewind if @body.respond_to?(:rewind)
|
||||||
|
end
|
||||||
|
|
||||||
|
def empty?
|
||||||
|
return true if @body.nil?
|
||||||
|
return false if chunked?
|
||||||
|
|
||||||
|
@body.bytesize.zero?
|
||||||
|
end
|
||||||
|
|
||||||
|
def bytesize
|
||||||
|
return 0 if @body.nil?
|
||||||
|
|
||||||
|
@body.bytesize
|
||||||
|
end
|
||||||
|
|
||||||
|
def stream(body)
|
||||||
|
encoded = body
|
||||||
|
encoded = Transcoder::Chunker.encode(body.enum_for(:each)) if chunked?
|
||||||
|
encoded
|
||||||
|
end
|
||||||
|
|
||||||
|
def unbounded_body?
|
||||||
|
return @unbounded_body if defined?(@unbounded_body)
|
||||||
|
|
||||||
|
@unbounded_body = !@body.nil? && (chunked? || @body.bytesize == Float::INFINITY)
|
||||||
|
end
|
||||||
|
|
||||||
|
def chunked?
|
||||||
|
@headers["transfer-encoding"] == "chunked"
|
||||||
|
end
|
||||||
|
|
||||||
|
def chunk!
|
||||||
|
@headers.add("transfer-encoding", "chunked")
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nocov:
|
||||||
|
def inspect
|
||||||
|
"#<HTTPX::Request::Body:#{object_id} " \
|
||||||
|
"#{unbounded_body? ? "stream" : "@bytesize=#{bytesize}"}>"
|
||||||
|
end
|
||||||
|
# :nocov:
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def initialize_body(options)
|
||||||
|
@body = if options.body
|
||||||
|
Transcoder::Body.encode(options.body)
|
||||||
|
elsif options.form
|
||||||
|
Transcoder::Form.encode(options.form)
|
||||||
|
elsif options.json
|
||||||
|
Transcoder::JSON.encode(options.json)
|
||||||
|
elsif options.xml
|
||||||
|
Transcoder::Xml.encode(options.xml)
|
||||||
|
end
|
||||||
|
|
||||||
|
return unless @body
|
||||||
|
|
||||||
|
return unless options.compress_request_body
|
||||||
|
|
||||||
|
return unless @headers.key?("content-encoding")
|
||||||
|
|
||||||
|
@headers.get("content-encoding").each do |encoding|
|
||||||
|
@body = self.class.initialize_deflater_body(@body, encoding)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def initialize_deflater_body(body, encoding)
|
||||||
|
case encoding
|
||||||
|
when "gzip"
|
||||||
|
Transcoder::GZIP.encode(body)
|
||||||
|
when "deflate"
|
||||||
|
Transcoder::Deflate.encode(body)
|
||||||
|
when "identity"
|
||||||
|
body
|
||||||
|
else
|
||||||
|
body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class ProcIO
|
||||||
|
def initialize(block)
|
||||||
|
@block = block
|
||||||
|
end
|
||||||
|
|
||||||
|
def write(data)
|
||||||
|
@block.call(data.dup)
|
||||||
|
data.bytesize
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -9,7 +9,6 @@ module HTTPX
|
|||||||
class Resolver::HTTPS < Resolver::Resolver
|
class Resolver::HTTPS < Resolver::Resolver
|
||||||
extend Forwardable
|
extend Forwardable
|
||||||
using URIExtensions
|
using URIExtensions
|
||||||
using StringExtensions
|
|
||||||
|
|
||||||
module DNSExtensions
|
module DNSExtensions
|
||||||
refine Resolv::DNS do
|
refine Resolv::DNS do
|
||||||
|
@ -8,20 +8,12 @@ module HTTPX
|
|||||||
extend Forwardable
|
extend Forwardable
|
||||||
using URIExtensions
|
using URIExtensions
|
||||||
|
|
||||||
DEFAULTS = if RUBY_VERSION < "2.2"
|
DEFAULTS = {
|
||||||
{
|
nameserver: nil,
|
||||||
**Resolv::DNS::Config.default_config_hash,
|
**Resolv::DNS::Config.default_config_hash,
|
||||||
packet_size: 512,
|
packet_size: 512,
|
||||||
timeouts: Resolver::RESOLVE_TIMEOUT,
|
timeouts: Resolver::RESOLVE_TIMEOUT,
|
||||||
}
|
}.freeze
|
||||||
else
|
|
||||||
{
|
|
||||||
nameserver: nil,
|
|
||||||
**Resolv::DNS::Config.default_config_hash,
|
|
||||||
packet_size: 512,
|
|
||||||
timeouts: Resolver::RESOLVE_TIMEOUT,
|
|
||||||
}
|
|
||||||
end.freeze
|
|
||||||
|
|
||||||
DNS_PORT = 53
|
DNS_PORT = 53
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
|
|
||||||
def resolve_error(hostname, ex = nil)
|
def resolve_error(hostname, ex = nil)
|
||||||
return ex if ex.is_a?(ResolveError)
|
return ex if ex.is_a?(ResolveError) || ex.is_a?(ResolveTimeoutError)
|
||||||
|
|
||||||
message = ex ? ex.message : "Can't resolve #{hostname}"
|
message = ex ? ex.message : "Can't resolve #{hostname}"
|
||||||
error = ResolveError.new(message)
|
error = ResolveError.new(message)
|
||||||
|
@ -92,6 +92,12 @@ module HTTPX
|
|||||||
resolve
|
resolve
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def raise_timeout_error(interval)
|
||||||
|
error = HTTPX::ResolveTimeoutError.new(interval, "timed out while waiting on select")
|
||||||
|
error.set_backtrace(caller)
|
||||||
|
on_error(error)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def transition(nextstate)
|
def transition(nextstate)
|
||||||
@ -164,6 +170,7 @@ module HTTPX
|
|||||||
Thread.current.report_on_exception = false
|
Thread.current.report_on_exception = false
|
||||||
begin
|
begin
|
||||||
addrs = if resolve_timeout
|
addrs = if resolve_timeout
|
||||||
|
|
||||||
Timeout.timeout(resolve_timeout) do
|
Timeout.timeout(resolve_timeout) do
|
||||||
__addrinfo_resolve(hostname, scheme)
|
__addrinfo_resolve(hostname, scheme)
|
||||||
end
|
end
|
||||||
@ -182,16 +189,11 @@ module HTTPX
|
|||||||
@pipe_write.putc(DONE) unless @pipe_write.closed?
|
@pipe_write.putc(DONE) unless @pipe_write.closed?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
rescue Timeout::Error => e
|
|
||||||
ex = ResolveTimeoutError.new(resolve_timeout, e.message)
|
|
||||||
ex.set_backtrace(ex.backtrace)
|
|
||||||
@pipe_mutex.synchronize do
|
|
||||||
families.each do |family|
|
|
||||||
@ips.unshift([family, connection, ex])
|
|
||||||
@pipe_write.putc(ERROR) unless @pipe_write.closed?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
|
if e.is_a?(Timeout::Error)
|
||||||
|
e = ResolveTimeoutError.new(resolve_timeout, e.message)
|
||||||
|
e.set_backtrace(e.backtrace)
|
||||||
|
end
|
||||||
@pipe_mutex.synchronize do
|
@pipe_mutex.synchronize do
|
||||||
families.each do |family|
|
families.each do |family|
|
||||||
@ips.unshift([family, connection, e])
|
@ips.unshift([family, connection, e])
|
||||||
|
@ -124,199 +124,6 @@ module HTTPX
|
|||||||
content_length == "0"
|
content_length == "0"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class Body
|
|
||||||
attr_reader :encoding
|
|
||||||
|
|
||||||
def initialize(response, options)
|
|
||||||
@response = response
|
|
||||||
@headers = response.headers
|
|
||||||
@options = options
|
|
||||||
@threshold_size = options.body_threshold_size
|
|
||||||
@window_size = options.window_size
|
|
||||||
@encoding = response.content_type.charset || Encoding::BINARY
|
|
||||||
@length = 0
|
|
||||||
@buffer = nil
|
|
||||||
@state = :idle
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize_dup(other)
|
|
||||||
super
|
|
||||||
|
|
||||||
@buffer = other.instance_variable_get(:@buffer).dup
|
|
||||||
end
|
|
||||||
|
|
||||||
def closed?
|
|
||||||
@state == :closed
|
|
||||||
end
|
|
||||||
|
|
||||||
def write(chunk)
|
|
||||||
return if @state == :closed
|
|
||||||
|
|
||||||
size = chunk.bytesize
|
|
||||||
@length += size
|
|
||||||
transition
|
|
||||||
@buffer.write(chunk)
|
|
||||||
|
|
||||||
@response.emit(:chunk_received, chunk)
|
|
||||||
size
|
|
||||||
end
|
|
||||||
|
|
||||||
def read(*args)
|
|
||||||
return unless @buffer
|
|
||||||
|
|
||||||
unless @reader
|
|
||||||
rewind
|
|
||||||
@reader = @buffer
|
|
||||||
end
|
|
||||||
|
|
||||||
@reader.read(*args)
|
|
||||||
end
|
|
||||||
|
|
||||||
def bytesize
|
|
||||||
@length
|
|
||||||
end
|
|
||||||
|
|
||||||
def each
|
|
||||||
return enum_for(__method__) unless block_given?
|
|
||||||
|
|
||||||
begin
|
|
||||||
if @buffer
|
|
||||||
rewind
|
|
||||||
while (chunk = @buffer.read(@window_size))
|
|
||||||
yield(chunk.force_encoding(@encoding))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
ensure
|
|
||||||
close
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def filename
|
|
||||||
return unless @headers.key?("content-disposition")
|
|
||||||
|
|
||||||
Utils.get_filename(@headers["content-disposition"])
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
|
||||||
case @buffer
|
|
||||||
when StringIO
|
|
||||||
begin
|
|
||||||
@buffer.string.force_encoding(@encoding)
|
|
||||||
rescue ArgumentError
|
|
||||||
@buffer.string
|
|
||||||
end
|
|
||||||
when Tempfile
|
|
||||||
rewind
|
|
||||||
content = _with_same_buffer_pos { @buffer.read }
|
|
||||||
begin
|
|
||||||
content.force_encoding(@encoding)
|
|
||||||
rescue ArgumentError # ex: unknown encoding name - utf
|
|
||||||
content
|
|
||||||
end
|
|
||||||
else
|
|
||||||
"".b
|
|
||||||
end
|
|
||||||
end
|
|
||||||
alias_method :to_str, :to_s
|
|
||||||
|
|
||||||
def empty?
|
|
||||||
@length.zero?
|
|
||||||
end
|
|
||||||
|
|
||||||
def copy_to(dest)
|
|
||||||
return unless @buffer
|
|
||||||
|
|
||||||
rewind
|
|
||||||
|
|
||||||
if dest.respond_to?(:path) && @buffer.respond_to?(:path)
|
|
||||||
FileUtils.mv(@buffer.path, dest.path)
|
|
||||||
else
|
|
||||||
::IO.copy_stream(@buffer, dest)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# closes/cleans the buffer, resets everything
|
|
||||||
def close
|
|
||||||
if @buffer
|
|
||||||
@buffer.close
|
|
||||||
@buffer.unlink if @buffer.respond_to?(:unlink)
|
|
||||||
@buffer = nil
|
|
||||||
end
|
|
||||||
@length = 0
|
|
||||||
@state = :closed
|
|
||||||
end
|
|
||||||
|
|
||||||
def ==(other)
|
|
||||||
object_id == other.object_id || begin
|
|
||||||
if other.respond_to?(:read)
|
|
||||||
_with_same_buffer_pos { FileUtils.compare_stream(@buffer, other) }
|
|
||||||
else
|
|
||||||
to_s == other.to_s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# :nocov:
|
|
||||||
def inspect
|
|
||||||
"#<HTTPX::Response::Body:#{object_id} " \
|
|
||||||
"@state=#{@state} " \
|
|
||||||
"@length=#{@length}>"
|
|
||||||
end
|
|
||||||
# :nocov:
|
|
||||||
|
|
||||||
def rewind
|
|
||||||
return unless @buffer
|
|
||||||
|
|
||||||
# in case there's some reading going on
|
|
||||||
@reader = nil
|
|
||||||
|
|
||||||
@buffer.rewind
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def transition
|
|
||||||
case @state
|
|
||||||
when :idle
|
|
||||||
if @length > @threshold_size
|
|
||||||
@state = :buffer
|
|
||||||
@buffer = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
|
|
||||||
else
|
|
||||||
@state = :memory
|
|
||||||
@buffer = StringIO.new("".b)
|
|
||||||
end
|
|
||||||
when :memory
|
|
||||||
# @type ivar @buffer: StringIO | Tempfile
|
|
||||||
if @length > @threshold_size
|
|
||||||
aux = @buffer
|
|
||||||
@buffer = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
|
|
||||||
aux.rewind
|
|
||||||
::IO.copy_stream(aux, @buffer)
|
|
||||||
# (this looks like a bug from Ruby < 2.3
|
|
||||||
@buffer.pos = aux.pos ##################
|
|
||||||
########################################
|
|
||||||
aux.close
|
|
||||||
@state = :buffer
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
nil unless %i[memory buffer].include?(@state)
|
|
||||||
end
|
|
||||||
|
|
||||||
def _with_same_buffer_pos
|
|
||||||
return yield unless @buffer && @buffer.respond_to?(:pos)
|
|
||||||
|
|
||||||
# @type ivar @buffer: StringIO | Tempfile
|
|
||||||
current_pos = @buffer.pos
|
|
||||||
@buffer.rewind
|
|
||||||
begin
|
|
||||||
yield
|
|
||||||
ensure
|
|
||||||
@buffer.pos = current_pos
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
class ContentType
|
class ContentType
|
||||||
@ -358,20 +165,8 @@ module HTTPX
|
|||||||
log_exception(@error)
|
log_exception(@error)
|
||||||
end
|
end
|
||||||
|
|
||||||
def status
|
def to_s
|
||||||
warn ":#{__method__} is deprecated, use :error.message instead"
|
@error.full_message(highlight: false)
|
||||||
@error.message
|
|
||||||
end
|
|
||||||
|
|
||||||
if Exception.method_defined?(:full_message)
|
|
||||||
def to_s
|
|
||||||
@error.full_message(highlight: false)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
def to_s
|
|
||||||
"#{@error.message} (#{@error.class})\n" \
|
|
||||||
"#{@error.backtrace.join("\n") if @error.backtrace}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def close
|
def close
|
||||||
@ -388,4 +183,6 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
require "httpx/pmatch_extensions" if RUBY_VERSION >= "3.0.0"
|
require_relative "response/body"
|
||||||
|
require_relative "response/buffer"
|
||||||
|
require_relative "pmatch_extensions" if RUBY_VERSION >= "3.0.0"
|
||||||
|
206
lib/httpx/response/body.rb
Normal file
206
lib/httpx/response/body.rb
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module HTTPX
|
||||||
|
class Response::Body
|
||||||
|
attr_reader :encoding, :encodings
|
||||||
|
|
||||||
|
def initialize(response, options)
|
||||||
|
@response = response
|
||||||
|
@headers = response.headers
|
||||||
|
@options = options
|
||||||
|
@threshold_size = options.body_threshold_size
|
||||||
|
@window_size = options.window_size
|
||||||
|
@encoding = response.content_type.charset || Encoding::BINARY
|
||||||
|
@encodings = []
|
||||||
|
@length = 0
|
||||||
|
@buffer = nil
|
||||||
|
@state = :idle
|
||||||
|
initialize_inflaters
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_dup(other)
|
||||||
|
super
|
||||||
|
|
||||||
|
@buffer = other.instance_variable_get(:@buffer).dup
|
||||||
|
end
|
||||||
|
|
||||||
|
def closed?
|
||||||
|
@state == :closed
|
||||||
|
end
|
||||||
|
|
||||||
|
def write(chunk)
|
||||||
|
return if @state == :closed
|
||||||
|
|
||||||
|
@inflaters.reverse_each do |inflater|
|
||||||
|
chunk = inflater.call(chunk)
|
||||||
|
end if @inflaters
|
||||||
|
|
||||||
|
size = chunk.bytesize
|
||||||
|
@length += size
|
||||||
|
transition(:open)
|
||||||
|
@buffer.write(chunk)
|
||||||
|
|
||||||
|
@response.emit(:chunk_received, chunk)
|
||||||
|
size
|
||||||
|
end
|
||||||
|
|
||||||
|
def read(*args)
|
||||||
|
return unless @buffer
|
||||||
|
|
||||||
|
unless @reader
|
||||||
|
rewind
|
||||||
|
@reader = @buffer
|
||||||
|
end
|
||||||
|
|
||||||
|
@reader.read(*args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def bytesize
|
||||||
|
@length
|
||||||
|
end
|
||||||
|
|
||||||
|
def each
|
||||||
|
return enum_for(__method__) unless block_given?
|
||||||
|
|
||||||
|
begin
|
||||||
|
if @buffer
|
||||||
|
rewind
|
||||||
|
while (chunk = @buffer.read(@window_size))
|
||||||
|
yield(chunk.force_encoding(@encoding))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
close
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def filename
|
||||||
|
return unless @headers.key?("content-disposition")
|
||||||
|
|
||||||
|
Utils.get_filename(@headers["content-disposition"])
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
return "".b unless @buffer
|
||||||
|
|
||||||
|
@buffer.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
alias_method :to_str, :to_s
|
||||||
|
|
||||||
|
def empty?
|
||||||
|
@length.zero?
|
||||||
|
end
|
||||||
|
|
||||||
|
def copy_to(dest)
|
||||||
|
return unless @buffer
|
||||||
|
|
||||||
|
rewind
|
||||||
|
|
||||||
|
if dest.respond_to?(:path) && @buffer.respond_to?(:path)
|
||||||
|
FileUtils.mv(@buffer.path, dest.path)
|
||||||
|
else
|
||||||
|
::IO.copy_stream(@buffer, dest)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# closes/cleans the buffer, resets everything
|
||||||
|
def close
|
||||||
|
if @buffer
|
||||||
|
@buffer.close
|
||||||
|
@buffer = nil
|
||||||
|
end
|
||||||
|
@length = 0
|
||||||
|
transition(:closed)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ==(other)
|
||||||
|
object_id == other.object_id || begin
|
||||||
|
if other.respond_to?(:read)
|
||||||
|
_with_same_buffer_pos { FileUtils.compare_stream(@buffer, other) }
|
||||||
|
else
|
||||||
|
to_s == other.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nocov:
|
||||||
|
def inspect
|
||||||
|
"#<HTTPX::Response::Body:#{object_id} " \
|
||||||
|
"@state=#{@state} " \
|
||||||
|
"@length=#{@length}>"
|
||||||
|
end
|
||||||
|
# :nocov:
|
||||||
|
|
||||||
|
def rewind
|
||||||
|
return unless @buffer
|
||||||
|
|
||||||
|
# in case there's some reading going on
|
||||||
|
@reader = nil
|
||||||
|
|
||||||
|
@buffer.rewind
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def initialize_inflaters
|
||||||
|
return unless @headers.key?("content-encoding")
|
||||||
|
|
||||||
|
return unless @options.decompress_response_body
|
||||||
|
|
||||||
|
@inflaters = @headers.get("content-encoding").filter_map do |encoding|
|
||||||
|
next if encoding == "identity"
|
||||||
|
|
||||||
|
inflater = self.class.initialize_inflater_by_encoding(encoding, @response)
|
||||||
|
|
||||||
|
# do not uncompress if there is no decoder available. In fact, we can't reliably
|
||||||
|
# continue decompressing beyond that, so ignore.
|
||||||
|
break unless inflater
|
||||||
|
|
||||||
|
@encodings << encoding
|
||||||
|
inflater
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def transition(nextstate)
|
||||||
|
case nextstate
|
||||||
|
when :open
|
||||||
|
return unless @state == :idle
|
||||||
|
|
||||||
|
@buffer = Response::Buffer.new(
|
||||||
|
threshold_size: @threshold_size,
|
||||||
|
bytesize: @length,
|
||||||
|
encoding: @encoding
|
||||||
|
)
|
||||||
|
when :closed
|
||||||
|
return if @state == :closed
|
||||||
|
end
|
||||||
|
|
||||||
|
@state = nextstate
|
||||||
|
end
|
||||||
|
|
||||||
|
def _with_same_buffer_pos
|
||||||
|
return yield unless @buffer && @buffer.respond_to?(:pos)
|
||||||
|
|
||||||
|
# @type ivar @buffer: StringIO | Tempfile
|
||||||
|
current_pos = @buffer.pos
|
||||||
|
@buffer.rewind
|
||||||
|
begin
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
@buffer.pos = current_pos
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def initialize_inflater_by_encoding(encoding, response, **kwargs)
|
||||||
|
case encoding
|
||||||
|
when "gzip"
|
||||||
|
Transcoder::GZIP.decode(response, **kwargs)
|
||||||
|
when "deflate"
|
||||||
|
Transcoder::Deflate.decode(response, **kwargs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
90
lib/httpx/response/buffer.rb
Normal file
90
lib/httpx/response/buffer.rb
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "delegate"
|
||||||
|
require "stringio"
|
||||||
|
require "tempfile"
|
||||||
|
|
||||||
|
module HTTPX
|
||||||
|
class Response::Buffer < SimpleDelegator
|
||||||
|
def initialize(threshold_size:, bytesize: 0, encoding: Encoding::BINARY)
|
||||||
|
@threshold_size = threshold_size
|
||||||
|
@bytesize = bytesize
|
||||||
|
@encoding = encoding
|
||||||
|
try_upgrade_buffer
|
||||||
|
super(@buffer)
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_dup(other)
|
||||||
|
super
|
||||||
|
|
||||||
|
@buffer = other.instance_variable_get(:@buffer).dup
|
||||||
|
end
|
||||||
|
|
||||||
|
def size
|
||||||
|
@bytesize
|
||||||
|
end
|
||||||
|
|
||||||
|
def write(chunk)
|
||||||
|
@bytesize += chunk.bytesize
|
||||||
|
try_upgrade_buffer
|
||||||
|
@buffer.write(chunk)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
case @buffer
|
||||||
|
when StringIO
|
||||||
|
begin
|
||||||
|
@buffer.string.force_encoding(@encoding)
|
||||||
|
rescue ArgumentError
|
||||||
|
@buffer.string
|
||||||
|
end
|
||||||
|
when Tempfile
|
||||||
|
rewind
|
||||||
|
content = _with_same_buffer_pos { @buffer.read }
|
||||||
|
begin
|
||||||
|
content.force_encoding(@encoding)
|
||||||
|
rescue ArgumentError # ex: unknown encoding name - utf
|
||||||
|
content
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def close
|
||||||
|
@buffer.close
|
||||||
|
@buffer.unlink if @buffer.respond_to?(:unlink)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def try_upgrade_buffer
|
||||||
|
if !@buffer.is_a?(Tempfile) && @bytesize > @threshold_size
|
||||||
|
aux = @buffer
|
||||||
|
|
||||||
|
@buffer = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
|
||||||
|
|
||||||
|
if aux
|
||||||
|
aux.rewind
|
||||||
|
::IO.copy_stream(aux, @buffer)
|
||||||
|
aux.close
|
||||||
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
return if @buffer
|
||||||
|
|
||||||
|
@buffer = StringIO.new("".b)
|
||||||
|
|
||||||
|
end
|
||||||
|
__setobj__(@buffer)
|
||||||
|
end
|
||||||
|
|
||||||
|
def _with_same_buffer_pos
|
||||||
|
current_pos = @buffer.pos
|
||||||
|
@buffer.rewind
|
||||||
|
begin
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
@buffer.pos = current_pos
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -9,8 +9,6 @@ class HTTPX::Selector
|
|||||||
private_constant :READABLE
|
private_constant :READABLE
|
||||||
private_constant :WRITABLE
|
private_constant :WRITABLE
|
||||||
|
|
||||||
using HTTPX::IOExtensions
|
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@selectables = []
|
@selectables = []
|
||||||
end
|
end
|
||||||
|
@ -339,16 +339,6 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
# :nocov:
|
|
||||||
def plugins(pls)
|
|
||||||
warn ":#{__method__} is deprecated, use :plugin instead"
|
|
||||||
pls.each do |pl|
|
|
||||||
plugin(pl)
|
|
||||||
end
|
|
||||||
self
|
|
||||||
end
|
|
||||||
# :nocov:
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
module HTTPX
|
module HTTPX
|
||||||
module Transcoder
|
module Transcoder
|
||||||
using RegexpExtensions unless Regexp.method_defined?(:match?)
|
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def normalize_keys(key, value, cond = nil, &block)
|
def normalize_keys(key, value, cond = nil, &block)
|
||||||
@ -90,3 +88,5 @@ require "httpx/transcoder/form"
|
|||||||
require "httpx/transcoder/json"
|
require "httpx/transcoder/json"
|
||||||
require "httpx/transcoder/xml"
|
require "httpx/transcoder/xml"
|
||||||
require "httpx/transcoder/chunker"
|
require "httpx/transcoder/chunker"
|
||||||
|
require "httpx/transcoder/deflate"
|
||||||
|
require "httpx/transcoder/gzip"
|
||||||
|
@ -9,7 +9,6 @@ module HTTPX::Transcoder
|
|||||||
module_function
|
module_function
|
||||||
|
|
||||||
class Encoder
|
class Encoder
|
||||||
using HTTPX::ArrayExtensions::Sum
|
|
||||||
extend Forwardable
|
extend Forwardable
|
||||||
|
|
||||||
def_delegator :@raw, :to_s
|
def_delegator :@raw, :to_s
|
||||||
|
37
lib/httpx/transcoder/deflate.rb
Normal file
37
lib/httpx/transcoder/deflate.rb
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "zlib"
|
||||||
|
require_relative "utils/deflater"
|
||||||
|
|
||||||
|
module HTTPX
|
||||||
|
module Transcoder
|
||||||
|
module Deflate
|
||||||
|
class Deflater < Transcoder::Deflater
|
||||||
|
def deflate(chunk)
|
||||||
|
@deflater ||= Zlib::Deflate.new
|
||||||
|
|
||||||
|
if chunk.nil?
|
||||||
|
unless @deflater.closed?
|
||||||
|
last = @deflater.finish
|
||||||
|
@deflater.close
|
||||||
|
last.empty? ? nil : last
|
||||||
|
end
|
||||||
|
else
|
||||||
|
@deflater.deflate(chunk)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def encode(body)
|
||||||
|
Deflater.new(body)
|
||||||
|
end
|
||||||
|
|
||||||
|
def decode(response, bytesize: nil)
|
||||||
|
bytesize ||= response.headers.key?("content-length") ? response.headers["content-length"].to_i : Float::INFINITY
|
||||||
|
GZIP::Inflater.new(bytesize)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -2,57 +2,77 @@
|
|||||||
|
|
||||||
require "forwardable"
|
require "forwardable"
|
||||||
require "uri"
|
require "uri"
|
||||||
|
require_relative "multipart"
|
||||||
|
|
||||||
module HTTPX::Transcoder
|
module HTTPX
|
||||||
module Form
|
module Transcoder
|
||||||
module_function
|
module Form
|
||||||
|
module_function
|
||||||
|
|
||||||
PARAM_DEPTH_LIMIT = 32
|
PARAM_DEPTH_LIMIT = 32
|
||||||
|
|
||||||
class Encoder
|
class Encoder
|
||||||
extend Forwardable
|
extend Forwardable
|
||||||
|
|
||||||
def_delegator :@raw, :to_s
|
def_delegator :@raw, :to_s
|
||||||
|
|
||||||
def_delegator :@raw, :to_str
|
def_delegator :@raw, :to_str
|
||||||
|
|
||||||
def_delegator :@raw, :bytesize
|
def_delegator :@raw, :bytesize
|
||||||
|
|
||||||
def initialize(form)
|
def initialize(form)
|
||||||
@raw = form.each_with_object("".b) do |(key, val), buf|
|
@raw = form.each_with_object("".b) do |(key, val), buf|
|
||||||
HTTPX::Transcoder.normalize_keys(key, val) do |k, v|
|
HTTPX::Transcoder.normalize_keys(key, val) do |k, v|
|
||||||
buf << "&" unless buf.empty?
|
buf << "&" unless buf.empty?
|
||||||
buf << URI.encode_www_form_component(k)
|
buf << URI.encode_www_form_component(k)
|
||||||
buf << "=#{URI.encode_www_form_component(v.to_s)}" unless v.nil?
|
buf << "=#{URI.encode_www_form_component(v.to_s)}" unless v.nil?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def content_type
|
||||||
|
"application/x-www-form-urlencoded"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module Decoder
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def call(response, *)
|
||||||
|
URI.decode_www_form(response.to_s).each_with_object({}) do |(field, value), params|
|
||||||
|
HTTPX::Transcoder.normalize_query(params, field, value, PARAM_DEPTH_LIMIT)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def content_type
|
def encode(form)
|
||||||
"application/x-www-form-urlencoded"
|
if multipart?(form)
|
||||||
|
Multipart::Encoder.new(form)
|
||||||
|
else
|
||||||
|
Encoder.new(form)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
module Decoder
|
def decode(response)
|
||||||
module_function
|
content_type = response.content_type.mime_type
|
||||||
|
|
||||||
def call(response, *)
|
case content_type
|
||||||
URI.decode_www_form(response.to_s).each_with_object({}) do |(field, value), params|
|
when "application/x-www-form-urlencoded"
|
||||||
HTTPX::Transcoder.normalize_query(params, field, value, PARAM_DEPTH_LIMIT)
|
Decoder
|
||||||
|
when "multipart/form-data"
|
||||||
|
Multipart::Decoder.new(response)
|
||||||
|
else
|
||||||
|
raise Error, "invalid form mime type (#{content_type})"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def multipart?(data)
|
||||||
|
data.any? do |_, v|
|
||||||
|
Multipart::MULTIPART_VALUE_COND.call(v) ||
|
||||||
|
(v.respond_to?(:to_ary) && v.to_ary.any?(&Multipart::MULTIPART_VALUE_COND)) ||
|
||||||
|
(v.respond_to?(:to_hash) && v.to_hash.any? { |_, e| Multipart::MULTIPART_VALUE_COND.call(e) })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def encode(form)
|
|
||||||
Encoder.new(form)
|
|
||||||
end
|
|
||||||
|
|
||||||
def decode(response)
|
|
||||||
content_type = response.content_type.mime_type
|
|
||||||
|
|
||||||
raise HTTPX::Error, "invalid form mime type (#{content_type})" unless content_type == "application/x-www-form-urlencoded"
|
|
||||||
|
|
||||||
Decoder
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
74
lib/httpx/transcoder/gzip.rb
Normal file
74
lib/httpx/transcoder/gzip.rb
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "forwardable"
|
||||||
|
require "uri"
|
||||||
|
require "stringio"
|
||||||
|
require "zlib"
|
||||||
|
|
||||||
|
module HTTPX
|
||||||
|
module Transcoder
|
||||||
|
module GZIP
|
||||||
|
class Deflater < Transcoder::Deflater
|
||||||
|
def initialize(body)
|
||||||
|
@compressed_chunk = "".b
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
def deflate(chunk)
|
||||||
|
@deflater ||= Zlib::GzipWriter.new(self)
|
||||||
|
|
||||||
|
if chunk.nil?
|
||||||
|
unless @deflater.closed?
|
||||||
|
@deflater.flush
|
||||||
|
@deflater.close
|
||||||
|
compressed_chunk
|
||||||
|
end
|
||||||
|
else
|
||||||
|
@deflater.write(chunk)
|
||||||
|
compressed_chunk
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def write(chunk)
|
||||||
|
@compressed_chunk << chunk
|
||||||
|
end
|
||||||
|
|
||||||
|
def compressed_chunk
|
||||||
|
@compressed_chunk.dup
|
||||||
|
ensure
|
||||||
|
@compressed_chunk.clear
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Inflater
|
||||||
|
def initialize(bytesize)
|
||||||
|
@inflater = Zlib::Inflate.new(Zlib::MAX_WBITS + 32)
|
||||||
|
@bytesize = bytesize
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(chunk)
|
||||||
|
buffer = @inflater.inflate(chunk)
|
||||||
|
@bytesize -= chunk.bytesize
|
||||||
|
if @bytesize <= 0
|
||||||
|
buffer << @inflater.finish
|
||||||
|
@inflater.close
|
||||||
|
end
|
||||||
|
buffer
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def encode(body)
|
||||||
|
Deflater.new(body)
|
||||||
|
end
|
||||||
|
|
||||||
|
def decode(response, bytesize: nil)
|
||||||
|
bytesize ||= response.headers.key?("content-length") ? response.headers["content-length"].to_i : Float::INFINITY
|
||||||
|
Inflater.new(bytesize)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -4,12 +4,10 @@ require "forwardable"
|
|||||||
|
|
||||||
module HTTPX::Transcoder
|
module HTTPX::Transcoder
|
||||||
module JSON
|
module JSON
|
||||||
JSON_REGEX = %r{\bapplication/(?:vnd\.api\+)?json\b}i.freeze
|
|
||||||
|
|
||||||
using HTTPX::RegexpExtensions unless Regexp.method_defined?(:match?)
|
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
|
JSON_REGEX = %r{\bapplication/(?:vnd\.api\+)?json\b}i.freeze
|
||||||
|
|
||||||
class Encoder
|
class Encoder
|
||||||
extend Forwardable
|
extend Forwardable
|
||||||
|
|
||||||
|
17
lib/httpx/transcoder/multipart.rb
Normal file
17
lib/httpx/transcoder/multipart.rb
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative "multipart/encoder"
|
||||||
|
require_relative "multipart/decoder"
|
||||||
|
require_relative "multipart/part"
|
||||||
|
require_relative "multipart/mime_type_detector"
|
||||||
|
|
||||||
|
module HTTPX::Transcoder
|
||||||
|
module Multipart
|
||||||
|
MULTIPART_VALUE_COND = lambda do |value|
|
||||||
|
value.respond_to?(:read) ||
|
||||||
|
(value.respond_to?(:to_hash) &&
|
||||||
|
value.key?(:body) &&
|
||||||
|
(value.key?(:filename) || value.key?(:content_type)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
139
lib/httpx/transcoder/multipart/decoder.rb
Normal file
139
lib/httpx/transcoder/multipart/decoder.rb
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "tempfile"
|
||||||
|
require "delegate"
|
||||||
|
|
||||||
|
module HTTPX
|
||||||
|
module Transcoder
|
||||||
|
module Multipart
|
||||||
|
class FilePart < SimpleDelegator
|
||||||
|
attr_reader :original_filename, :content_type
|
||||||
|
|
||||||
|
def initialize(filename, content_type)
|
||||||
|
@original_filename = filename
|
||||||
|
@content_type = content_type
|
||||||
|
@file = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
|
||||||
|
super(@file)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Decoder
|
||||||
|
include HTTPX::Utils
|
||||||
|
|
||||||
|
CRLF = "\r\n"
|
||||||
|
BOUNDARY_RE = /;\s*boundary=([^;]+)/i.freeze
|
||||||
|
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{CRLF}/ni.freeze
|
||||||
|
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni.freeze
|
||||||
|
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{CRLF}]*)/ni.freeze
|
||||||
|
WINDOW_SIZE = 2 << 14
|
||||||
|
|
||||||
|
def initialize(response)
|
||||||
|
@boundary = begin
|
||||||
|
m = response.headers["content-type"].to_s[BOUNDARY_RE, 1]
|
||||||
|
raise Error, "no boundary declared in content-type header" unless m
|
||||||
|
|
||||||
|
m.strip
|
||||||
|
end
|
||||||
|
@buffer = "".b
|
||||||
|
@parts = {}
|
||||||
|
@intermediate_boundary = "--#{@boundary}"
|
||||||
|
@state = :idle
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(response, *)
|
||||||
|
response.body.each do |chunk|
|
||||||
|
@buffer << chunk
|
||||||
|
|
||||||
|
parse
|
||||||
|
end
|
||||||
|
|
||||||
|
raise Error, "invalid or unsupported multipart format" unless @buffer.empty?
|
||||||
|
|
||||||
|
@parts
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def parse
|
||||||
|
case @state
|
||||||
|
when :idle
|
||||||
|
raise Error, "payload does not start with boundary" unless @buffer.start_with?("#{@intermediate_boundary}#{CRLF}")
|
||||||
|
|
||||||
|
@buffer = @buffer.byteslice(@intermediate_boundary.bytesize + 2..-1)
|
||||||
|
|
||||||
|
@state = :part_header
|
||||||
|
when :part_header
|
||||||
|
idx = @buffer.index("#{CRLF}#{CRLF}")
|
||||||
|
|
||||||
|
# raise Error, "couldn't parse part headers" unless idx
|
||||||
|
return unless idx
|
||||||
|
|
||||||
|
head = @buffer.byteslice(0..idx + 4 - 1)
|
||||||
|
|
||||||
|
@buffer = @buffer.byteslice(head.bytesize..-1)
|
||||||
|
|
||||||
|
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
||||||
|
if (name = head[MULTIPART_CONTENT_DISPOSITION, 1])
|
||||||
|
name = /\A"(.*)"\Z/ =~ name ? Regexp.last_match(1) : name.dup
|
||||||
|
name.gsub!(/\\(.)/, "\\1")
|
||||||
|
name
|
||||||
|
else
|
||||||
|
name = head[MULTIPART_CONTENT_ID, 1]
|
||||||
|
end
|
||||||
|
|
||||||
|
filename = HTTPX::Utils.get_filename(head)
|
||||||
|
|
||||||
|
name = filename || +"#{content_type || "text/plain"}[]" if name.nil? || name.empty?
|
||||||
|
|
||||||
|
@current = name
|
||||||
|
|
||||||
|
@parts[name] = if filename
|
||||||
|
FilePart.new(filename, content_type)
|
||||||
|
else
|
||||||
|
"".b
|
||||||
|
end
|
||||||
|
|
||||||
|
@state = :part_body
|
||||||
|
when :part_body
|
||||||
|
part = @parts[@current]
|
||||||
|
|
||||||
|
body_separator = if part.is_a?(FilePart)
|
||||||
|
"#{CRLF}#{CRLF}"
|
||||||
|
else
|
||||||
|
CRLF
|
||||||
|
end
|
||||||
|
idx = @buffer.index(body_separator)
|
||||||
|
|
||||||
|
if idx
|
||||||
|
payload = @buffer.byteslice(0..idx - 1)
|
||||||
|
@buffer = @buffer.byteslice(idx + body_separator.bytesize..-1)
|
||||||
|
part << payload
|
||||||
|
part.rewind if part.respond_to?(:rewind)
|
||||||
|
@state = :parse_boundary
|
||||||
|
else
|
||||||
|
part << @buffer
|
||||||
|
@buffer.clear
|
||||||
|
end
|
||||||
|
when :parse_boundary
|
||||||
|
raise Error, "payload does not start with boundary" unless @buffer.start_with?(@intermediate_boundary)
|
||||||
|
|
||||||
|
@buffer = @buffer.byteslice(@intermediate_boundary.bytesize..-1)
|
||||||
|
|
||||||
|
if @buffer == "--"
|
||||||
|
@buffer.clear
|
||||||
|
@state = :done
|
||||||
|
return
|
||||||
|
elsif @buffer.start_with?(CRLF)
|
||||||
|
@buffer = @buffer.byteslice(2..-1)
|
||||||
|
@state = :part_header
|
||||||
|
else
|
||||||
|
return
|
||||||
|
end
|
||||||
|
when :done
|
||||||
|
raise Error, "parsing should have been over by now"
|
||||||
|
end until @buffer.empty?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module HTTPX::Plugins
|
module HTTPX
|
||||||
module Multipart
|
module Transcoder::Multipart
|
||||||
class Encoder
|
class Encoder
|
||||||
attr_reader :bytesize
|
attr_reader :bytesize
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ module HTTPX::Plugins
|
|||||||
def to_parts(form)
|
def to_parts(form)
|
||||||
@bytesize = 0
|
@bytesize = 0
|
||||||
params = form.each_with_object([]) do |(key, val), aux|
|
params = form.each_with_object([]) do |(key, val), aux|
|
||||||
Multipart.normalize_keys(key, val) do |k, v|
|
Transcoder.normalize_keys(key, val, MULTIPART_VALUE_COND) do |k, v|
|
||||||
next if v.nil?
|
next if v.nil?
|
||||||
|
|
||||||
value, content_type, filename = Part.call(v)
|
value, content_type, filename = Part.call(v)
|
@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module HTTPX
|
module HTTPX
|
||||||
module Plugins::Multipart
|
module Transcoder::Multipart
|
||||||
module MimeTypeDetector
|
module MimeTypeDetector
|
||||||
module_function
|
module_function
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module HTTPX
|
module HTTPX
|
||||||
module Plugins::Multipart
|
module Transcoder::Multipart
|
||||||
module Part
|
module Part
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
@ -21,7 +21,8 @@ module HTTPX
|
|||||||
|
|
||||||
value = value.open(File::RDONLY) if Object.const_defined?(:Pathname) && value.is_a?(Pathname)
|
value = value.open(File::RDONLY) if Object.const_defined?(:Pathname) && value.is_a?(Pathname)
|
||||||
|
|
||||||
if value.is_a?(File)
|
if value.respond_to?(:path) && value.respond_to?(:read)
|
||||||
|
# either a File, a Tempfile, or something else which has to quack like a file
|
||||||
filename ||= File.basename(value.path)
|
filename ||= File.basename(value.path)
|
||||||
content_type ||= MimeTypeDetector.call(value, filename) || "application/octet-stream"
|
content_type ||= MimeTypeDetector.call(value, filename) || "application/octet-stream"
|
||||||
[value, content_type, filename]
|
[value, content_type, filename]
|
46
lib/httpx/transcoder/utils/body_reader.rb
Normal file
46
lib/httpx/transcoder/utils/body_reader.rb
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "stringio"
|
||||||
|
|
||||||
|
module HTTPX
|
||||||
|
module Transcoder
|
||||||
|
class BodyReader
|
||||||
|
def initialize(body)
|
||||||
|
@body = if body.respond_to?(:read)
|
||||||
|
body.rewind if body.respond_to?(:rewind)
|
||||||
|
body
|
||||||
|
elsif body.respond_to?(:each)
|
||||||
|
body.enum_for(:each)
|
||||||
|
else
|
||||||
|
StringIO.new(body.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def bytesize
|
||||||
|
return @body.bytesize if @body.respond_to?(:bytesize)
|
||||||
|
|
||||||
|
Float::INFINITY
|
||||||
|
end
|
||||||
|
|
||||||
|
def read(length = nil, outbuf = nil)
|
||||||
|
return @body.read(length, outbuf) if @body.respond_to?(:read)
|
||||||
|
|
||||||
|
begin
|
||||||
|
chunk = @body.next
|
||||||
|
if outbuf
|
||||||
|
outbuf.clear.force_encoding(Encoding::BINARY)
|
||||||
|
outbuf << chunk
|
||||||
|
else
|
||||||
|
outbuf = chunk
|
||||||
|
end
|
||||||
|
outbuf unless length && outbuf.empty?
|
||||||
|
rescue StopIteration
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def close
|
||||||
|
@body.close if @body.respond_to?(:close)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
72
lib/httpx/transcoder/utils/deflater.rb
Normal file
72
lib/httpx/transcoder/utils/deflater.rb
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "forwardable"
|
||||||
|
require_relative "body_reader"
|
||||||
|
|
||||||
|
module HTTPX
|
||||||
|
module Transcoder
|
||||||
|
class Deflater
|
||||||
|
extend Forwardable
|
||||||
|
|
||||||
|
attr_reader :content_type
|
||||||
|
|
||||||
|
def initialize(body)
|
||||||
|
@content_type = body.content_type
|
||||||
|
@body = BodyReader.new(body)
|
||||||
|
@closed = false
|
||||||
|
end
|
||||||
|
|
||||||
|
def bytesize
|
||||||
|
buffer_deflate!
|
||||||
|
|
||||||
|
@buffer.size
|
||||||
|
end
|
||||||
|
|
||||||
|
def read(length = nil, outbuf = nil)
|
||||||
|
return @buffer.read(length, outbuf) if @buffer
|
||||||
|
|
||||||
|
return if @closed
|
||||||
|
|
||||||
|
chunk = @body.read(length)
|
||||||
|
|
||||||
|
compressed_chunk = deflate(chunk)
|
||||||
|
|
||||||
|
return unless compressed_chunk
|
||||||
|
|
||||||
|
if outbuf
|
||||||
|
outbuf.clear.force_encoding(Encoding::BINARY)
|
||||||
|
outbuf << compressed_chunk
|
||||||
|
else
|
||||||
|
compressed_chunk
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def close
|
||||||
|
return if @closed
|
||||||
|
|
||||||
|
@buffer.close if @buffer
|
||||||
|
|
||||||
|
@body.close
|
||||||
|
|
||||||
|
@closed = true
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# rubocop:disable Naming/MemoizedInstanceVariableName
|
||||||
|
def buffer_deflate!
|
||||||
|
return @buffer if defined?(@buffer)
|
||||||
|
|
||||||
|
buffer = Response::Buffer.new(
|
||||||
|
threshold_size: Options::MAX_BODY_THRESHOLD_SIZE
|
||||||
|
)
|
||||||
|
::IO.copy_stream(self, buffer)
|
||||||
|
|
||||||
|
buffer.rewind
|
||||||
|
|
||||||
|
@buffer = buffer
|
||||||
|
end
|
||||||
|
# rubocop:enable Naming/MemoizedInstanceVariableName
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -6,8 +6,6 @@ require "uri"
|
|||||||
|
|
||||||
module HTTPX::Transcoder
|
module HTTPX::Transcoder
|
||||||
module Xml
|
module Xml
|
||||||
using HTTPX::RegexpExtensions
|
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
MIME_TYPES = %r{\b(application|text)/(.+\+)?xml\b}.freeze
|
MIME_TYPES = %r{\b(application|text)/(.+\+)?xml\b}.freeze
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
module HTTPX
|
module HTTPX
|
||||||
module Utils
|
module Utils
|
||||||
using URIExtensions
|
using URIExtensions
|
||||||
using HTTPX::RegexpExtensions unless Regexp.method_defined?(:match?)
|
|
||||||
|
|
||||||
TOKEN = %r{[^\s()<>,;:\\"/\[\]?=]+}.freeze
|
TOKEN = %r{[^\s()<>,;:\\"/\[\]?=]+}.freeze
|
||||||
VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/.freeze
|
VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/.freeze
|
||||||
@ -55,31 +54,22 @@ module HTTPX
|
|||||||
filename
|
filename
|
||||||
end
|
end
|
||||||
|
|
||||||
if RUBY_VERSION < "2.3"
|
URIParser = URI::RFC2396_Parser.new
|
||||||
|
|
||||||
def to_uri(uri)
|
def to_uri(uri)
|
||||||
URI(uri)
|
return URI(uri) unless uri.is_a?(String) && !uri.ascii_only?
|
||||||
end
|
|
||||||
|
|
||||||
else
|
uri = URI(URIParser.escape(uri))
|
||||||
|
|
||||||
URIParser = URI::RFC2396_Parser.new
|
non_ascii_hostname = URIParser.unescape(uri.host)
|
||||||
|
|
||||||
def to_uri(uri)
|
non_ascii_hostname.force_encoding(Encoding::UTF_8)
|
||||||
return URI(uri) unless uri.is_a?(String) && !uri.ascii_only?
|
|
||||||
|
|
||||||
uri = URI(URIParser.escape(uri))
|
idna_hostname = Punycode.encode_hostname(non_ascii_hostname)
|
||||||
|
|
||||||
non_ascii_hostname = URIParser.unescape(uri.host)
|
uri.host = idna_hostname
|
||||||
|
uri.non_ascii_hostname = non_ascii_hostname
|
||||||
non_ascii_hostname.force_encoding(Encoding::UTF_8)
|
uri
|
||||||
|
|
||||||
idna_hostname = Punycode.encode_hostname(non_ascii_hostname)
|
|
||||||
|
|
||||||
uri.host = idna_hostname
|
|
||||||
uri.non_ascii_hostname = non_ascii_hostname
|
|
||||||
uri
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,22 +2,6 @@
|
|||||||
|
|
||||||
require "objspace"
|
require "objspace"
|
||||||
|
|
||||||
unless ObjectSpace.method_defined?(:memsize_of_all)
|
|
||||||
module ObjectSpace
|
|
||||||
module_function
|
|
||||||
|
|
||||||
def memsize_of_all(klass = false)
|
|
||||||
total = 0
|
|
||||||
total_mem = 0
|
|
||||||
ObjectSpace.each_object(klass) do |e|
|
|
||||||
total += 1
|
|
||||||
total_mem += ObjectSpace.memsize_of(e)
|
|
||||||
end
|
|
||||||
[total, total_mem]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module ProfilerHelpers
|
module ProfilerHelpers
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
|
@ -9,8 +9,7 @@ class Bug_0_14_1_Test < Minitest::Test
|
|||||||
def test_multipart_can_have_arbitrary_content_type
|
def test_multipart_can_have_arbitrary_content_type
|
||||||
uri = "https://#{httpbin}/post"
|
uri = "https://#{httpbin}/post"
|
||||||
|
|
||||||
response = HTTPX.plugin(:multipart)
|
response = HTTPX.post(uri, form: {
|
||||||
.post(uri, form: {
|
|
||||||
image: {
|
image: {
|
||||||
content_type: "image/png",
|
content_type: "image/png",
|
||||||
body: File.new(fixture_file_path),
|
body: File.new(fixture_file_path),
|
||||||
|
@ -9,8 +9,7 @@ class Bug_0_14_2_Test < Minitest::Test
|
|||||||
def test_multipart_can_have_arbitrary_filename
|
def test_multipart_can_have_arbitrary_filename
|
||||||
uri = "https://#{httpbin}/post"
|
uri = "https://#{httpbin}/post"
|
||||||
|
|
||||||
response = HTTPX.plugin(:multipart)
|
response = HTTPX.post(uri, form: {
|
||||||
.post(uri, form: {
|
|
||||||
image: {
|
image: {
|
||||||
filename: "weird-al-jankovic",
|
filename: "weird-al-jankovic",
|
||||||
body: File.new(fixture_file_path),
|
body: File.new(fixture_file_path),
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require "test_helper"
|
|
||||||
require "support/http_helpers"
|
|
||||||
require "support/minitest_extensions"
|
|
||||||
|
|
||||||
class Bug_0_18_2_Test < Minitest::Test
|
|
||||||
include HTTPHelpers
|
|
||||||
|
|
||||||
def test_no_loop_forever_when_total_timeout_on_persistent
|
|
||||||
session = HTTPX.plugin(:persistent).with_timeout(total_timeout: 5)
|
|
||||||
|
|
||||||
response1 = session.get("https://#{httpbin}/get")
|
|
||||||
sleep 2
|
|
||||||
response2 = session.get("https://#{httpbin}/get")
|
|
||||||
verify_status(response1, 200)
|
|
||||||
verify_status(response2, 200)
|
|
||||||
end
|
|
||||||
end
|
|
@ -12,18 +12,17 @@ module HTTPX
|
|||||||
def with: (options) -> Session
|
def with: (options) -> Session
|
||||||
| (options) { (Session) -> void } -> void
|
| (options) { (Session) -> void } -> void
|
||||||
|
|
||||||
def plugin: (:authentication, ?options) -> Plugins::sessionAuthentication
|
def plugin: (:auth, ?options) -> Plugins::sessionAuthorization
|
||||||
| (:basic_authentication, ?options) -> Plugins::sessionBasicAuth
|
| (:basic_auth, ?options) -> Plugins::sessionBasicAuth
|
||||||
| (:digest_authentication, ?options) -> Plugins::sessionDigestAuth
|
| (:digest_auth, ?options) -> Plugins::sessionDigestAuth
|
||||||
| (:ntlm_authentication, ?options) -> Plugins::sessionNTLMAuth
|
| (:ntlm_auth, ?options) -> Plugins::sessionNTLMAuth
|
||||||
| (:aws_sdk_authentication, ?options) -> Plugins::sessionAwsSdkAuthentication
|
| (:aws_sdk_authentication, ?options) -> Plugins::sessionAwsSdkAuthentication
|
||||||
| (:compression, ?options) -> Session
|
| (:brotli, ?options) -> Session
|
||||||
| (:cookies, ?options) -> Plugins::sessionCookies
|
| (:cookies, ?options) -> Plugins::sessionCookies
|
||||||
| (:expect, ?options) -> Session
|
| (:expect, ?options) -> Session
|
||||||
| (:follow_redirects, ?options) -> Plugins::sessionFollowRedirects
|
| (:follow_redirects, ?options) -> Plugins::sessionFollowRedirects
|
||||||
| (:upgrade, ?options) -> Session
|
| (:upgrade, ?options) -> Session
|
||||||
| (:h2c, ?options) -> Session
|
| (:h2c, ?options) -> Session
|
||||||
| (:multipart, ?options) -> Session
|
|
||||||
| (:persistent, ?options) -> Plugins::sessionPersistent
|
| (:persistent, ?options) -> Plugins::sessionPersistent
|
||||||
| (:proxy, ?options) -> (Plugins::sessionProxy & Plugins::httpProxy)
|
| (:proxy, ?options) -> (Plugins::sessionProxy & Plugins::httpProxy)
|
||||||
| (:push_promise, ?options) -> Plugins::sessionPushPromise
|
| (:push_promise, ?options) -> Plugins::sessionPushPromise
|
||||||
|
@ -36,7 +36,6 @@ module HTTPX
|
|||||||
@keep_alive_timeout: Numeric?
|
@keep_alive_timeout: Numeric?
|
||||||
@timeout: Numeric?
|
@timeout: Numeric?
|
||||||
@current_timeout: Numeric?
|
@current_timeout: Numeric?
|
||||||
@total_timeout: Numeric?
|
|
||||||
@io: TCP | SSL | UNIX
|
@io: TCP | SSL | UNIX
|
||||||
@parser: HTTP1 | HTTP2 | _Parser
|
@parser: HTTP1 | HTTP2 | _Parser
|
||||||
@connected_at: Float
|
@connected_at: Float
|
||||||
|
@ -17,9 +17,6 @@ module HTTPX
|
|||||||
def initialize: (Numeric timeout, String message) -> untyped
|
def initialize: (Numeric timeout, String message) -> untyped
|
||||||
end
|
end
|
||||||
|
|
||||||
class TotalTimeoutError < TimeoutError
|
|
||||||
end
|
|
||||||
|
|
||||||
class ConnectTimeoutError < TimeoutError
|
class ConnectTimeoutError < TimeoutError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -11,13 +11,11 @@ module HTTPX
|
|||||||
SETTINGS_TIMEOUT: Integer
|
SETTINGS_TIMEOUT: Integer
|
||||||
DEFAULT_OPTIONS: Hash[Symbol, untyped]
|
DEFAULT_OPTIONS: Hash[Symbol, untyped]
|
||||||
|
|
||||||
type timeout_type = :connect_timeout | :settings_timeout | :operation_timeout | :keep_alive_timeout | :total_timeout | :read_timeout | :write_timeout | :request_timeout
|
type timeout_type = :connect_timeout | :settings_timeout | :operation_timeout | :keep_alive_timeout | :read_timeout | :write_timeout | :request_timeout
|
||||||
type timeout = Hash[timeout_type, Numeric]
|
type timeout = Hash[timeout_type, Numeric]
|
||||||
|
|
||||||
def self.new: (?options) -> instance
|
def self.new: (?options) -> instance
|
||||||
|
|
||||||
def self.def_option: (Symbol, ?String) -> void
|
|
||||||
| (Symbol) { (*nil) -> untyped } -> void
|
|
||||||
# headers
|
# headers
|
||||||
attr_reader uri: URI?
|
attr_reader uri: URI?
|
||||||
|
|
||||||
@ -48,12 +46,18 @@ module HTTPX
|
|||||||
# transport
|
# transport
|
||||||
attr_reader transport: "unix" | nil
|
attr_reader transport: "unix" | nil
|
||||||
|
|
||||||
# transport_options
|
|
||||||
attr_reader transport_options: Hash[untyped, untyped]?
|
|
||||||
|
|
||||||
# addresses
|
# addresses
|
||||||
attr_reader addresses: Array[ipaddr]?
|
attr_reader addresses: Array[ipaddr]?
|
||||||
|
|
||||||
|
# supported_compression_formats
|
||||||
|
attr_reader supported_compression_formats: Array[String]
|
||||||
|
|
||||||
|
# compress_request_body
|
||||||
|
attr_reader compress_request_body: bool
|
||||||
|
|
||||||
|
# decompress_response_body
|
||||||
|
attr_reader decompress_response_body: bool
|
||||||
|
|
||||||
# params
|
# params
|
||||||
attr_reader params: Transcoder::urlencoded_input?
|
attr_reader params: Transcoder::urlencoded_input?
|
||||||
|
|
||||||
@ -124,9 +128,9 @@ module HTTPX
|
|||||||
|
|
||||||
REQUEST_IVARS: Array[Symbol]
|
REQUEST_IVARS: Array[Symbol]
|
||||||
|
|
||||||
def initialize: (?options options) -> untyped
|
def initialize: (?options options) -> void
|
||||||
|
|
||||||
def __initialize__: (?options options) -> untyped
|
def do_initialize: (?options options) -> void
|
||||||
end
|
end
|
||||||
|
|
||||||
type options = Options | Hash[Symbol, untyped]
|
type options = Options | Hash[Symbol, untyped]
|
||||||
|
13
sig/plugins/auth.rbs
Normal file
13
sig/plugins/auth.rbs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
module HTTPX
|
||||||
|
module Plugins
|
||||||
|
module Authorization
|
||||||
|
module InstanceMethods
|
||||||
|
def authorization: (string token) -> instance
|
||||||
|
|
||||||
|
def bearer_auth: (string token) -> instance
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
type sessionAuthorization = Session & Authorization::InstanceMethods
|
||||||
|
end
|
||||||
|
end
|
@ -5,8 +5,6 @@ module HTTPX
|
|||||||
@user: String
|
@user: String
|
||||||
@password: String
|
@password: String
|
||||||
|
|
||||||
def can_authenticate?: (String? authenticate) -> boolish
|
|
||||||
|
|
||||||
def authenticate: (*untyped) -> String
|
def authenticate: (*untyped) -> String
|
||||||
|
|
||||||
private
|
private
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user