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:
|
||||
./spec.sh jruby 9.0.0.0
|
||||
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_settings
|
||||
script:
|
||||
|
@ -23,7 +23,6 @@ AllCops:
|
||||
- 'vendor/**/*'
|
||||
- 'www/**/*'
|
||||
- 'lib/httpx/extensions.rb'
|
||||
- 'lib/httpx/punycode.rb'
|
||||
# Do not lint ffi block, for openssl parity
|
||||
- 'test/extensions/response_pattern_match.rb'
|
||||
|
||||
|
@ -6,5 +6,4 @@ SimpleCov.start do
|
||||
add_filter "/integration_tests/"
|
||||
add_filter "/regression_tests/"
|
||||
add_filter "/lib/httpx/plugins/internal_telemetry.rb"
|
||||
add_filter "/lib/httpx/punycode.rb"
|
||||
end
|
||||
|
@ -56,13 +56,13 @@ HTTPX.delete("https://myapi.com/users/1")
|
||||
require "httpx"
|
||||
|
||||
# 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
|
||||
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
|
||||
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
|
||||
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.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
|
||||
|
27
Gemfile
27
Gemfile
@ -8,31 +8,19 @@ gemspec
|
||||
gem "rake", "~> 13.0"
|
||||
|
||||
group :test do
|
||||
gem "ddtrace"
|
||||
gem "http-form_data", ">= 2.0.0"
|
||||
gem "minitest"
|
||||
gem "minitest-proveit"
|
||||
gem "nokogiri"
|
||||
gem "ruby-ntlm"
|
||||
gem "sentry-ruby" if RUBY_VERSION >= "2.4.0"
|
||||
gem "sentry-ruby"
|
||||
gem "spy"
|
||||
gem "webmock"
|
||||
gem "websocket-driver"
|
||||
|
||||
gem "net-ssh", "~> 4.2.0" if RUBY_VERSION < "2.2.0"
|
||||
|
||||
gem "ddtrace"
|
||||
|
||||
platform :mri do
|
||||
if RUBY_VERSION < "2.5.0"
|
||||
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 "grpc"
|
||||
gem "logging"
|
||||
gem "marcel", require: false
|
||||
gem "mimemagic", require: false
|
||||
@ -55,13 +43,12 @@ group :test do
|
||||
end
|
||||
|
||||
platform :jruby do
|
||||
gem "jruby-openssl" # , git: "https://github.com/jruby/jruby-openssl.git", branch: "master"
|
||||
gem "ruby-debug"
|
||||
end
|
||||
|
||||
gem "aws-sdk-s3"
|
||||
gem "faraday"
|
||||
gem "idnx" if RUBY_VERSION >= "2.4.0"
|
||||
gem "idnx"
|
||||
gem "oga"
|
||||
|
||||
if RUBY_VERSION >= "3.0.0"
|
||||
@ -72,11 +59,7 @@ group :test do
|
||||
end
|
||||
|
||||
group :coverage do
|
||||
if RUBY_VERSION < "2.5"
|
||||
gem "simplecov", "< 0.21.0"
|
||||
else
|
||||
gem "simplecov"
|
||||
end
|
||||
gem "simplecov"
|
||||
end
|
||||
|
||||
group :assorted do
|
||||
|
48
LICENSE.txt
48
LICENSE.txt
@ -189,51 +189,3 @@
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
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)
|
||||
* Streaming Requests
|
||||
* Authentication (Basic Auth, Digest Auth, NTLM)
|
||||
* Auth (Basic Auth, Digest Auth, NTLM)
|
||||
* Expect 100-continue
|
||||
* Multipart Requests
|
||||
* Advanced Cookie handling
|
||||
@ -113,7 +113,7 @@ response = HTTPX.plugin(:basic_authentication)
|
||||
.get("https://www.google.com")
|
||||
|
||||
# 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.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
|
||||
|
||||
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
|
||||
| | |
|
||||
@ -149,15 +149,6 @@ All Rubies greater or equal to 2.1, and always latest JRuby and Truffleruby.
|
||||
|
||||
## 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
|
||||
|
||||
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'
|
||||
services:
|
||||
httpx:
|
||||
image: jruby:9.3
|
||||
image: jruby:9.4
|
||||
environment:
|
||||
- JRUBY_OPTS=--debug
|
||||
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 "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 = PAGES.times.map do |page|
|
||||
|
@ -33,4 +33,6 @@ Gem::Specification.new do |gem|
|
||||
gem.require_paths = ["lib"]
|
||||
|
||||
gem.add_runtime_dependency "http-2-next", ">= 0.4.1"
|
||||
|
||||
gem.required_ruby_version = ">= 2.7.0"
|
||||
end
|
||||
|
@ -241,59 +241,29 @@ class DatadogTest < Minitest::Test
|
||||
assert span.get_metric("_dd1.sr.eausr") == sample_rate
|
||||
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|
|
||||
c.tracing.instrument(:httpx, options, &blk)
|
||||
end
|
||||
|
||||
tracer # initialize tracer patches
|
||||
def set_datadog(options = {}, &blk)
|
||||
Datadog.configure do |c|
|
||||
c.tracing.instrument(:httpx, options, &blk)
|
||||
end
|
||||
|
||||
def tracer
|
||||
@tracer ||= begin
|
||||
tr = Datadog::Tracing.send(:tracer)
|
||||
def tr.write(trace)
|
||||
@traces ||= []
|
||||
@traces << trace
|
||||
end
|
||||
tr
|
||||
tracer # initialize tracer patches
|
||||
end
|
||||
|
||||
def tracer
|
||||
@tracer ||= begin
|
||||
tr = Datadog::Tracing.send(:tracer)
|
||||
def tr.write(trace)
|
||||
@traces ||= []
|
||||
@traces << trace
|
||||
end
|
||||
tr
|
||||
end
|
||||
end
|
||||
|
||||
def trace_with_sampling_priority(priority)
|
||||
tracer.trace("foo.bar") do
|
||||
tracer.active_trace.sampling_priority = priority
|
||||
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
|
||||
def trace_with_sampling_priority(priority)
|
||||
tracer.trace("foo.bar") do
|
||||
tracer.active_trace.sampling_priority = priority
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
@ -305,11 +275,7 @@ class DatadogTest < Minitest::Test
|
||||
# Retrieves and sorts all spans in the current tracer instance.
|
||||
# This method does not cache its results.
|
||||
def fetch_spans
|
||||
spans = if defined?(::DDTrace) && Gem::Version.new(::DDTrace::VERSION::STRING) >= Gem::Version.new("1.0.0")
|
||||
(tracer.instance_variable_get(:@traces) || []).map(&:spans)
|
||||
else
|
||||
tracer.instance_variable_get(:@spans) || []
|
||||
end
|
||||
spans = (tracer.instance_variable_get(:@traces) || []).map(&:spans)
|
||||
spans.flatten.sort! do |a, b|
|
||||
if a.name == b.name
|
||||
if a.resource == b.resource
|
||||
|
@ -1,150 +1,148 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
if RUBY_VERSION >= "2.4.0"
|
||||
require "logger"
|
||||
require "stringio"
|
||||
require "sentry-ruby"
|
||||
require "test_helper"
|
||||
require "support/http_helpers"
|
||||
require "httpx/adapters/sentry"
|
||||
require "logger"
|
||||
require "stringio"
|
||||
require "sentry-ruby"
|
||||
require "test_helper"
|
||||
require "support/http_helpers"
|
||||
require "httpx/adapters/sentry"
|
||||
|
||||
class SentryTest < Minitest::Test
|
||||
include HTTPHelpers
|
||||
class SentryTest < Minitest::Test
|
||||
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
|
||||
before_pii = Sentry.configuration.send_default_pii
|
||||
begin
|
||||
Sentry.configuration.send_default_pii = true
|
||||
def test_sentry_send_yes_pii
|
||||
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("/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
|
||||
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
|
||||
uri = build_uri("/get")
|
||||
|
||||
def test_sentry_server_error_request
|
||||
transaction = Sentry.start_transaction
|
||||
Sentry.get_current_scope.set_span(transaction)
|
||||
response = HTTPX.get(uri, params: { "foo" => "bar" })
|
||||
|
||||
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")
|
||||
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 == { 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}"
|
||||
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
|
||||
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
|
||||
|
@ -53,14 +53,6 @@ module HTTPX
|
||||
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
|
||||
end
|
||||
|
||||
|
@ -1,51 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
if defined?(DDTrace) && DDTrace::VERSION::STRING >= "1.0.0"
|
||||
require "datadog/tracing/contrib/integration"
|
||||
require "datadog/tracing/contrib/configuration/settings"
|
||||
require "datadog/tracing/contrib/patcher"
|
||||
require "datadog/tracing/contrib/integration"
|
||||
require "datadog/tracing/contrib/configuration/settings"
|
||||
require "datadog/tracing/contrib/patcher"
|
||||
|
||||
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 Datadog::Tracing
|
||||
module Contrib
|
||||
module HTTPX
|
||||
if defined?(::DDTrace) && ::DDTrace::VERSION::STRING >= "1.0.0"
|
||||
METADATA_MODULE = TRACING_MODULE::Metadata
|
||||
METADATA_MODULE = Datadog::Tracing::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_METHOD = TRACING_MODULE::Metadata::Ext::HTTP::TAG_METHOD
|
||||
TAG_TARGET_HOST = TRACING_MODULE::Metadata::Ext::NET::TAG_TARGET_HOST
|
||||
TAG_TARGET_PORT = TRACING_MODULE::Metadata::Ext::NET::TAG_TARGET_PORT
|
||||
TAG_URL = Datadog::Tracing::Metadata::Ext::HTTP::TAG_URL
|
||||
TAG_METHOD = Datadog::Tracing::Metadata::Ext::HTTP::TAG_METHOD
|
||||
TAG_TARGET_HOST = Datadog::Tracing::Metadata::Ext::NET::TAG_TARGET_HOST
|
||||
TAG_TARGET_PORT = Datadog::Tracing::Metadata::Ext::NET::TAG_TARGET_PORT
|
||||
|
||||
TAG_STATUS_CODE = TRACING_MODULE::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
|
||||
TAG_STATUS_CODE = Datadog::Tracing::Metadata::Ext::HTTP::TAG_STATUS_CODE
|
||||
|
||||
# HTTPX Datadog Plugin
|
||||
#
|
||||
@ -64,14 +37,18 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
|
||||
end
|
||||
|
||||
def call
|
||||
return unless tracing_enabled?
|
||||
return unless Datadog::Tracing.enabled?
|
||||
|
||||
@request.on(:response, &method(:finish))
|
||||
|
||||
verb = @request.verb
|
||||
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
|
||||
|
||||
@ -86,7 +63,8 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
|
||||
# Tag as an external peer 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
|
||||
if Contrib::Analytics.enabled?(@configuration[:analytics_enabled])
|
||||
@ -113,48 +91,8 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
|
||||
|
||||
private
|
||||
|
||||
if defined?(::DDTrace) && ::DDTrace::VERSION::STRING >= "1.0.0"
|
||||
|
||||
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
|
||||
def configuration
|
||||
@configuration ||= Datadog.configuration.tracing[:httpx, @request.uri.host]
|
||||
end
|
||||
end
|
||||
|
||||
@ -179,7 +117,7 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
|
||||
module Configuration
|
||||
# Default settings for httpx
|
||||
#
|
||||
class Settings < TRACING_MODULE::Contrib::Configuration::Settings
|
||||
class Settings < Datadog::Tracing::Contrib::Configuration::Settings
|
||||
DEFAULT_ERROR_HANDLER = lambda do |response|
|
||||
Datadog::Ext::HTTP::ERROR_RANGE.cover?(response.status)
|
||||
end
|
||||
@ -203,10 +141,10 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
|
||||
o.lazy
|
||||
end
|
||||
|
||||
if defined?(TRACING_MODULE::Contrib::SpanAttributeSchema)
|
||||
if defined?(Datadog::Tracing::Contrib::SpanAttributeSchema)
|
||||
option :service_name do |o|
|
||||
o.default do
|
||||
TRACING_MODULE::Contrib::SpanAttributeSchema.fetch_service_name(
|
||||
Datadog::Tracing::Contrib::SpanAttributeSchema.fetch_service_name(
|
||||
"DD_TRACE_HTTPX_SERVICE_NAME",
|
||||
"httpx"
|
||||
)
|
||||
@ -231,7 +169,7 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
|
||||
# Patcher enables patching of 'httpx' with datadog components.
|
||||
#
|
||||
module Patcher
|
||||
include TRACING_MODULE::Contrib::Patcher
|
||||
include Datadog::Tracing::Contrib::Patcher
|
||||
|
||||
module_function
|
||||
|
||||
@ -254,7 +192,6 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
|
||||
class Integration
|
||||
include Contrib::Integration
|
||||
|
||||
# MINIMUM_VERSION = Gem::Version.new('0.11.0')
|
||||
MINIMUM_VERSION = Gem::Version.new("0.10.2")
|
||||
|
||||
register_as :httpx
|
||||
@ -271,14 +208,8 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
|
||||
super && version >= MINIMUM_VERSION
|
||||
end
|
||||
|
||||
if defined?(::DDTrace) && ::DDTrace::VERSION::STRING >= "1.0.0"
|
||||
def new_configuration
|
||||
Configuration::Settings.new
|
||||
end
|
||||
else
|
||||
def default_configuration
|
||||
Configuration::Settings.new
|
||||
end
|
||||
def new_configuration
|
||||
Configuration::Settings.new
|
||||
end
|
||||
|
||||
def patcher
|
||||
|
@ -7,38 +7,11 @@ require "faraday"
|
||||
module Faraday
|
||||
class 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
|
||||
using ::HTTPX::HashExtensions
|
||||
|
||||
def build_connection(env)
|
||||
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_opts = options_from_env(env)
|
||||
|
||||
@ -70,7 +43,7 @@ module Faraday
|
||||
def connect(env, &blk)
|
||||
connection(env, &blk)
|
||||
rescue ::HTTPX::TLSError => e
|
||||
raise SSL_ERROR, e
|
||||
raise Faraday::SSLError, e
|
||||
rescue Errno::ECONNABORTED,
|
||||
Errno::ECONNREFUSED,
|
||||
Errno::ECONNRESET,
|
||||
@ -79,9 +52,7 @@ module Faraday
|
||||
Errno::ENETUNREACH,
|
||||
Errno::EPIPE,
|
||||
::HTTPX::ConnectionError => e
|
||||
raise CONNECTION_FAILED_ERROR, e
|
||||
rescue ::HTTPX::TimeoutError => e
|
||||
raise Faraday::TimeoutError, e
|
||||
raise Faraday::ConnectionFailed, e
|
||||
end
|
||||
|
||||
def build_request(env)
|
||||
@ -159,24 +130,13 @@ module Faraday
|
||||
end
|
||||
|
||||
module ReasonPlugin
|
||||
if RUBY_VERSION < "2.5"
|
||||
def self.load_dependencies(*)
|
||||
require "webrick"
|
||||
end
|
||||
else
|
||||
def self.load_dependencies(*)
|
||||
require "net/http/status"
|
||||
end
|
||||
def self.load_dependencies(*)
|
||||
require "net/http/status"
|
||||
end
|
||||
|
||||
module ResponseMethods
|
||||
if RUBY_VERSION < "2.5"
|
||||
def reason
|
||||
WEBrick::HTTPStatus::StatusMessage.fetch(@status)
|
||||
end
|
||||
else
|
||||
def reason
|
||||
Net::HTTP::STATUS_CODES.fetch(@status)
|
||||
end
|
||||
def reason
|
||||
Net::HTTP::STATUS_CODES.fetch(@status)
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -261,10 +221,7 @@ module Faraday
|
||||
|
||||
# from Faraday::Adapter#request_timeout
|
||||
def request_timeout(type, options)
|
||||
key = Faraday::Adapter::TIMEOUT_KEYS.fetch(type) do
|
||||
msg = "Expected :read, :write, :open. Got #{type.inspect} :("
|
||||
raise ArgumentError, msg
|
||||
end
|
||||
key = Faraday::Adapter::TIMEOUT_KEYS[type]
|
||||
options[key] || options[:timeout]
|
||||
end
|
||||
end
|
||||
|
@ -2,13 +2,8 @@
|
||||
|
||||
module WebMock
|
||||
module HttpLibAdapters
|
||||
if RUBY_VERSION < "2.5"
|
||||
require "webrick/httpstatus"
|
||||
HTTP_REASONS = WEBrick::HTTPStatus::StatusMessage
|
||||
else
|
||||
require "net/http/status"
|
||||
HTTP_REASONS = Net::HTTP::STATUS_CODES
|
||||
end
|
||||
require "net/http/status"
|
||||
HTTP_REASONS = Net::HTTP::STATUS_CODES
|
||||
|
||||
#
|
||||
# HTTPX plugin for webmock.
|
||||
|
@ -98,29 +98,11 @@ module HTTPX
|
||||
end
|
||||
end
|
||||
|
||||
# :nocov:
|
||||
if RUBY_VERSION < "2.2"
|
||||
def parse_altsvc_origin(alt_proto, alt_origin)
|
||||
alt_scheme = parse_altsvc_scheme(alt_proto) or return
|
||||
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?("\"")
|
||||
|
||||
alt_origin = alt_origin[1..-2] if alt_origin.start_with?("\"") && alt_origin.end_with?("\"")
|
||||
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
|
||||
URI.parse("#{alt_scheme}://#{alt_origin}")
|
||||
end
|
||||
# :nocov:
|
||||
end
|
||||
end
|
||||
|
@ -27,18 +27,6 @@ module HTTPX
|
||||
branch(default_options).request(*args, **options)
|
||||
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)
|
||||
with(headers: { "accept" => String(type) })
|
||||
end
|
||||
@ -54,17 +42,6 @@ module HTTPX
|
||||
klass.plugin(pl, options, &blk).new
|
||||
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)
|
||||
branch(default_options.merge(options), &blk)
|
||||
end
|
||||
|
@ -33,7 +33,6 @@ module HTTPX
|
||||
include Callbacks
|
||||
|
||||
using URIExtensions
|
||||
using NumericExtensions
|
||||
|
||||
require "httpx/connection/http2"
|
||||
require "httpx/connection/http1"
|
||||
@ -70,7 +69,6 @@ module HTTPX
|
||||
|
||||
@inflight = 0
|
||||
@keep_alive_timeout = @options.timeout[:keep_alive_timeout]
|
||||
@total_timeout = @options.timeout[:total_timeout]
|
||||
|
||||
self.addresses = @options.addresses if @options.addresses
|
||||
end
|
||||
@ -270,21 +268,6 @@ module HTTPX
|
||||
end
|
||||
|
||||
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 @options.timeout[:connect_timeout] if @state == :idle
|
||||
@ -604,6 +587,9 @@ module HTTPX
|
||||
purge_after_closed
|
||||
when :already_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
|
||||
when :active
|
||||
return unless @state == :inactive
|
||||
@ -643,23 +629,16 @@ module HTTPX
|
||||
def on_error(error)
|
||||
if error.instance_of?(TimeoutError)
|
||||
|
||||
if @total_timeout && @connected_at &&
|
||||
Utils.elapsed_time(@connected_at) > @total_timeout
|
||||
ex = TotalTimeoutError.new(@total_timeout, "Timed out after #{@total_timeout} seconds")
|
||||
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
|
||||
# inactive connections do not contribute to the select loop, therefore
|
||||
# they should not fail due to such errors.
|
||||
return if @state == :inactive
|
||||
|
||||
if @timeout
|
||||
@timeout -= error.timeout
|
||||
return unless @timeout <= 0
|
||||
end
|
||||
|
||||
error = error.to_connection_error if connecting?
|
||||
if @timeout
|
||||
@timeout -= error.timeout
|
||||
return unless @timeout <= 0
|
||||
end
|
||||
|
||||
error = error.to_connection_error if connecting?
|
||||
end
|
||||
handle_error(error)
|
||||
reset
|
||||
|
@ -51,8 +51,6 @@ module HTTPX
|
||||
# non-canonical domain.
|
||||
attr_reader :domain
|
||||
|
||||
DOT = "." # :nodoc:
|
||||
|
||||
class << self
|
||||
def new(domain)
|
||||
return domain if domain.is_a?(self)
|
||||
@ -63,7 +61,7 @@ module HTTPX
|
||||
# Normalizes a _domain_ using the Punycode algorithm as necessary.
|
||||
# The result will be a downcased, ASCII-only string.
|
||||
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
|
||||
end
|
||||
end
|
||||
@ -73,7 +71,7 @@ module HTTPX
|
||||
def initialize(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
|
||||
@ipaddr = IPAddr.new(hostname)
|
||||
@ -84,7 +82,7 @@ module HTTPX
|
||||
end
|
||||
|
||||
@hostname = DomainName.normalize(hostname)
|
||||
tld = if (last_dot = @hostname.rindex(DOT))
|
||||
tld = if (last_dot = @hostname.rindex("."))
|
||||
@hostname[(last_dot + 1)..-1]
|
||||
else
|
||||
@hostname
|
||||
@ -94,7 +92,7 @@ module HTTPX
|
||||
@domain = if last_dot
|
||||
# fallback - accept cookies down to second level
|
||||
# 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]
|
||||
else
|
||||
@hostname
|
||||
@ -126,17 +124,12 @@ module HTTPX
|
||||
@domain && self <= domain && domain <= @domain
|
||||
end
|
||||
|
||||
# def ==(other)
|
||||
# other = DomainName.new(other)
|
||||
# other.hostname == @hostname
|
||||
# end
|
||||
|
||||
def <=>(other)
|
||||
other = DomainName.new(other)
|
||||
othername = other.hostname
|
||||
if othername == @hostname
|
||||
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
|
||||
-1
|
||||
else
|
||||
|
@ -22,8 +22,6 @@ module HTTPX
|
||||
end
|
||||
end
|
||||
|
||||
class TotalTimeoutError < TimeoutError; end
|
||||
|
||||
class ConnectTimeoutError < TimeoutError; end
|
||||
|
||||
class RequestTimeoutError < TimeoutError
|
||||
|
@ -3,96 +3,6 @@
|
||||
require "uri"
|
||||
|
||||
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 FilterMap
|
||||
refine Array do
|
||||
@ -108,16 +18,6 @@ module HTTPX
|
||||
end unless Array.method_defined?(:filter_map)
|
||||
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
|
||||
refine Array do
|
||||
# Ruby 3.1 backport
|
||||
@ -133,30 +33,6 @@ module HTTPX
|
||||
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
|
||||
# uri 0.11 backport, ships with ruby 3.1
|
||||
refine URI::Generic do
|
||||
|
@ -6,15 +6,11 @@ module HTTPX
|
||||
TLSError = OpenSSL::SSL::SSLError
|
||||
|
||||
class SSL < TCP
|
||||
using RegexpExtensions unless Regexp.method_defined?(:match?)
|
||||
|
||||
TLS_OPTIONS = if OpenSSL::SSL::SSLContext.instance_methods.include?(:alpn_protocols)
|
||||
{ alpn_protocols: %w[h2 http/1.1].freeze }
|
||||
else
|
||||
{}
|
||||
end
|
||||
# rubocop:disable Style/MutableConstant
|
||||
TLS_OPTIONS = { alpn_protocols: %w[h2 http/1.1].freeze }
|
||||
# https://github.com/jruby/jruby-openssl/issues/284
|
||||
TLS_OPTIONS[:verify_hostname] = true if RUBY_ENGINE == "jruby"
|
||||
# rubocop:enable Style/MutableConstant
|
||||
TLS_OPTIONS.freeze
|
||||
|
||||
attr_writer :ssl_session
|
||||
@ -103,61 +99,18 @@ module HTTPX
|
||||
try_ssl_connect
|
||||
end
|
||||
|
||||
if RUBY_VERSION < "2.3"
|
||||
# :nocov:
|
||||
def try_ssl_connect
|
||||
@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
|
||||
def try_ssl_connect
|
||||
case @io.connect_nonblock(exception: false)
|
||||
when :wait_readable
|
||||
@interests = :r
|
||||
rescue ::IO::WaitWritable
|
||||
return
|
||||
when :wait_writable
|
||||
@interests = :w
|
||||
return
|
||||
end
|
||||
|
||||
def read(_, buffer)
|
||||
super
|
||||
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:
|
||||
@io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE && @verify_hostname
|
||||
transition(:negotiated)
|
||||
@interests = :w
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -94,84 +94,43 @@ module HTTPX
|
||||
retry
|
||||
end
|
||||
|
||||
if RUBY_VERSION < "2.3"
|
||||
# :nocov:
|
||||
def try_connect
|
||||
@io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s))
|
||||
rescue ::IO::WaitWritable, Errno::EALREADY
|
||||
@interests = :w
|
||||
rescue ::IO::WaitReadable
|
||||
def try_connect
|
||||
case @io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s), exception: false)
|
||||
when :wait_readable
|
||||
@interests = :r
|
||||
rescue Errno::EISCONN
|
||||
transition(:connected)
|
||||
@interests = :w
|
||||
else
|
||||
transition(:connected)
|
||||
return
|
||||
when :wait_writable
|
||||
@interests = :w
|
||||
return
|
||||
end
|
||||
private :try_connect
|
||||
transition(:connected)
|
||||
@interests = :w
|
||||
rescue Errno::EALREADY
|
||||
@interests = :w
|
||||
end
|
||||
private :try_connect
|
||||
|
||||
def read(size, buffer)
|
||||
@io.read_nonblock(size, buffer)
|
||||
log { "READ: #{buffer.bytesize} bytes..." }
|
||||
buffer.bytesize
|
||||
rescue ::IO::WaitReadable
|
||||
def read(size, buffer)
|
||||
ret = @io.read_nonblock(size, buffer, exception: false)
|
||||
if ret == :wait_readable
|
||||
buffer.clear
|
||||
0
|
||||
rescue EOFError
|
||||
nil
|
||||
return 0
|
||||
end
|
||||
return if ret.nil?
|
||||
|
||||
def write(buffer)
|
||||
siz = @io.write_nonblock(buffer)
|
||||
log { "WRITE: #{siz} bytes..." }
|
||||
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
|
||||
log { "READ: #{buffer.bytesize} bytes..." }
|
||||
buffer.bytesize
|
||||
end
|
||||
|
||||
def read(size, buffer)
|
||||
ret = @io.read_nonblock(size, buffer, exception: false)
|
||||
if ret == :wait_readable
|
||||
buffer.clear
|
||||
return 0
|
||||
end
|
||||
return if ret.nil?
|
||||
def write(buffer)
|
||||
siz = @io.write_nonblock(buffer, exception: false)
|
||||
return 0 if siz == :wait_writable
|
||||
return if siz.nil?
|
||||
|
||||
log { "READ: #{buffer.bytesize} bytes..." }
|
||||
buffer.bytesize
|
||||
end
|
||||
log { "WRITE: #{siz} bytes..." }
|
||||
|
||||
def write(buffer)
|
||||
siz = @io.write_nonblock(buffer, exception: false)
|
||||
return 0 if siz == :wait_writable
|
||||
return if siz.nil?
|
||||
|
||||
log { "WRITE: #{siz} bytes..." }
|
||||
|
||||
buffer.shift!(siz)
|
||||
siz
|
||||
end
|
||||
buffer.shift!(siz)
|
||||
siz
|
||||
end
|
||||
|
||||
def close
|
||||
|
@ -23,45 +23,19 @@ module HTTPX
|
||||
true
|
||||
end
|
||||
|
||||
if RUBY_VERSION < "2.3"
|
||||
# :nocov:
|
||||
def close
|
||||
@io.close
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
# :nocov:
|
||||
else
|
||||
def close
|
||||
@io.close
|
||||
end
|
||||
def close
|
||||
@io.close
|
||||
end
|
||||
|
||||
# :nocov:
|
||||
if (RUBY_ENGINE == "truffleruby" && RUBY_ENGINE_VERSION < "21.1.0") ||
|
||||
RUBY_VERSION < "2.3"
|
||||
if RUBY_ENGINE == "jruby"
|
||||
# In JRuby, sendmsg_nonblock is not implemented
|
||||
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..." }
|
||||
buffer.shift!(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
|
||||
else
|
||||
|
||||
def write(buffer)
|
||||
siz = @io.sendmsg_nonblock(buffer.to_s, 0, Socket.sockaddr_in(@port, @host.to_s), exception: false)
|
||||
return 0 if siz == :wait_writable
|
||||
@ -72,26 +46,17 @@ module HTTPX
|
||||
buffer.shift!(siz)
|
||||
siz
|
||||
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
|
||||
|
||||
# In JRuby, sendmsg_nonblock is not implemented
|
||||
def write(buffer)
|
||||
siz = @io.send(buffer.to_s, 0, @host, @port)
|
||||
log { "WRITE: #{siz} bytes..." }
|
||||
buffer.shift!(siz)
|
||||
siz
|
||||
end if RUBY_ENGINE == "jruby"
|
||||
# :nocov:
|
||||
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
|
||||
|
@ -27,14 +27,7 @@ module HTTPX
|
||||
@keep_open = true
|
||||
@state = :connected
|
||||
else
|
||||
if @options.transport_options
|
||||
# :nocov:
|
||||
warn ":transport_options is deprecated, use :addresses instead"
|
||||
@path = @options.transport_options[:path]
|
||||
# :nocov:
|
||||
else
|
||||
@path = addresses.first
|
||||
end
|
||||
@path = addresses.first
|
||||
end
|
||||
@io ||= build_socket
|
||||
end
|
||||
|
@ -24,26 +24,11 @@ module HTTPX
|
||||
debug_stream << message
|
||||
end
|
||||
|
||||
if Exception.instance_methods.include?(:full_message)
|
||||
|
||||
def log_exception(ex, level: @options.debug_level, color: nil)
|
||||
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
|
||||
def log_exception(ex, level: @options.debug_level, color: nil)
|
||||
return unless @options.debug
|
||||
return unless @options.debug_level >= level
|
||||
|
||||
log(level: level, color: color) { ex.full_message }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -7,11 +7,10 @@ module HTTPX
|
||||
BUFFER_SIZE = 1 << 14
|
||||
WINDOW_SIZE = 1 << 14 # 16K
|
||||
MAX_BODY_THRESHOLD_SIZE = (1 << 10) * 112 # 112K
|
||||
CONNECT_TIMEOUT = 60
|
||||
OPERATION_TIMEOUT = 60
|
||||
KEEP_ALIVE_TIMEOUT = 20
|
||||
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
|
||||
ip_address_families = begin
|
||||
@ -31,6 +30,9 @@ module HTTPX
|
||||
:ssl => {},
|
||||
:http2_settings => { settings_enable_push: 0 },
|
||||
:fallback_protocol => "http/1.1",
|
||||
:supported_compression_formats => %w[gzip deflate],
|
||||
:decompress_response_body => true,
|
||||
:compress_request_body => true,
|
||||
:timeout => {
|
||||
connect_timeout: CONNECT_TIMEOUT,
|
||||
settings_timeout: SETTINGS_TIMEOUT,
|
||||
@ -52,7 +54,6 @@ module HTTPX
|
||||
:connection_class => Class.new(Connection),
|
||||
:options_class => Class.new(self),
|
||||
:transport => nil,
|
||||
:transport_options => nil,
|
||||
:addresses => nil,
|
||||
:persistent => false,
|
||||
:resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym,
|
||||
@ -60,28 +61,6 @@ module HTTPX
|
||||
:ip_families => ip_address_families,
|
||||
}.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
|
||||
def new(options = {})
|
||||
# let enhanced options go through
|
||||
@ -100,38 +79,10 @@ module HTTPX
|
||||
|
||||
attr_reader(optname)
|
||||
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
|
||||
|
||||
def initialize(options = {})
|
||||
__initialize__(options)
|
||||
do_initialize(options)
|
||||
freeze
|
||||
end
|
||||
|
||||
@ -142,6 +93,7 @@ module HTTPX
|
||||
@timeout.freeze
|
||||
@headers.freeze
|
||||
@addresses.freeze
|
||||
@supported_compression_formats.freeze
|
||||
end
|
||||
|
||||
def option_origin(value)
|
||||
@ -157,14 +109,11 @@ module HTTPX
|
||||
end
|
||||
|
||||
def option_timeout(value)
|
||||
timeouts = Hash[value]
|
||||
Hash[value]
|
||||
end
|
||||
|
||||
if timeouts.key?(:loop_timeout)
|
||||
warn ":loop_timeout is deprecated, use :operation_timeout instead"
|
||||
timeouts[:operation_timeout] = timeouts.delete(:loop_timeout)
|
||||
end
|
||||
|
||||
timeouts
|
||||
def option_supported_compression_formats(value)
|
||||
Array(value).map(&:to_s)
|
||||
end
|
||||
|
||||
def option_max_concurrent_requests(value)
|
||||
@ -196,7 +145,10 @@ module HTTPX
|
||||
end
|
||||
|
||||
def option_body_threshold_size(value)
|
||||
Integer(value)
|
||||
bytes = Integer(value)
|
||||
raise TypeError, ":body_threshold_size must be positive" unless bytes.positive?
|
||||
|
||||
bytes
|
||||
end
|
||||
|
||||
def option_transport(value)
|
||||
@ -218,10 +170,13 @@ module HTTPX
|
||||
params form json xml body ssl http2_settings
|
||||
request_class response_class headers_class request_body_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
|
||||
].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
|
||||
|
||||
REQUEST_IVARS = %i[@params @form @xml @json @body].freeze
|
||||
@ -270,30 +225,15 @@ module HTTPX
|
||||
end
|
||||
end
|
||||
|
||||
if RUBY_VERSION > "2.4.0"
|
||||
def initialize_dup(other)
|
||||
instance_variables.each do |ivar|
|
||||
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
|
||||
def initialize_dup(other)
|
||||
instance_variables.each do |ivar|
|
||||
instance_variable_set(ivar, other.instance_variable_get(ivar).dup)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def __initialize__(options = {})
|
||||
def do_initialize(options = {})
|
||||
defaults = DEFAULT_OPTIONS.merge(options)
|
||||
defaults.each do |k, v|
|
||||
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
|
||||
end
|
||||
|
||||
def can_authenticate?(authenticate)
|
||||
authenticate && /Basic .*/.match?(authenticate)
|
||||
end
|
||||
|
||||
def authenticate(*)
|
||||
"Basic #{Base64.strict_encode64("#{@user}:#{@password}")}"
|
||||
end
|
@ -8,8 +8,6 @@ module HTTPX
|
||||
module Plugins
|
||||
module Authentication
|
||||
class Digest
|
||||
using RegexpExtensions unless Regexp.method_defined?(:match?)
|
||||
|
||||
def initialize(user, password, hashed: false, **)
|
||||
@user = user
|
||||
@password = password
|
||||
@ -31,9 +29,8 @@ module HTTPX
|
||||
# discard first token, it's Digest
|
||||
auth_info = authenticate[/^(\w+) (.*)/, 2]
|
||||
|
||||
params = Hash[auth_info.split(/ *, */)
|
||||
.map { |val| val.split("=") }
|
||||
.map { |k, v| [k, v.delete("\"")] }]
|
||||
params = auth_info.split(/ *, */)
|
||||
.to_h { |val| val.split("=") }.transform_values { |v| v.delete("\"") }
|
||||
nonce = params["nonce"]
|
||||
nc = next_nonce
|
||||
|
@ -7,8 +7,6 @@ module HTTPX
|
||||
module Plugins
|
||||
module Authentication
|
||||
class Ntlm
|
||||
using RegexpExtensions unless Regexp.method_defined?(:match?)
|
||||
|
||||
def initialize(user, password, domain: nil)
|
||||
@user = user
|
||||
@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)
|
||||
klass.plugin(:expect)
|
||||
klass.plugin(:compression)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -5,26 +5,25 @@ module HTTPX
|
||||
#
|
||||
# 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
|
||||
class << self
|
||||
def load_dependencies(_klass)
|
||||
require_relative "authentication/basic"
|
||||
require_relative "auth/basic"
|
||||
end
|
||||
|
||||
def configure(klass)
|
||||
klass.plugin(:authentication)
|
||||
klass.plugin(:auth)
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
def basic_auth(user, password)
|
||||
authentication(Authentication::Basic.new(user, password).authenticate)
|
||||
authorization(Authentication::Basic.new(user, password).authenticate)
|
||||
end
|
||||
alias_method :basic_authentication, :basic_auth
|
||||
end
|
||||
end
|
||||
register_plugin :basic_authentication, BasicAuth
|
||||
register_plugin :basic_auth, BasicAuth
|
||||
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
|
||||
|
||||
module OptionsMethods
|
||||
def __initialize__(*)
|
||||
def do_initialize(*)
|
||||
super
|
||||
|
||||
return unless @headers.key?("cookie")
|
||||
|
@ -6,8 +6,6 @@ require "time"
|
||||
module HTTPX
|
||||
module Plugins::Cookies
|
||||
module SetCookieParser
|
||||
using(RegexpExtensions) unless Regexp.method_defined?(:match?)
|
||||
|
||||
# Whitespace.
|
||||
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)
|
||||
#
|
||||
# https://gitlab.com/os85/httpx/wikis/Authentication#authentication
|
||||
# https://gitlab.com/os85/httpx/wikis/Authorization#digest-auth
|
||||
#
|
||||
module DigestAuth
|
||||
DigestError = Class.new(Error)
|
||||
@ -16,7 +16,7 @@ module HTTPX
|
||||
end
|
||||
|
||||
def load_dependencies(*)
|
||||
require_relative "authentication/digest"
|
||||
require_relative "auth/digest"
|
||||
end
|
||||
end
|
||||
|
||||
@ -29,11 +29,11 @@ module HTTPX
|
||||
end
|
||||
|
||||
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))
|
||||
end
|
||||
|
||||
alias_method :digest_auth, :digest_authentication
|
||||
private
|
||||
|
||||
def send_requests(*requests)
|
||||
requests.flat_map do |request|
|
||||
@ -57,6 +57,6 @@ module HTTPX
|
||||
end
|
||||
end
|
||||
|
||||
register_plugin :digest_authentication, DigestAuth
|
||||
register_plugin :digest_auth, DigestAuth
|
||||
end
|
||||
end
|
@ -48,7 +48,27 @@ module HTTPX
|
||||
return response unless REDIRECT_STATUS.include?(response.status) && response.headers.key?("location")
|
||||
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
|
||||
|
||||
@ -83,29 +103,6 @@ module HTTPX
|
||||
nil
|
||||
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)
|
||||
location_uri = URI(response.headers["location"])
|
||||
location_uri = response.uri.merge(location_uri) if location_uri.relative?
|
||||
|
@ -49,20 +49,19 @@ module HTTPX
|
||||
class << self
|
||||
def load_dependencies(*)
|
||||
require "stringio"
|
||||
require "httpx/plugins/grpc/grpc_encoding"
|
||||
require "httpx/plugins/grpc/message"
|
||||
require "httpx/plugins/grpc/call"
|
||||
end
|
||||
|
||||
def configure(klass)
|
||||
klass.plugin(:persistent)
|
||||
klass.plugin(:compression)
|
||||
klass.plugin(:stream)
|
||||
end
|
||||
|
||||
def extra_options(options)
|
||||
options.merge(
|
||||
fallback_protocol: "h2",
|
||||
http2_settings: { wait_for_handshake: false },
|
||||
grpc_rpcs: {}.freeze,
|
||||
grpc_compression: false,
|
||||
grpc_deadline: DEADLINE
|
||||
@ -108,9 +107,18 @@ module HTTPX
|
||||
@trailing_metadata = Hash[trailers]
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def encoders
|
||||
@options.encodings
|
||||
module RequestBodyMethods
|
||||
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
|
||||
|
||||
@ -233,7 +241,7 @@ module HTTPX
|
||||
uri.path = rpc_method
|
||||
|
||||
headers = HEADERS.merge(
|
||||
"grpc-accept-encoding" => ["identity", *@options.encodings.keys]
|
||||
"grpc-accept-encoding" => ["identity", *@options.supported_compression_formats]
|
||||
)
|
||||
unless deadline == Float::INFINITY
|
||||
# convert to milliseconds
|
||||
@ -244,27 +252,13 @@ module HTTPX
|
||||
headers = headers.merge(metadata) if metadata
|
||||
|
||||
# prepare compressor
|
||||
deflater = nil
|
||||
compression = @options.grpc_compression == true ? "gzip" : @options.grpc_compression
|
||||
|
||||
if compression
|
||||
headers["grpc-encoding"] = compression
|
||||
deflater = @options.encodings[compression].deflater if @options.encodings.key?(compression)
|
||||
end
|
||||
headers["grpc-encoding"] = compression if compression
|
||||
|
||||
headers.merge!(@options.call_credentials.call) if @options.call_credentials
|
||||
|
||||
body = if input.respond_to?(:each)
|
||||
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)
|
||||
build_request("POST", uri, headers: headers, body: input)
|
||||
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
|
||||
def unary(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
|
||||
|
||||
# lazy decodes a grpc stream response
|
||||
def stream(response, &block)
|
||||
return enum_for(__method__, response) unless block
|
||||
|
||||
decoder = Transcoder::GRPCEncoding.decode(response)
|
||||
|
||||
response.each do |frame|
|
||||
decode(frame, encodings: response.headers.get("grpc-encoding"), encoders: response.encoders, &block)
|
||||
decoder.call(frame, &block)
|
||||
end
|
||||
|
||||
verify_status(response)
|
||||
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)
|
||||
request.emit(:refuse, :client_cancellation)
|
||||
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 Plugins
|
||||
#
|
||||
# https://gitlab.com/os85/httpx/wikis/Authentication#ntlm-authentication
|
||||
# https://gitlab.com/os85/httpx/wikis/Authorization#ntlm-auth
|
||||
#
|
||||
module NTLMAuth
|
||||
class << self
|
||||
def load_dependencies(_klass)
|
||||
require_relative "authentication/ntlm"
|
||||
require_relative "auth/ntlm"
|
||||
end
|
||||
|
||||
def extra_options(options)
|
||||
@ -25,11 +25,11 @@ module HTTPX
|
||||
end
|
||||
|
||||
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))
|
||||
end
|
||||
|
||||
alias_method :ntlm_auth, :ntlm_authentication
|
||||
private
|
||||
|
||||
def send_requests(*requests)
|
||||
requests.flat_map do |request|
|
||||
@ -55,6 +55,6 @@ module HTTPX
|
||||
end
|
||||
end
|
||||
end
|
||||
register_plugin :ntlm_authentication, NTLMAuth
|
||||
register_plugin :ntlm_auth, NTLMAuth
|
||||
end
|
||||
end
|
@ -8,7 +8,7 @@ module HTTPX
|
||||
module OAuth
|
||||
class << self
|
||||
def load_dependencies(_klass)
|
||||
require_relative "authentication/basic"
|
||||
require_relative "auth/basic"
|
||||
end
|
||||
end
|
||||
|
||||
@ -106,7 +106,7 @@ module HTTPX
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
def oauth_authentication(**args)
|
||||
def oauth_auth(**args)
|
||||
with(oauth_session: OAuthSession.new(**args))
|
||||
end
|
||||
|
||||
|
@ -28,35 +28,6 @@ module HTTPX
|
||||
def extra_options(options)
|
||||
options.merge(supported_proxy_protocols: [])
|
||||
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
|
||||
|
||||
class Parameters
|
||||
@ -82,7 +53,7 @@ module HTTPX
|
||||
|
||||
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)
|
||||
end
|
||||
@ -162,8 +133,8 @@ module HTTPX
|
||||
no_proxy = proxy[:no_proxy]
|
||||
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,
|
||||
next_proxy.port, no_proxy)
|
||||
return super(request, connections, options.merge(proxy: nil)) unless URI::Generic.use_proxy?(uri.host, next_proxy.host,
|
||||
next_proxy.port, no_proxy)
|
||||
end
|
||||
|
||||
proxy.merge(uri: next_proxy)
|
||||
|
@ -92,10 +92,6 @@ module HTTPX
|
||||
@options = Options.new(options)
|
||||
end
|
||||
|
||||
def timeout
|
||||
@options.timeout[:operation_timeout]
|
||||
end
|
||||
|
||||
def close; end
|
||||
|
||||
def consume(*); end
|
||||
|
@ -20,7 +20,7 @@ module HTTPX
|
||||
|
||||
class << self
|
||||
def load_dependencies(*)
|
||||
require_relative "../authentication/socks5"
|
||||
require_relative "../auth/socks5"
|
||||
end
|
||||
|
||||
def extra_options(options)
|
||||
@ -144,10 +144,6 @@ module HTTPX
|
||||
@options = Options.new(options)
|
||||
end
|
||||
|
||||
def timeout
|
||||
@options.timeout[:operation_timeout]
|
||||
end
|
||||
|
||||
def close; end
|
||||
|
||||
def consume(*); end
|
||||
|
@ -88,13 +88,12 @@ module HTTPX
|
||||
request.retries.positive? &&
|
||||
__repeatable_request?(request, options) &&
|
||||
(
|
||||
# rubocop:disable Style/MultilineTernaryOperator
|
||||
options.retry_on ?
|
||||
options.retry_on.call(response) :
|
||||
(
|
||||
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)
|
||||
log { "failed to get response, #{request.retries} tries to go..." }
|
||||
|
@ -94,6 +94,10 @@ module HTTPX
|
||||
# https://gitlab.com/honeyryderchuck/httpx/wikis/Stream
|
||||
#
|
||||
module Stream
|
||||
def self.extra_options(options)
|
||||
options.merge(timeout: { read_timeout: Float::INFINITY, operation_timeout: 60 })
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
def request(*args, stream: false, **options)
|
||||
return super(*args, **options) unless stream
|
||||
@ -139,12 +143,6 @@ module HTTPX
|
||||
super
|
||||
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
|
||||
register_plugin :stream, Stream
|
||||
end
|
||||
|
@ -1,304 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTTPX
|
||||
begin
|
||||
require "idnx"
|
||||
module Punycode
|
||||
module_function
|
||||
|
||||
module Punycode
|
||||
module_function
|
||||
begin
|
||||
require "idnx"
|
||||
|
||||
def encode_hostname(hostname)
|
||||
Idnx.to_punycode(hostname)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
rescue LoadError
|
||||
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|
|
||||
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
|
||||
hostname
|
||||
end
|
||||
end
|
||||
# :nocov:
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -19,10 +19,6 @@ module HTTPX
|
||||
def_delegator :@body, :empty?
|
||||
|
||||
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
|
||||
@options = Options.new(options)
|
||||
@uri = Utils.to_uri(uri)
|
||||
@ -69,16 +65,6 @@ module HTTPX
|
||||
:w
|
||||
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)
|
||||
@headers = @headers.merge(h)
|
||||
end
|
||||
@ -153,100 +139,6 @@ module HTTPX
|
||||
end
|
||||
# :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)
|
||||
case nextstate
|
||||
when :idle
|
||||
@ -284,16 +176,7 @@ module HTTPX
|
||||
def expects?
|
||||
@headers["expect"] == "100-continue" && @informational_status == 100 && !@response
|
||||
end
|
||||
|
||||
class ProcIO
|
||||
def initialize(block)
|
||||
@block = block
|
||||
end
|
||||
|
||||
def write(data)
|
||||
@block.call(data.dup)
|
||||
data.bytesize
|
||||
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
|
||||
extend Forwardable
|
||||
using URIExtensions
|
||||
using StringExtensions
|
||||
|
||||
module DNSExtensions
|
||||
refine Resolv::DNS do
|
||||
|
@ -8,20 +8,12 @@ module HTTPX
|
||||
extend Forwardable
|
||||
using URIExtensions
|
||||
|
||||
DEFAULTS = if RUBY_VERSION < "2.2"
|
||||
{
|
||||
**Resolv::DNS::Config.default_config_hash,
|
||||
packet_size: 512,
|
||||
timeouts: Resolver::RESOLVE_TIMEOUT,
|
||||
}
|
||||
else
|
||||
{
|
||||
nameserver: nil,
|
||||
**Resolv::DNS::Config.default_config_hash,
|
||||
packet_size: 512,
|
||||
timeouts: Resolver::RESOLVE_TIMEOUT,
|
||||
}
|
||||
end.freeze
|
||||
DEFAULTS = {
|
||||
nameserver: nil,
|
||||
**Resolv::DNS::Config.default_config_hash,
|
||||
packet_size: 512,
|
||||
timeouts: Resolver::RESOLVE_TIMEOUT,
|
||||
}.freeze
|
||||
|
||||
DNS_PORT = 53
|
||||
|
||||
|
@ -95,7 +95,7 @@ module HTTPX
|
||||
end
|
||||
|
||||
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}"
|
||||
error = ResolveError.new(message)
|
||||
|
@ -92,6 +92,12 @@ module HTTPX
|
||||
resolve
|
||||
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
|
||||
|
||||
def transition(nextstate)
|
||||
@ -164,6 +170,7 @@ module HTTPX
|
||||
Thread.current.report_on_exception = false
|
||||
begin
|
||||
addrs = if resolve_timeout
|
||||
|
||||
Timeout.timeout(resolve_timeout) do
|
||||
__addrinfo_resolve(hostname, scheme)
|
||||
end
|
||||
@ -182,16 +189,11 @@ module HTTPX
|
||||
@pipe_write.putc(DONE) unless @pipe_write.closed?
|
||||
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
|
||||
if e.is_a?(Timeout::Error)
|
||||
e = ResolveTimeoutError.new(resolve_timeout, e.message)
|
||||
e.set_backtrace(e.backtrace)
|
||||
end
|
||||
@pipe_mutex.synchronize do
|
||||
families.each do |family|
|
||||
@ips.unshift([family, connection, e])
|
||||
|
@ -124,199 +124,6 @@ module HTTPX
|
||||
content_length == "0"
|
||||
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
|
||||
|
||||
class ContentType
|
||||
@ -358,20 +165,8 @@ module HTTPX
|
||||
log_exception(@error)
|
||||
end
|
||||
|
||||
def status
|
||||
warn ":#{__method__} is deprecated, use :error.message instead"
|
||||
@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
|
||||
def to_s
|
||||
@error.full_message(highlight: false)
|
||||
end
|
||||
|
||||
def close
|
||||
@ -388,4 +183,6 @@ module HTTPX
|
||||
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 :WRITABLE
|
||||
|
||||
using HTTPX::IOExtensions
|
||||
|
||||
def initialize
|
||||
@selectables = []
|
||||
end
|
||||
|
@ -339,16 +339,6 @@ module HTTPX
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# :nocov:
|
||||
def plugins(pls)
|
||||
warn ":#{__method__} is deprecated, use :plugin instead"
|
||||
pls.each do |pl|
|
||||
plugin(pl)
|
||||
end
|
||||
self
|
||||
end
|
||||
# :nocov:
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -2,8 +2,6 @@
|
||||
|
||||
module HTTPX
|
||||
module Transcoder
|
||||
using RegexpExtensions unless Regexp.method_defined?(:match?)
|
||||
|
||||
module_function
|
||||
|
||||
def normalize_keys(key, value, cond = nil, &block)
|
||||
@ -90,3 +88,5 @@ require "httpx/transcoder/form"
|
||||
require "httpx/transcoder/json"
|
||||
require "httpx/transcoder/xml"
|
||||
require "httpx/transcoder/chunker"
|
||||
require "httpx/transcoder/deflate"
|
||||
require "httpx/transcoder/gzip"
|
||||
|
@ -9,7 +9,6 @@ module HTTPX::Transcoder
|
||||
module_function
|
||||
|
||||
class Encoder
|
||||
using HTTPX::ArrayExtensions::Sum
|
||||
extend Forwardable
|
||||
|
||||
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 "uri"
|
||||
require_relative "multipart"
|
||||
|
||||
module HTTPX::Transcoder
|
||||
module Form
|
||||
module_function
|
||||
module HTTPX
|
||||
module Transcoder
|
||||
module Form
|
||||
module_function
|
||||
|
||||
PARAM_DEPTH_LIMIT = 32
|
||||
PARAM_DEPTH_LIMIT = 32
|
||||
|
||||
class Encoder
|
||||
extend Forwardable
|
||||
class Encoder
|
||||
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)
|
||||
@raw = form.each_with_object("".b) do |(key, val), buf|
|
||||
HTTPX::Transcoder.normalize_keys(key, val) do |k, v|
|
||||
buf << "&" unless buf.empty?
|
||||
buf << URI.encode_www_form_component(k)
|
||||
buf << "=#{URI.encode_www_form_component(v.to_s)}" unless v.nil?
|
||||
def initialize(form)
|
||||
@raw = form.each_with_object("".b) do |(key, val), buf|
|
||||
HTTPX::Transcoder.normalize_keys(key, val) do |k, v|
|
||||
buf << "&" unless buf.empty?
|
||||
buf << URI.encode_www_form_component(k)
|
||||
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
|
||||
|
||||
def content_type
|
||||
"application/x-www-form-urlencoded"
|
||||
def encode(form)
|
||||
if multipart?(form)
|
||||
Multipart::Encoder.new(form)
|
||||
else
|
||||
Encoder.new(form)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Decoder
|
||||
module_function
|
||||
def decode(response)
|
||||
content_type = response.content_type.mime_type
|
||||
|
||||
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)
|
||||
case content_type
|
||||
when "application/x-www-form-urlencoded"
|
||||
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
|
||||
|
||||
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
|
||||
|
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 JSON
|
||||
JSON_REGEX = %r{\bapplication/(?:vnd\.api\+)?json\b}i.freeze
|
||||
|
||||
using HTTPX::RegexpExtensions unless Regexp.method_defined?(:match?)
|
||||
|
||||
module_function
|
||||
|
||||
JSON_REGEX = %r{\bapplication/(?:vnd\.api\+)?json\b}i.freeze
|
||||
|
||||
class Encoder
|
||||
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
|
||||
|
||||
module HTTPX::Plugins
|
||||
module Multipart
|
||||
module HTTPX
|
||||
module Transcoder::Multipart
|
||||
class Encoder
|
||||
attr_reader :bytesize
|
||||
|
||||
@ -43,7 +43,7 @@ module HTTPX::Plugins
|
||||
def to_parts(form)
|
||||
@bytesize = 0
|
||||
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?
|
||||
|
||||
value, content_type, filename = Part.call(v)
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTTPX
|
||||
module Plugins::Multipart
|
||||
module Transcoder::Multipart
|
||||
module MimeTypeDetector
|
||||
module_function
|
||||
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTTPX
|
||||
module Plugins::Multipart
|
||||
module Transcoder::Multipart
|
||||
module Part
|
||||
module_function
|
||||
|
||||
@ -21,7 +21,8 @@ module HTTPX
|
||||
|
||||
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)
|
||||
content_type ||= MimeTypeDetector.call(value, filename) || "application/octet-stream"
|
||||
[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 Xml
|
||||
using HTTPX::RegexpExtensions
|
||||
|
||||
module_function
|
||||
|
||||
MIME_TYPES = %r{\b(application|text)/(.+\+)?xml\b}.freeze
|
||||
|
@ -3,7 +3,6 @@
|
||||
module HTTPX
|
||||
module Utils
|
||||
using URIExtensions
|
||||
using HTTPX::RegexpExtensions unless Regexp.method_defined?(:match?)
|
||||
|
||||
TOKEN = %r{[^\s()<>,;:\\"/\[\]?=]+}.freeze
|
||||
VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/.freeze
|
||||
@ -55,31 +54,22 @@ module HTTPX
|
||||
filename
|
||||
end
|
||||
|
||||
if RUBY_VERSION < "2.3"
|
||||
URIParser = URI::RFC2396_Parser.new
|
||||
|
||||
def to_uri(uri)
|
||||
URI(uri)
|
||||
end
|
||||
def to_uri(uri)
|
||||
return URI(uri) unless uri.is_a?(String) && !uri.ascii_only?
|
||||
|
||||
else
|
||||
uri = URI(URIParser.escape(uri))
|
||||
|
||||
URIParser = URI::RFC2396_Parser.new
|
||||
non_ascii_hostname = URIParser.unescape(uri.host)
|
||||
|
||||
def to_uri(uri)
|
||||
return URI(uri) unless uri.is_a?(String) && !uri.ascii_only?
|
||||
non_ascii_hostname.force_encoding(Encoding::UTF_8)
|
||||
|
||||
uri = URI(URIParser.escape(uri))
|
||||
idna_hostname = Punycode.encode_hostname(non_ascii_hostname)
|
||||
|
||||
non_ascii_hostname = URIParser.unescape(uri.host)
|
||||
|
||||
non_ascii_hostname.force_encoding(Encoding::UTF_8)
|
||||
|
||||
idna_hostname = Punycode.encode_hostname(non_ascii_hostname)
|
||||
|
||||
uri.host = idna_hostname
|
||||
uri.non_ascii_hostname = non_ascii_hostname
|
||||
uri
|
||||
end
|
||||
uri.host = idna_hostname
|
||||
uri.non_ascii_hostname = non_ascii_hostname
|
||||
uri
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -2,22 +2,6 @@
|
||||
|
||||
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_function
|
||||
|
||||
|
@ -9,8 +9,7 @@ class Bug_0_14_1_Test < Minitest::Test
|
||||
def test_multipart_can_have_arbitrary_content_type
|
||||
uri = "https://#{httpbin}/post"
|
||||
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.post(uri, form: {
|
||||
response = HTTPX.post(uri, form: {
|
||||
image: {
|
||||
content_type: "image/png",
|
||||
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
|
||||
uri = "https://#{httpbin}/post"
|
||||
|
||||
response = HTTPX.plugin(:multipart)
|
||||
.post(uri, form: {
|
||||
response = HTTPX.post(uri, form: {
|
||||
image: {
|
||||
filename: "weird-al-jankovic",
|
||||
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
|
||||
| (options) { (Session) -> void } -> void
|
||||
|
||||
def plugin: (:authentication, ?options) -> Plugins::sessionAuthentication
|
||||
| (:basic_authentication, ?options) -> Plugins::sessionBasicAuth
|
||||
| (:digest_authentication, ?options) -> Plugins::sessionDigestAuth
|
||||
| (:ntlm_authentication, ?options) -> Plugins::sessionNTLMAuth
|
||||
def plugin: (:auth, ?options) -> Plugins::sessionAuthorization
|
||||
| (:basic_auth, ?options) -> Plugins::sessionBasicAuth
|
||||
| (:digest_auth, ?options) -> Plugins::sessionDigestAuth
|
||||
| (:ntlm_auth, ?options) -> Plugins::sessionNTLMAuth
|
||||
| (:aws_sdk_authentication, ?options) -> Plugins::sessionAwsSdkAuthentication
|
||||
| (:compression, ?options) -> Session
|
||||
| (:brotli, ?options) -> Session
|
||||
| (:cookies, ?options) -> Plugins::sessionCookies
|
||||
| (:expect, ?options) -> Session
|
||||
| (:follow_redirects, ?options) -> Plugins::sessionFollowRedirects
|
||||
| (:upgrade, ?options) -> Session
|
||||
| (:h2c, ?options) -> Session
|
||||
| (:multipart, ?options) -> Session
|
||||
| (:persistent, ?options) -> Plugins::sessionPersistent
|
||||
| (:proxy, ?options) -> (Plugins::sessionProxy & Plugins::httpProxy)
|
||||
| (:push_promise, ?options) -> Plugins::sessionPushPromise
|
||||
|
@ -36,7 +36,6 @@ module HTTPX
|
||||
@keep_alive_timeout: Numeric?
|
||||
@timeout: Numeric?
|
||||
@current_timeout: Numeric?
|
||||
@total_timeout: Numeric?
|
||||
@io: TCP | SSL | UNIX
|
||||
@parser: HTTP1 | HTTP2 | _Parser
|
||||
@connected_at: Float
|
||||
|
@ -17,9 +17,6 @@ module HTTPX
|
||||
def initialize: (Numeric timeout, String message) -> untyped
|
||||
end
|
||||
|
||||
class TotalTimeoutError < TimeoutError
|
||||
end
|
||||
|
||||
class ConnectTimeoutError < TimeoutError
|
||||
end
|
||||
|
||||
|
@ -11,13 +11,11 @@ module HTTPX
|
||||
SETTINGS_TIMEOUT: Integer
|
||||
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]
|
||||
|
||||
def self.new: (?options) -> instance
|
||||
|
||||
def self.def_option: (Symbol, ?String) -> void
|
||||
| (Symbol) { (*nil) -> untyped } -> void
|
||||
# headers
|
||||
attr_reader uri: URI?
|
||||
|
||||
@ -48,12 +46,18 @@ module HTTPX
|
||||
# transport
|
||||
attr_reader transport: "unix" | nil
|
||||
|
||||
# transport_options
|
||||
attr_reader transport_options: Hash[untyped, untyped]?
|
||||
|
||||
# addresses
|
||||
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
|
||||
attr_reader params: Transcoder::urlencoded_input?
|
||||
|
||||
@ -124,9 +128,9 @@ module HTTPX
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
@password: String
|
||||
|
||||
def can_authenticate?: (String? authenticate) -> boolish
|
||||
|
||||
def authenticate: (*untyped) -> String
|
||||
|
||||
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