Compare commits

...

27 Commits

Author SHA1 Message Date
HoneyryderChuck
1494ba872a Merge branch 'v1' into 'master'
1.0.0

See merge request os85/httpx!270
2023-09-20 17:19:53 +00:00
HoneyryderChuck
685e6e4c7f allow multipart requests to accept tempfile
in fact, anything responding to .path, .eof?, .rewind and .read can be accepted
2023-09-20 17:57:41 +01:00
HoneyryderChuck
085cec0c8e improve coverage and simplified faraday adapter
and some other modules
2023-09-20 17:57:41 +01:00
HoneyryderChuck
288ac05508 fix: proxy plugin broke when processing a 305 use proxy redirect
the proxy plugin contained an enhancement, when used with the follow_redirects plugin, which retries a request over the received proxy. This contained a bug, which was now caught with the added test
2023-09-20 17:57:41 +01:00
HoneyryderChuck
c777aa779e test socks5 no auth methods error path 2023-09-20 17:57:41 +01:00
HoneyryderChuck
d55bfec80c fix: system resolv timeout raise ResolveTimeoutError instead of ResolveError 2023-09-20 17:57:41 +01:00
HoneyryderChuck
e88956a16f improving coverage of tests for proxy module 2023-09-20 17:57:41 +01:00
HoneyryderChuck
aab30279ac allow default errors catch up besides retry on 2023-09-20 17:57:41 +01:00
HoneyryderChuck
2f9247abfb use default HTTP/2 handshake strategy for grpc 2023-09-20 17:57:41 +01:00
HoneyryderChuck
0d58408c58 compression plugins for gzip and deflate supported by default
most of the code was moved to the transcoder layer.

The `compression_threshold_size` option has been removed.

The `:compression/brotli` plugin becomes only ´:brotli`, and depends on
the new transcoding APIs.

options to skip compression and decompression were added.
2023-09-20 17:57:41 +01:00
HoneyryderChuck
3f73d2e3ce multipart supported by default
the plugin was now moved to the transcoder layer, where it is available
from the get-go.
2023-09-20 17:57:41 +01:00
HoneyryderChuck
896914e189 lint change 2023-09-20 17:57:41 +01:00
HoneyryderChuck
4f587c5508 renaming authenticationn modules to just auth
* `: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
2023-09-20 17:57:41 +01:00
HoneyryderChuck
a9cb0a69a2 seting :read_timeout and :write_timeout by default 2023-09-20 17:57:41 +01:00
HoneyryderChuck
6baca35422 support has been removed 2023-09-20 17:57:41 +01:00
HoneyryderChuck
b4c5e75705 drop faraday adapter support for faraday lower than v1 2023-09-20 17:57:41 +01:00
HoneyryderChuck
d859c3a1eb remove support for older (< v1) versions of dddtrace in the datadog plugin 2023-09-20 17:57:41 +01:00
HoneyryderChuck
b7f5a3dfad adding release notes with latest updates 2023-09-20 17:57:41 +01:00
HoneyryderChuck
8cd1aac99c remove deprecated APIs 2023-09-20 17:57:39 +01:00
HoneyryderChuck
f0f6b5f7e2 removed punycode ruby implementation inherited from domain_name
it's IDNA 2003 compliant only, and people can already load idnx
optionally.
2023-09-20 17:57:05 +01:00
HoneyryderChuck
acbc22e79f test against jruby 9.4 2023-09-20 17:57:05 +01:00
HoneyryderChuck
134bef69e0 removed overrides and refinements of methods prior to 2.7 2023-09-20 17:57:05 +01:00
HoneyryderChuck
477c3601fc eliminated blocks testing for ruby < 2.7 2023-09-20 17:57:05 +01:00
HoneyryderChuck
f0dabb9a83 rearranged deps to adapt to the new constraints 2023-09-20 17:57:05 +01:00
HoneyryderChuck
7407adefb9 set min ruby gemspec constraint 2023-09-20 17:57:05 +01:00
HoneyryderChuck
91bfa84c12 removed ruby < 2.7 from CI 2023-09-20 17:57:05 +01:00
HoneyryderChuck
7473af6d9d removed punycode ruby implementation inherited from domain_name
it's IDNA 2003 compliant only, and people can already load idnx
optionally.
2023-09-20 17:57:05 +01:00
168 changed files with 2719 additions and 3411 deletions

View File

@ -43,24 +43,6 @@ test jruby:
script: script:
./spec.sh jruby 9.0.0.0 ./spec.sh jruby 9.0.0.0
allow_failure: true allow_failure: true
test ruby 2/4:
<<: *test_settings
only:
- master
script:
./spec.sh ruby 2.4
test ruby 2/5:
<<: *test_settings
only:
- master
script:
./spec.sh ruby 2.5
test ruby 2/6:
<<: *test_settings
only:
- master
script:
./spec.sh ruby 2.6
test ruby 2/7: test ruby 2/7:
<<: *test_settings <<: *test_settings
script: script:

View File

@ -23,7 +23,6 @@ AllCops:
- 'vendor/**/*' - 'vendor/**/*'
- 'www/**/*' - 'www/**/*'
- 'lib/httpx/extensions.rb' - 'lib/httpx/extensions.rb'
- 'lib/httpx/punycode.rb'
# Do not lint ffi block, for openssl parity # Do not lint ffi block, for openssl parity
- 'test/extensions/response_pattern_match.rb' - 'test/extensions/response_pattern_match.rb'

View File

@ -6,5 +6,4 @@ SimpleCov.start do
add_filter "/integration_tests/" add_filter "/integration_tests/"
add_filter "/regression_tests/" add_filter "/regression_tests/"
add_filter "/lib/httpx/plugins/internal_telemetry.rb" add_filter "/lib/httpx/plugins/internal_telemetry.rb"
add_filter "/lib/httpx/punycode.rb"
end end

View File

@ -56,13 +56,13 @@ HTTPX.delete("https://myapi.com/users/1")
require "httpx" require "httpx"
# Basic Auth # Basic Auth
response = HTTPX.plugin(:basic_authentication).basic_authentication("username", "password").get("https://google.com") response = HTTPX.plugin(:basic_auth).basic_auth("username", "password").get("https://google.com")
# Digest Auth # Digest Auth
response = HTTPX.plugin(:digest_authentication).digest_authentication("username", "password").get("https://google.com") response = HTTPX.plugin(:digest_auth).digest_auth("username", "password").get("https://google.com")
# Bearer Token Auth # Bearer Token Auth
response = HTTPX.plugin(:authentication).authentication("eyrandomtoken").get("https://google.com") response = HTTPX.plugin(:auth).authorization("eyrandomtoken").get("https://google.com")
``` ```
@ -139,9 +139,14 @@ end
```ruby ```ruby
require "httpx" require "httpx"
response = HTTPX.plugin(:compression).get("https://www.google.com") response = HTTPX.get("https://www.google.com")
puts response.headers["content-encoding"] #=> "gzip" puts response.headers["content-encoding"] #=> "gzip"
puts response.to_s #=> uncompressed payload
# uncompressed request payload
HTTPX.post("https://myapi.com/users", body: super_large_text_payload)
# gzip-compressed request payload
HTTPX.post("https://myapi.com/users", headers: { "content-encoding" => %w[gzip] } body: super_large_text_payload)
``` ```
## Proxy ## Proxy

27
Gemfile
View File

@ -8,31 +8,19 @@ gemspec
gem "rake", "~> 13.0" gem "rake", "~> 13.0"
group :test do group :test do
gem "ddtrace"
gem "http-form_data", ">= 2.0.0" gem "http-form_data", ">= 2.0.0"
gem "minitest" gem "minitest"
gem "minitest-proveit" gem "minitest-proveit"
gem "nokogiri" gem "nokogiri"
gem "ruby-ntlm" gem "ruby-ntlm"
gem "sentry-ruby" if RUBY_VERSION >= "2.4.0" gem "sentry-ruby"
gem "spy" gem "spy"
gem "webmock" gem "webmock"
gem "websocket-driver" gem "websocket-driver"
gem "net-ssh", "~> 4.2.0" if RUBY_VERSION < "2.2.0"
gem "ddtrace"
platform :mri do platform :mri do
if RUBY_VERSION < "2.5.0" gem "grpc"
gem "google-protobuf", "< 3.19.2"
elsif RUBY_VERSION < "2.7.0"
gem "google-protobuf", "< 3.22.0"
end
if RUBY_VERSION <= "2.6.0"
gem "grpc", "< 1.49.0"
else
gem "grpc"
end
gem "logging" gem "logging"
gem "marcel", require: false gem "marcel", require: false
gem "mimemagic", require: false gem "mimemagic", require: false
@ -55,13 +43,12 @@ group :test do
end end
platform :jruby do platform :jruby do
gem "jruby-openssl" # , git: "https://github.com/jruby/jruby-openssl.git", branch: "master"
gem "ruby-debug" gem "ruby-debug"
end end
gem "aws-sdk-s3" gem "aws-sdk-s3"
gem "faraday" gem "faraday"
gem "idnx" if RUBY_VERSION >= "2.4.0" gem "idnx"
gem "oga" gem "oga"
if RUBY_VERSION >= "3.0.0" if RUBY_VERSION >= "3.0.0"
@ -72,11 +59,7 @@ group :test do
end end
group :coverage do group :coverage do
if RUBY_VERSION < "2.5" gem "simplecov"
gem "simplecov", "< 0.21.0"
else
gem "simplecov"
end
end end
group :assorted do group :assorted do

View File

@ -189,51 +189,3 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
* lib/httpx/domain_name.rb
This file is derived from the implementation of punycode available at
here:
https://www.verisign.com/en_US/channel-resources/domain-registry-products/idn-sdks/index.xhtml
Copyright (C) 2000-2002 Verisign Inc., All rights reserved.
Redistribution and use in source and binary forms, with or
without modification, are permitted provided that the following
conditions are met:
1) Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2) Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
3) Neither the name of the VeriSign Inc. nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
This software is licensed under the BSD open source license. For more
information visit www.opensource.org.
Authors:
John Colosi (VeriSign)
Srikanth Veeramachaneni (VeriSign)
Nagesh Chigurupati (Verisign)
Praveen Srinivasan(Verisign)

View File

@ -19,7 +19,7 @@ And also:
* Compression (gzip, deflate, brotli) * Compression (gzip, deflate, brotli)
* Streaming Requests * Streaming Requests
* Authentication (Basic Auth, Digest Auth, NTLM) * Auth (Basic Auth, Digest Auth, NTLM)
* Expect 100-continue * Expect 100-continue
* Multipart Requests * Multipart Requests
* Advanced Cookie handling * Advanced Cookie handling
@ -113,7 +113,7 @@ response = HTTPX.plugin(:basic_authentication)
.get("https://www.google.com") .get("https://www.google.com")
# more complex client objects can be cached, and are thread-safe # more complex client objects can be cached, and are thread-safe
http = HTTPX.plugin(:compression).plugin(:expect).with(headers: { "x-pvt-token" => "TOKEN"}) http = HTTPX.plugin(:expect).with(headers: { "x-pvt-token" => "TOKEN"})
http.get("https://example.com") # the above options will apply http.get("https://example.com") # the above options will apply
http.post("https://example2.com", form: {name: "John", age: "22"}) # same, plus the form POST body http.post("https://example2.com", form: {name: "John", age: "22"}) # same, plus the form POST body
``` ```
@ -134,9 +134,9 @@ The test suite runs against [httpbin proxied over nghttp2](https://nghttp2.org/h
## Supported Rubies ## Supported Rubies
All Rubies greater or equal to 2.1, and always latest JRuby and Truffleruby. All Rubies greater or equal to 2.7, and always latest JRuby and Truffleruby.
**Note**: This gem is tested against all latest patch versions, i.e. if you're using 2.2.0 and you experience some issue, please test it against 2.2.10 (latest patch version of 2.2) before creating an issue. **Note**: This gem is tested against all latest patch versions, i.e. if you're using 3.2.0 and you experience some issue, please test it against 3.2.$latest before creating an issue.
## Resources ## Resources
| | | | | |
@ -149,15 +149,6 @@ All Rubies greater or equal to 2.1, and always latest JRuby and Truffleruby.
## Caveats ## Caveats
### ALPN support
ALPN negotiation is required for "auto" HTTP/2 "https" requests. This is available in ruby since version 2.3 .
### Known bugs
* Doesn't work with ruby 2.4.0 for Windows (see [#36](https://gitlab.com/os85/httpx/issues/36)).
* Using `total_timeout` along with the `:persistent` plugin [does not work as you might expect](https://gitlab.com/os85/httpx/-/wikis/Timeouts#total_timeout).
## Versioning Policy ## Versioning Policy
Although 0.x software, `httpx` is considered API-stable and production-ready, i.e. current API or options may be subject to deprecation and emit log warnings, but can only effectively be removed in a major version change. Although 0.x software, `httpx` is considered API-stable and production-ready, i.e. current API or options may be subject to deprecation and emit log warnings, but can only effectively be removed in a major version change.

View 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 })`.

View File

@ -1,7 +1,7 @@
version: '3' version: '3'
services: services:
httpx: httpx:
image: jruby:9.3 image: jruby:9.4
environment: environment:
- JRUBY_OPTS=--debug - JRUBY_OPTS=--debug
entrypoint: entrypoint:

View File

@ -1,8 +0,0 @@
version: '3'
services:
httpx:
image: ruby:2.4
environment:
- HTTPBIN_COALESCING_HOST=another
links:
- "nghttp2:another"

View File

@ -1,8 +0,0 @@
version: '3'
services:
httpx:
image: ruby:2.5
environment:
- HTTPBIN_COALESCING_HOST=another
links:
- "nghttp2:another"

View File

@ -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"

View File

@ -1,7 +1,7 @@
require "httpx" require "httpx"
require "oga" require "oga"
http = HTTPX.plugin(:compression).plugin(:persistent).with(timeout: { operation_timeut: 5, connect_timeout: 5}) http = HTTPX.plugin(:persistent).with(timeout: { operation_timeut: 5, connect_timeout: 5})
PAGES = (ARGV.first || 10).to_i PAGES = (ARGV.first || 10).to_i
pages = PAGES.times.map do |page| pages = PAGES.times.map do |page|

View File

@ -33,4 +33,6 @@ Gem::Specification.new do |gem|
gem.require_paths = ["lib"] gem.require_paths = ["lib"]
gem.add_runtime_dependency "http-2-next", ">= 0.4.1" gem.add_runtime_dependency "http-2-next", ">= 0.4.1"
gem.required_ruby_version = ">= 2.7.0"
end end

View File

@ -241,59 +241,29 @@ class DatadogTest < Minitest::Test
assert span.get_metric("_dd1.sr.eausr") == sample_rate assert span.get_metric("_dd1.sr.eausr") == sample_rate
end end
if defined?(::DDTrace) && Gem::Version.new(::DDTrace::VERSION::STRING) >= Gem::Version.new("1.0.0") def set_datadog(options = {}, &blk)
Datadog.configure do |c|
def set_datadog(options = {}, &blk) c.tracing.instrument(:httpx, options, &blk)
Datadog.configure do |c|
c.tracing.instrument(:httpx, options, &blk)
end
tracer # initialize tracer patches
end end
def tracer tracer # initialize tracer patches
@tracer ||= begin end
tr = Datadog::Tracing.send(:tracer)
def tr.write(trace) def tracer
@traces ||= [] @tracer ||= begin
@traces << trace tr = Datadog::Tracing.send(:tracer)
end def tr.write(trace)
tr @traces ||= []
@traces << trace
end end
tr
end end
end
def trace_with_sampling_priority(priority) def trace_with_sampling_priority(priority)
tracer.trace("foo.bar") do tracer.trace("foo.bar") do
tracer.active_trace.sampling_priority = priority tracer.active_trace.sampling_priority = priority
yield yield
end
end
else
def set_datadog(options = {}, &blk)
Datadog.configure do |c|
c.use(:httpx, options, &blk)
end
tracer # initialize tracer patches
end
def tracer
@tracer ||= begin
tr = Datadog.tracer
def tr.write(trace)
@spans ||= []
@spans << trace
end
tr
end
end
def trace_with_sampling_priority(priority)
tracer.trace("foo.bar") do |span|
span.context.sampling_priority = priority
yield
end
end end
end end
@ -305,11 +275,7 @@ class DatadogTest < Minitest::Test
# Retrieves and sorts all spans in the current tracer instance. # Retrieves and sorts all spans in the current tracer instance.
# This method does not cache its results. # This method does not cache its results.
def fetch_spans def fetch_spans
spans = if defined?(::DDTrace) && Gem::Version.new(::DDTrace::VERSION::STRING) >= Gem::Version.new("1.0.0") spans = (tracer.instance_variable_get(:@traces) || []).map(&:spans)
(tracer.instance_variable_get(:@traces) || []).map(&:spans)
else
tracer.instance_variable_get(:@spans) || []
end
spans.flatten.sort! do |a, b| spans.flatten.sort! do |a, b|
if a.name == b.name if a.name == b.name
if a.resource == b.resource if a.resource == b.resource

View File

@ -1,150 +1,148 @@
# frozen_string_literal: true # frozen_string_literal: true
if RUBY_VERSION >= "2.4.0" require "logger"
require "logger" require "stringio"
require "stringio" require "sentry-ruby"
require "sentry-ruby" require "test_helper"
require "test_helper" require "support/http_helpers"
require "support/http_helpers" require "httpx/adapters/sentry"
require "httpx/adapters/sentry"
class SentryTest < Minitest::Test class SentryTest < Minitest::Test
include HTTPHelpers include HTTPHelpers
DUMMY_DSN = "http://12345:67890@sentry.localdomain/sentry/42" DUMMY_DSN = "http://12345:67890@sentry.localdomain/sentry/42"
def test_sentry_send_yes_pii def test_sentry_send_yes_pii
before_pii = Sentry.configuration.send_default_pii before_pii = Sentry.configuration.send_default_pii
begin begin
Sentry.configuration.send_default_pii = true Sentry.configuration.send_default_pii = true
transaction = Sentry.start_transaction
Sentry.get_current_scope.set_span(transaction)
uri = build_uri("/get")
response = HTTPX.get(uri, params: { "foo" => "bar" })
verify_status(response, 200)
verify_spans(transaction, response, description: "GET #{uri}?foo=bar")
crumb = Sentry.get_current_scope.breadcrumbs.peek
assert crumb.category == "httpx"
assert crumb.data == { status: 200, method: "GET", url: "#{uri}?foo=bar" }
ensure
Sentry.configuration.send_default_pii = before_pii
end
end
def test_sentry_send_no_pii
before_pii = Sentry.configuration.send_default_pii
begin
Sentry.configuration.send_default_pii = false
transaction = Sentry.start_transaction
Sentry.get_current_scope.set_span(transaction)
uri = build_uri("/get")
response = HTTPX.get(uri, params: { "foo" => "bar" })
verify_status(response, 200)
verify_spans(transaction, response, description: "GET #{uri}")
crumb = Sentry.get_current_scope.breadcrumbs.peek
assert crumb.category == "httpx"
assert crumb.data == { status: 200, method: "GET", url: uri }
ensure
Sentry.configuration.send_default_pii = before_pii
end
end
def test_sentry_post_request
before_pii = Sentry.configuration.send_default_pii
begin
Sentry.configuration.send_default_pii = true
transaction = Sentry.start_transaction
Sentry.get_current_scope.set_span(transaction)
uri = build_uri("/post")
response = HTTPX.post(uri, form: { foo: "bar" })
verify_status(response, 200)
verify_spans(transaction, response, verb: "POST")
crumb = Sentry.get_current_scope.breadcrumbs.peek
assert crumb.category == "httpx"
assert crumb.data == { status: 200, method: "POST", url: uri, body: "foo=bar" }
ensure
Sentry.configuration.send_default_pii = before_pii
end
end
def test_sentry_multiple_requests
transaction = Sentry.start_transaction transaction = Sentry.start_transaction
Sentry.get_current_scope.set_span(transaction) Sentry.get_current_scope.set_span(transaction)
responses = HTTPX.get(build_uri("/status/200"), build_uri("/status/404")) uri = build_uri("/get")
verify_status(responses[0], 200)
verify_status(responses[1], 404)
verify_spans(transaction, *responses)
end
def test_sentry_server_error_request response = HTTPX.get(uri, params: { "foo" => "bar" })
transaction = Sentry.start_transaction
Sentry.get_current_scope.set_span(transaction)
uri = URI("http://unexisting/") verify_status(response, 200)
verify_spans(transaction, response, description: "GET #{uri}?foo=bar")
response = HTTPX.get(uri)
verify_error_response(response, /name or service not known/)
assert response.is_a?(HTTPX::ErrorResponse), "response should contain errors"
verify_spans(transaction, response, verb: "GET")
crumb = Sentry.get_current_scope.breadcrumbs.peek crumb = Sentry.get_current_scope.breadcrumbs.peek
assert crumb.category == "httpx" assert crumb.category == "httpx"
assert crumb.data == { error: "name or service not known", method: "GET", url: uri.to_s } assert crumb.data == { status: 200, method: "GET", url: "#{uri}?foo=bar" }
end ensure
Sentry.configuration.send_default_pii = before_pii
private
def verify_spans(transaction, *responses, verb: nil, description: nil)
assert transaction.span_recorder.spans.count == responses.size + 1
assert transaction.span_recorder.spans[0] == transaction
response_spans = transaction.span_recorder.spans[1..-1]
responses.each_with_index do |response, idx|
request_span = response_spans[idx]
assert request_span.op == "httpx.client"
assert !request_span.start_timestamp.nil?
assert !request_span.timestamp.nil?
assert request_span.start_timestamp != request_span.timestamp
assert request_span.description == (description || "#{verb || "GET"} #{response.uri}")
if response.is_a?(HTTPX::ErrorResponse)
assert request_span.data == { error: response.error.message }
else
assert request_span.data == { status: response.status }
end
end
end
def setup
super
mock_io = StringIO.new
mock_logger = Logger.new(mock_io)
Sentry.init do |config|
config.traces_sample_rate = 1.0
config.logger = mock_logger
config.dsn = DUMMY_DSN
config.transport.transport_class = Sentry::DummyTransport
config.breadcrumbs_logger = [:http_logger]
# so the events will be sent synchronously for testing
config.background_worker_threads = 0
end
end
def origin
"https://#{httpbin}"
end end
end end
def test_sentry_send_no_pii
before_pii = Sentry.configuration.send_default_pii
begin
Sentry.configuration.send_default_pii = false
transaction = Sentry.start_transaction
Sentry.get_current_scope.set_span(transaction)
uri = build_uri("/get")
response = HTTPX.get(uri, params: { "foo" => "bar" })
verify_status(response, 200)
verify_spans(transaction, response, description: "GET #{uri}")
crumb = Sentry.get_current_scope.breadcrumbs.peek
assert crumb.category == "httpx"
assert crumb.data == { status: 200, method: "GET", url: uri }
ensure
Sentry.configuration.send_default_pii = before_pii
end
end
def test_sentry_post_request
before_pii = Sentry.configuration.send_default_pii
begin
Sentry.configuration.send_default_pii = true
transaction = Sentry.start_transaction
Sentry.get_current_scope.set_span(transaction)
uri = build_uri("/post")
response = HTTPX.post(uri, form: { foo: "bar" })
verify_status(response, 200)
verify_spans(transaction, response, verb: "POST")
crumb = Sentry.get_current_scope.breadcrumbs.peek
assert crumb.category == "httpx"
assert crumb.data == { status: 200, method: "POST", url: uri, body: "foo=bar" }
ensure
Sentry.configuration.send_default_pii = before_pii
end
end
def test_sentry_multiple_requests
transaction = Sentry.start_transaction
Sentry.get_current_scope.set_span(transaction)
responses = HTTPX.get(build_uri("/status/200"), build_uri("/status/404"))
verify_status(responses[0], 200)
verify_status(responses[1], 404)
verify_spans(transaction, *responses)
end
def test_sentry_server_error_request
transaction = Sentry.start_transaction
Sentry.get_current_scope.set_span(transaction)
uri = URI("http://unexisting/")
response = HTTPX.get(uri)
verify_error_response(response, /name or service not known/)
assert response.is_a?(HTTPX::ErrorResponse), "response should contain errors"
verify_spans(transaction, response, verb: "GET")
crumb = Sentry.get_current_scope.breadcrumbs.peek
assert crumb.category == "httpx"
assert crumb.data == { error: "name or service not known", method: "GET", url: uri.to_s }
end
private
def verify_spans(transaction, *responses, verb: nil, description: nil)
assert transaction.span_recorder.spans.count == responses.size + 1
assert transaction.span_recorder.spans[0] == transaction
response_spans = transaction.span_recorder.spans[1..-1]
responses.each_with_index do |response, idx|
request_span = response_spans[idx]
assert request_span.op == "httpx.client"
assert !request_span.start_timestamp.nil?
assert !request_span.timestamp.nil?
assert request_span.start_timestamp != request_span.timestamp
assert request_span.description == (description || "#{verb || "GET"} #{response.uri}")
if response.is_a?(HTTPX::ErrorResponse)
assert request_span.data == { error: response.error.message }
else
assert request_span.data == { status: response.status }
end
end
end
def setup
super
mock_io = StringIO.new
mock_logger = Logger.new(mock_io)
Sentry.init do |config|
config.traces_sample_rate = 1.0
config.logger = mock_logger
config.dsn = DUMMY_DSN
config.transport.transport_class = Sentry::DummyTransport
config.breadcrumbs_logger = [:http_logger]
# so the events will be sent synchronously for testing
config.background_worker_threads = 0
end
end
def origin
"https://#{httpbin}"
end
end end

View File

@ -53,14 +53,6 @@ module HTTPX
end end
end end
# :nocov:
def self.const_missing(const_name)
super unless const_name == :Client
warn "DEPRECATION WARNING: the class #{self}::Client is deprecated. Use #{self}::Session instead."
Session
end
# :nocov:
extend Chainable extend Chainable
end end

View File

@ -1,51 +1,24 @@
# frozen_string_literal: true # frozen_string_literal: true
if defined?(DDTrace) && DDTrace::VERSION::STRING >= "1.0.0" require "datadog/tracing/contrib/integration"
require "datadog/tracing/contrib/integration" require "datadog/tracing/contrib/configuration/settings"
require "datadog/tracing/contrib/configuration/settings" require "datadog/tracing/contrib/patcher"
require "datadog/tracing/contrib/patcher"
TRACING_MODULE = Datadog::Tracing module Datadog::Tracing
else
require "ddtrace/contrib/integration"
require "ddtrace/contrib/configuration/settings"
require "ddtrace/contrib/patcher"
TRACING_MODULE = Datadog
end
module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
module Contrib module Contrib
module HTTPX module HTTPX
if defined?(::DDTrace) && ::DDTrace::VERSION::STRING >= "1.0.0" METADATA_MODULE = Datadog::Tracing::Metadata
METADATA_MODULE = TRACING_MODULE::Metadata
TYPE_OUTBOUND = TRACING_MODULE::Metadata::Ext::HTTP::TYPE_OUTBOUND TYPE_OUTBOUND = Datadog::Tracing::Metadata::Ext::HTTP::TYPE_OUTBOUND
TAG_PEER_SERVICE = TRACING_MODULE::Metadata::Ext::TAG_PEER_SERVICE TAG_PEER_SERVICE = Datadog::Tracing::Metadata::Ext::TAG_PEER_SERVICE
TAG_URL = TRACING_MODULE::Metadata::Ext::HTTP::TAG_URL TAG_URL = Datadog::Tracing::Metadata::Ext::HTTP::TAG_URL
TAG_METHOD = TRACING_MODULE::Metadata::Ext::HTTP::TAG_METHOD TAG_METHOD = Datadog::Tracing::Metadata::Ext::HTTP::TAG_METHOD
TAG_TARGET_HOST = TRACING_MODULE::Metadata::Ext::NET::TAG_TARGET_HOST TAG_TARGET_HOST = Datadog::Tracing::Metadata::Ext::NET::TAG_TARGET_HOST
TAG_TARGET_PORT = TRACING_MODULE::Metadata::Ext::NET::TAG_TARGET_PORT TAG_TARGET_PORT = Datadog::Tracing::Metadata::Ext::NET::TAG_TARGET_PORT
TAG_STATUS_CODE = TRACING_MODULE::Metadata::Ext::HTTP::TAG_STATUS_CODE TAG_STATUS_CODE = Datadog::Tracing::Metadata::Ext::HTTP::TAG_STATUS_CODE
else
METADATA_MODULE = Datadog
TYPE_OUTBOUND = TRACING_MODULE::Ext::HTTP::TYPE_OUTBOUND
TAG_PEER_SERVICE = TRACING_MODULE::Ext::Integration::TAG_PEER_SERVICE
TAG_URL = TRACING_MODULE::Ext::HTTP::URL
TAG_METHOD = TRACING_MODULE::Ext::HTTP::METHOD
TAG_TARGET_HOST = TRACING_MODULE::Ext::NET::TARGET_HOST
TAG_TARGET_PORT = TRACING_MODULE::Ext::NET::TARGET_PORT
TAG_STATUS_CODE = Datadog::Ext::HTTP::STATUS_CODE
PROPAGATOR = TRACING_MODULE::HTTPPropagator
end
# HTTPX Datadog Plugin # HTTPX Datadog Plugin
# #
@ -64,14 +37,18 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
end end
def call def call
return unless tracing_enabled? return unless Datadog::Tracing.enabled?
@request.on(:response, &method(:finish)) @request.on(:response, &method(:finish))
verb = @request.verb verb = @request.verb
uri = @request.uri uri = @request.uri
@span = build_span @span = Datadog::Tracing.trace(
SPAN_REQUEST,
service: service_name(@request.uri.host, configuration, Datadog.configuration_for(self)),
span_type: TYPE_OUTBOUND
)
@span.resource = verb @span.resource = verb
@ -86,7 +63,8 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
# Tag as an external peer service # Tag as an external peer service
@span.set_tag(TAG_PEER_SERVICE, @span.service) @span.set_tag(TAG_PEER_SERVICE, @span.service)
propagate_headers if @configuration[:distributed_tracing] Datadog::Tracing::Propagation::HTTP.inject!(Datadog::Tracing.active_trace,
@request.headers) if @configuration[:distributed_tracing]
# Set analytics sample rate # Set analytics sample rate
if Contrib::Analytics.enabled?(@configuration[:analytics_enabled]) if Contrib::Analytics.enabled?(@configuration[:analytics_enabled])
@ -113,48 +91,8 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
private private
if defined?(::DDTrace) && ::DDTrace::VERSION::STRING >= "1.0.0" def configuration
@configuration ||= Datadog.configuration.tracing[:httpx, @request.uri.host]
def build_span
TRACING_MODULE.trace(
SPAN_REQUEST,
service: service_name(@request.uri.host, configuration, Datadog.configuration_for(self)),
span_type: TYPE_OUTBOUND
)
end
def propagate_headers
TRACING_MODULE::Propagation::HTTP.inject!(TRACING_MODULE.active_trace, @request.headers)
end
def configuration
@configuration ||= Datadog.configuration.tracing[:httpx, @request.uri.host]
end
def tracing_enabled?
TRACING_MODULE.enabled?
end
else
def build_span
service_name = configuration[:split_by_domain] ? @request.uri.host : configuration[:service_name]
configuration[:tracer].trace(
SPAN_REQUEST,
service: service_name,
span_type: TYPE_OUTBOUND
)
end
def propagate_headers
Datadog::HTTPPropagator.inject!(@span.context, @request.headers)
end
def configuration
@configuration ||= Datadog.configuration[:httpx, @request.uri.host]
end
def tracing_enabled?
configuration[:tracer].enabled
end
end end
end end
@ -179,7 +117,7 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
module Configuration module Configuration
# Default settings for httpx # Default settings for httpx
# #
class Settings < TRACING_MODULE::Contrib::Configuration::Settings class Settings < Datadog::Tracing::Contrib::Configuration::Settings
DEFAULT_ERROR_HANDLER = lambda do |response| DEFAULT_ERROR_HANDLER = lambda do |response|
Datadog::Ext::HTTP::ERROR_RANGE.cover?(response.status) Datadog::Ext::HTTP::ERROR_RANGE.cover?(response.status)
end end
@ -203,10 +141,10 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
o.lazy o.lazy
end end
if defined?(TRACING_MODULE::Contrib::SpanAttributeSchema) if defined?(Datadog::Tracing::Contrib::SpanAttributeSchema)
option :service_name do |o| option :service_name do |o|
o.default do o.default do
TRACING_MODULE::Contrib::SpanAttributeSchema.fetch_service_name( Datadog::Tracing::Contrib::SpanAttributeSchema.fetch_service_name(
"DD_TRACE_HTTPX_SERVICE_NAME", "DD_TRACE_HTTPX_SERVICE_NAME",
"httpx" "httpx"
) )
@ -231,7 +169,7 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
# Patcher enables patching of 'httpx' with datadog components. # Patcher enables patching of 'httpx' with datadog components.
# #
module Patcher module Patcher
include TRACING_MODULE::Contrib::Patcher include Datadog::Tracing::Contrib::Patcher
module_function module_function
@ -254,7 +192,6 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
class Integration class Integration
include Contrib::Integration include Contrib::Integration
# MINIMUM_VERSION = Gem::Version.new('0.11.0')
MINIMUM_VERSION = Gem::Version.new("0.10.2") MINIMUM_VERSION = Gem::Version.new("0.10.2")
register_as :httpx register_as :httpx
@ -271,14 +208,8 @@ module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
super && version >= MINIMUM_VERSION super && version >= MINIMUM_VERSION
end end
if defined?(::DDTrace) && ::DDTrace::VERSION::STRING >= "1.0.0" def new_configuration
def new_configuration Configuration::Settings.new
Configuration::Settings.new
end
else
def default_configuration
Configuration::Settings.new
end
end end
def patcher def patcher

View File

@ -7,38 +7,11 @@ require "faraday"
module Faraday module Faraday
class Adapter class Adapter
class HTTPX < Faraday::Adapter class HTTPX < Faraday::Adapter
# :nocov:
SSL_ERROR = if defined?(Faraday::SSLError)
Faraday::SSLError
else
Faraday::Error::SSLError
end
CONNECTION_FAILED_ERROR = if defined?(Faraday::ConnectionFailed)
Faraday::ConnectionFailed
else
Faraday::Error::ConnectionFailed
end
# :nocov:
unless Faraday::RequestOptions.method_defined?(:stream_response?)
module RequestOptionsExtensions
refine Faraday::RequestOptions do
def stream_response?
false
end
end
end
using RequestOptionsExtensions
end
module RequestMixin module RequestMixin
using ::HTTPX::HashExtensions
def build_connection(env) def build_connection(env)
return @connection if defined?(@connection) return @connection if defined?(@connection)
@connection = ::HTTPX.plugin(:compression).plugin(:persistent).plugin(ReasonPlugin) @connection = ::HTTPX.plugin(:persistent).plugin(ReasonPlugin)
@connection = @connection.with(@connection_options) unless @connection_options.empty? @connection = @connection.with(@connection_options) unless @connection_options.empty?
connection_opts = options_from_env(env) connection_opts = options_from_env(env)
@ -70,7 +43,7 @@ module Faraday
def connect(env, &blk) def connect(env, &blk)
connection(env, &blk) connection(env, &blk)
rescue ::HTTPX::TLSError => e rescue ::HTTPX::TLSError => e
raise SSL_ERROR, e raise Faraday::SSLError, e
rescue Errno::ECONNABORTED, rescue Errno::ECONNABORTED,
Errno::ECONNREFUSED, Errno::ECONNREFUSED,
Errno::ECONNRESET, Errno::ECONNRESET,
@ -79,9 +52,7 @@ module Faraday
Errno::ENETUNREACH, Errno::ENETUNREACH,
Errno::EPIPE, Errno::EPIPE,
::HTTPX::ConnectionError => e ::HTTPX::ConnectionError => e
raise CONNECTION_FAILED_ERROR, e raise Faraday::ConnectionFailed, e
rescue ::HTTPX::TimeoutError => e
raise Faraday::TimeoutError, e
end end
def build_request(env) def build_request(env)
@ -159,24 +130,13 @@ module Faraday
end end
module ReasonPlugin module ReasonPlugin
if RUBY_VERSION < "2.5" def self.load_dependencies(*)
def self.load_dependencies(*) require "net/http/status"
require "webrick"
end
else
def self.load_dependencies(*)
require "net/http/status"
end
end end
module ResponseMethods module ResponseMethods
if RUBY_VERSION < "2.5" def reason
def reason Net::HTTP::STATUS_CODES.fetch(@status)
WEBrick::HTTPStatus::StatusMessage.fetch(@status)
end
else
def reason
Net::HTTP::STATUS_CODES.fetch(@status)
end
end end
end end
end end
@ -261,10 +221,7 @@ module Faraday
# from Faraday::Adapter#request_timeout # from Faraday::Adapter#request_timeout
def request_timeout(type, options) def request_timeout(type, options)
key = Faraday::Adapter::TIMEOUT_KEYS.fetch(type) do key = Faraday::Adapter::TIMEOUT_KEYS[type]
msg = "Expected :read, :write, :open. Got #{type.inspect} :("
raise ArgumentError, msg
end
options[key] || options[:timeout] options[key] || options[:timeout]
end end
end end

View File

@ -2,13 +2,8 @@
module WebMock module WebMock
module HttpLibAdapters module HttpLibAdapters
if RUBY_VERSION < "2.5" require "net/http/status"
require "webrick/httpstatus" HTTP_REASONS = Net::HTTP::STATUS_CODES
HTTP_REASONS = WEBrick::HTTPStatus::StatusMessage
else
require "net/http/status"
HTTP_REASONS = Net::HTTP::STATUS_CODES
end
# #
# HTTPX plugin for webmock. # HTTPX plugin for webmock.

View File

@ -98,29 +98,11 @@ module HTTPX
end end
end end
# :nocov: def parse_altsvc_origin(alt_proto, alt_origin)
if RUBY_VERSION < "2.2" alt_scheme = parse_altsvc_scheme(alt_proto) or return
def parse_altsvc_origin(alt_proto, alt_origin) alt_origin = alt_origin[1..-2] if alt_origin.start_with?("\"") && alt_origin.end_with?("\"")
alt_scheme = parse_altsvc_scheme(alt_proto) or return
alt_origin = alt_origin[1..-2] if alt_origin.start_with?("\"") && alt_origin.end_with?("\"") URI.parse("#{alt_scheme}://#{alt_origin}")
if alt_origin.start_with?(":")
alt_origin = "#{alt_scheme}://dummy#{alt_origin}"
uri = URI.parse(alt_origin)
uri.host = nil
uri
else
URI.parse("#{alt_scheme}://#{alt_origin}")
end
end
else
def parse_altsvc_origin(alt_proto, alt_origin)
alt_scheme = parse_altsvc_scheme(alt_proto) or return
alt_origin = alt_origin[1..-2] if alt_origin.start_with?("\"") && alt_origin.end_with?("\"")
URI.parse("#{alt_scheme}://#{alt_origin}")
end
end end
# :nocov:
end end
end end

View File

@ -27,18 +27,6 @@ module HTTPX
branch(default_options).request(*args, **options) branch(default_options).request(*args, **options)
end end
# :nocov:
def timeout(**args)
warn ":#{__method__} is deprecated, use :with_timeout instead"
with(timeout: args)
end
def headers(headers)
warn ":#{__method__} is deprecated, use :with_headers instead"
with(headers: headers)
end
# :nocov:
def accept(type) def accept(type)
with(headers: { "accept" => String(type) }) with(headers: { "accept" => String(type) })
end end
@ -54,17 +42,6 @@ module HTTPX
klass.plugin(pl, options, &blk).new klass.plugin(pl, options, &blk).new
end end
# deprecated
# :nocov:
def plugins(pls)
warn ":#{__method__} is deprecated, use :plugin instead"
klass = is_a?(S) ? self.class : Session
klass = Class.new(klass)
klass.instance_variable_set(:@default_options, klass.default_options.merge(default_options))
klass.plugins(pls).new
end
# :nocov:
def with(options, &blk) def with(options, &blk)
branch(default_options.merge(options), &blk) branch(default_options.merge(options), &blk)
end end

View File

@ -33,7 +33,6 @@ module HTTPX
include Callbacks include Callbacks
using URIExtensions using URIExtensions
using NumericExtensions
require "httpx/connection/http2" require "httpx/connection/http2"
require "httpx/connection/http1" require "httpx/connection/http1"
@ -70,7 +69,6 @@ module HTTPX
@inflight = 0 @inflight = 0
@keep_alive_timeout = @options.timeout[:keep_alive_timeout] @keep_alive_timeout = @options.timeout[:keep_alive_timeout]
@total_timeout = @options.timeout[:total_timeout]
self.addresses = @options.addresses if @options.addresses self.addresses = @options.addresses if @options.addresses
end end
@ -270,21 +268,6 @@ module HTTPX
end end
def timeout def timeout
if @total_timeout
return @total_timeout unless @connected_at
elapsed_time = @total_timeout - Utils.elapsed_time(@connected_at)
if elapsed_time.negative?
ex = TotalTimeoutError.new(@total_timeout, "Timed out after #{@total_timeout} seconds")
ex.set_backtrace(caller)
on_error(ex)
return
end
return elapsed_time
end
return @timeout if defined?(@timeout) return @timeout if defined?(@timeout)
return @options.timeout[:connect_timeout] if @state == :idle return @options.timeout[:connect_timeout] if @state == :idle
@ -604,6 +587,9 @@ module HTTPX
purge_after_closed purge_after_closed
when :already_open when :already_open
nextstate = :open nextstate = :open
# the first check for given io readiness must still use a timeout.
# connect is the reasonable choice in such a case.
@timeout = @options.timeout[:connect_timeout]
send_pending send_pending
when :active when :active
return unless @state == :inactive return unless @state == :inactive
@ -643,23 +629,16 @@ module HTTPX
def on_error(error) def on_error(error)
if error.instance_of?(TimeoutError) if error.instance_of?(TimeoutError)
if @total_timeout && @connected_at && # inactive connections do not contribute to the select loop, therefore
Utils.elapsed_time(@connected_at) > @total_timeout # they should not fail due to such errors.
ex = TotalTimeoutError.new(@total_timeout, "Timed out after #{@total_timeout} seconds") return if @state == :inactive
ex.set_backtrace(error.backtrace)
error = ex
else
# inactive connections do not contribute to the select loop, therefore
# they should not fail due to such errors.
return if @state == :inactive
if @timeout if @timeout
@timeout -= error.timeout @timeout -= error.timeout
return unless @timeout <= 0 return unless @timeout <= 0
end
error = error.to_connection_error if connecting?
end end
error = error.to_connection_error if connecting?
end end
handle_error(error) handle_error(error)
reset reset

View File

@ -51,8 +51,6 @@ module HTTPX
# non-canonical domain. # non-canonical domain.
attr_reader :domain attr_reader :domain
DOT = "." # :nodoc:
class << self class << self
def new(domain) def new(domain)
return domain if domain.is_a?(self) return domain if domain.is_a?(self)
@ -63,7 +61,7 @@ module HTTPX
# Normalizes a _domain_ using the Punycode algorithm as necessary. # Normalizes a _domain_ using the Punycode algorithm as necessary.
# The result will be a downcased, ASCII-only string. # The result will be a downcased, ASCII-only string.
def normalize(domain) def normalize(domain)
domain = domain.chomp(DOT).unicode_normalize(:nfc) unless domain.ascii_only? domain = domain.chomp(".").unicode_normalize(:nfc) unless domain.ascii_only?
Punycode.encode_hostname(domain).downcase Punycode.encode_hostname(domain).downcase
end end
end end
@ -73,7 +71,7 @@ module HTTPX
def initialize(hostname) def initialize(hostname)
hostname = String(hostname) hostname = String(hostname)
raise ArgumentError, "domain name must not start with a dot: #{hostname}" if hostname.start_with?(DOT) raise ArgumentError, "domain name must not start with a dot: #{hostname}" if hostname.start_with?(".")
begin begin
@ipaddr = IPAddr.new(hostname) @ipaddr = IPAddr.new(hostname)
@ -84,7 +82,7 @@ module HTTPX
end end
@hostname = DomainName.normalize(hostname) @hostname = DomainName.normalize(hostname)
tld = if (last_dot = @hostname.rindex(DOT)) tld = if (last_dot = @hostname.rindex("."))
@hostname[(last_dot + 1)..-1] @hostname[(last_dot + 1)..-1]
else else
@hostname @hostname
@ -94,7 +92,7 @@ module HTTPX
@domain = if last_dot @domain = if last_dot
# fallback - accept cookies down to second level # fallback - accept cookies down to second level
# cf. http://www.dkim-reputation.org/regdom-libs/ # cf. http://www.dkim-reputation.org/regdom-libs/
if (penultimate_dot = @hostname.rindex(DOT, last_dot - 1)) if (penultimate_dot = @hostname.rindex(".", last_dot - 1))
@hostname[(penultimate_dot + 1)..-1] @hostname[(penultimate_dot + 1)..-1]
else else
@hostname @hostname
@ -126,17 +124,12 @@ module HTTPX
@domain && self <= domain && domain <= @domain @domain && self <= domain && domain <= @domain
end end
# def ==(other)
# other = DomainName.new(other)
# other.hostname == @hostname
# end
def <=>(other) def <=>(other)
other = DomainName.new(other) other = DomainName.new(other)
othername = other.hostname othername = other.hostname
if othername == @hostname if othername == @hostname
0 0
elsif @hostname.end_with?(othername) && @hostname[-othername.size - 1, 1] == DOT elsif @hostname.end_with?(othername) && @hostname[-othername.size - 1, 1] == "."
# The other is higher # The other is higher
-1 -1
else else

View File

@ -22,8 +22,6 @@ module HTTPX
end end
end end
class TotalTimeoutError < TimeoutError; end
class ConnectTimeoutError < TimeoutError; end class ConnectTimeoutError < TimeoutError; end
class RequestTimeoutError < TimeoutError class RequestTimeoutError < TimeoutError

View File

@ -3,96 +3,6 @@
require "uri" require "uri"
module HTTPX module HTTPX
unless Method.method_defined?(:curry)
# Backport
#
# Ruby 2.1 and lower implement curry only for Procs.
#
# Why not using Refinements? Because they don't work for Method (tested with ruby 2.1.9).
#
module CurryMethods
# Backport for the Method#curry method, which is part of ruby core since 2.2 .
#
def curry(*args)
to_proc.curry(*args)
end
end
Method.__send__(:include, CurryMethods)
end
unless String.method_defined?(:+@)
# Backport for +"", to initialize unfrozen strings from the string literal.
#
module LiteralStringExtensions
def +@
frozen? ? dup : self
end
end
String.__send__(:include, LiteralStringExtensions)
end
unless Numeric.method_defined?(:positive?)
# Ruby 2.3 Backport (Numeric#positive?)
#
module PosMethods
def positive?
self > 0
end
end
Numeric.__send__(:include, PosMethods)
end
unless Numeric.method_defined?(:negative?)
# Ruby 2.3 Backport (Numeric#negative?)
#
module NegMethods
def negative?
self < 0
end
end
Numeric.__send__(:include, NegMethods)
end
module NumericExtensions
# Ruby 2.4 backport
refine Numeric do
def infinite?
self == Float::INFINITY
end unless Numeric.method_defined?(:infinite?)
end
end
module StringExtensions
refine String do
# Ruby 2.5 backport
def delete_suffix!(suffix)
suffix = Backports.coerce_to_str(suffix)
chomp! if frozen?
len = suffix.length
if len > 0 && index(suffix, -len)
self[-len..-1] = ''
self
else
nil
end
end unless String.method_defined?(:delete_suffix!)
end
end
module HashExtensions
refine Hash do
# Ruby 2.4 backport
def compact
h = {}
each do |key, value|
h[key] = value unless value == nil
end
h
end unless Hash.method_defined?(:compact)
end
end
module ArrayExtensions module ArrayExtensions
module FilterMap module FilterMap
refine Array do refine Array do
@ -108,16 +18,6 @@ module HTTPX
end unless Array.method_defined?(:filter_map) end unless Array.method_defined?(:filter_map)
end end
module Sum
refine Array do
# Ruby 2.6 backport
def sum(accumulator = 0, &block)
values = block_given? ? map(&block) : self
values.inject(accumulator, :+)
end
end unless Array.method_defined?(:sum)
end
module Intersect module Intersect
refine Array do refine Array do
# Ruby 3.1 backport # Ruby 3.1 backport
@ -133,30 +33,6 @@ module HTTPX
end end
end end
module IOExtensions
refine IO do
# Ruby 2.3 backport
# provides a fallback for rubies where IO#wait isn't implemented,
# but IO#wait_readable and IO#wait_writable are.
def wait(timeout = nil, _mode = :read_write)
r, w = IO.select([self], [self], nil, timeout)
return unless r || w
self
end unless IO.method_defined?(:wait) && IO.instance_method(:wait).arity == 2
end
end
module RegexpExtensions
refine(Regexp) do
# Ruby 2.4 backport
def match?(*args)
!match(*args).nil?
end
end
end
module URIExtensions module URIExtensions
# uri 0.11 backport, ships with ruby 3.1 # uri 0.11 backport, ships with ruby 3.1
refine URI::Generic do refine URI::Generic do

View File

@ -6,15 +6,11 @@ module HTTPX
TLSError = OpenSSL::SSL::SSLError TLSError = OpenSSL::SSL::SSLError
class SSL < TCP class SSL < TCP
using RegexpExtensions unless Regexp.method_defined?(:match?) # rubocop:disable Style/MutableConstant
TLS_OPTIONS = { alpn_protocols: %w[h2 http/1.1].freeze }
TLS_OPTIONS = if OpenSSL::SSL::SSLContext.instance_methods.include?(:alpn_protocols)
{ alpn_protocols: %w[h2 http/1.1].freeze }
else
{}
end
# https://github.com/jruby/jruby-openssl/issues/284 # https://github.com/jruby/jruby-openssl/issues/284
TLS_OPTIONS[:verify_hostname] = true if RUBY_ENGINE == "jruby" TLS_OPTIONS[:verify_hostname] = true if RUBY_ENGINE == "jruby"
# rubocop:enable Style/MutableConstant
TLS_OPTIONS.freeze TLS_OPTIONS.freeze
attr_writer :ssl_session attr_writer :ssl_session
@ -103,61 +99,18 @@ module HTTPX
try_ssl_connect try_ssl_connect
end end
if RUBY_VERSION < "2.3" def try_ssl_connect
# :nocov: case @io.connect_nonblock(exception: false)
def try_ssl_connect when :wait_readable
@io.connect_nonblock
@io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE && @verify_hostname
transition(:negotiated)
@interests = :w
rescue ::IO::WaitReadable
@interests = :r @interests = :r
rescue ::IO::WaitWritable return
when :wait_writable
@interests = :w @interests = :w
return
end end
@io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE && @verify_hostname
def read(_, buffer) transition(:negotiated)
super @interests = :w
rescue ::IO::WaitWritable
buffer.clear
0
end
def write(*)
super
rescue ::IO::WaitReadable
0
end
# :nocov:
else
def try_ssl_connect
case @io.connect_nonblock(exception: false)
when :wait_readable
@interests = :r
return
when :wait_writable
@interests = :w
return
end
@io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE && @verify_hostname
transition(:negotiated)
@interests = :w
end
# :nocov:
if OpenSSL::VERSION < "2.0.6"
def read(size, buffer)
@io.read_nonblock(size, buffer)
buffer.bytesize
rescue ::IO::WaitReadable,
::IO::WaitWritable
buffer.clear
0
rescue EOFError
nil
end
end
# :nocov:
end end
private private

View File

@ -94,84 +94,43 @@ module HTTPX
retry retry
end end
if RUBY_VERSION < "2.3" def try_connect
# :nocov: case @io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s), exception: false)
def try_connect when :wait_readable
@io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s))
rescue ::IO::WaitWritable, Errno::EALREADY
@interests = :w
rescue ::IO::WaitReadable
@interests = :r @interests = :r
rescue Errno::EISCONN return
transition(:connected) when :wait_writable
@interests = :w
else
transition(:connected)
@interests = :w @interests = :w
return
end end
private :try_connect transition(:connected)
@interests = :w
rescue Errno::EALREADY
@interests = :w
end
private :try_connect
def read(size, buffer) def read(size, buffer)
@io.read_nonblock(size, buffer) ret = @io.read_nonblock(size, buffer, exception: false)
log { "READ: #{buffer.bytesize} bytes..." } if ret == :wait_readable
buffer.bytesize
rescue ::IO::WaitReadable
buffer.clear buffer.clear
0 return 0
rescue EOFError
nil
end end
return if ret.nil?
def write(buffer) log { "READ: #{buffer.bytesize} bytes..." }
siz = @io.write_nonblock(buffer) buffer.bytesize
log { "WRITE: #{siz} bytes..." } end
buffer.shift!(siz)
siz
rescue ::IO::WaitWritable
0
rescue EOFError
nil
end
# :nocov:
else
def try_connect
case @io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s), exception: false)
when :wait_readable
@interests = :r
return
when :wait_writable
@interests = :w
return
end
transition(:connected)
@interests = :w
rescue Errno::EALREADY
@interests = :w
end
private :try_connect
def read(size, buffer) def write(buffer)
ret = @io.read_nonblock(size, buffer, exception: false) siz = @io.write_nonblock(buffer, exception: false)
if ret == :wait_readable return 0 if siz == :wait_writable
buffer.clear return if siz.nil?
return 0
end
return if ret.nil?
log { "READ: #{buffer.bytesize} bytes..." } log { "WRITE: #{siz} bytes..." }
buffer.bytesize
end
def write(buffer) buffer.shift!(siz)
siz = @io.write_nonblock(buffer, exception: false) siz
return 0 if siz == :wait_writable
return if siz.nil?
log { "WRITE: #{siz} bytes..." }
buffer.shift!(siz)
siz
end
end end
def close def close

View File

@ -23,45 +23,19 @@ module HTTPX
true true
end end
if RUBY_VERSION < "2.3" def close
# :nocov: @io.close
def close
@io.close
rescue StandardError
nil
end
# :nocov:
else
def close
@io.close
end
end end
# :nocov: if RUBY_ENGINE == "jruby"
if (RUBY_ENGINE == "truffleruby" && RUBY_ENGINE_VERSION < "21.1.0") || # In JRuby, sendmsg_nonblock is not implemented
RUBY_VERSION < "2.3"
def write(buffer) def write(buffer)
siz = @io.sendmsg_nonblock(buffer.to_s, 0, Socket.sockaddr_in(@port, @host.to_s)) siz = @io.send(buffer.to_s, 0, @host, @port)
log { "WRITE: #{siz} bytes..." } log { "WRITE: #{siz} bytes..." }
buffer.shift!(siz) buffer.shift!(siz)
siz siz
rescue ::IO::WaitWritable
0
rescue EOFError
nil
end
def read(size, buffer)
data, _ = @io.recvfrom_nonblock(size)
buffer.replace(data)
log { "READ: #{buffer.bytesize} bytes..." }
buffer.bytesize
rescue ::IO::WaitReadable
0
rescue IOError
end end
else else
def write(buffer) def write(buffer)
siz = @io.sendmsg_nonblock(buffer.to_s, 0, Socket.sockaddr_in(@port, @host.to_s), exception: false) siz = @io.sendmsg_nonblock(buffer.to_s, 0, Socket.sockaddr_in(@port, @host.to_s), exception: false)
return 0 if siz == :wait_writable return 0 if siz == :wait_writable
@ -72,26 +46,17 @@ module HTTPX
buffer.shift!(siz) buffer.shift!(siz)
siz siz
end end
def read(size, buffer)
ret = @io.recvfrom_nonblock(size, 0, buffer, exception: false)
return 0 if ret == :wait_readable
return if ret.nil?
log { "READ: #{buffer.bytesize} bytes..." }
buffer.bytesize
rescue IOError
end
end end
# In JRuby, sendmsg_nonblock is not implemented def read(size, buffer)
def write(buffer) ret = @io.recvfrom_nonblock(size, 0, buffer, exception: false)
siz = @io.send(buffer.to_s, 0, @host, @port) return 0 if ret == :wait_readable
log { "WRITE: #{siz} bytes..." } return if ret.nil?
buffer.shift!(siz)
siz log { "READ: #{buffer.bytesize} bytes..." }
end if RUBY_ENGINE == "jruby"
# :nocov: buffer.bytesize
rescue IOError
end
end end
end end

View File

@ -27,14 +27,7 @@ module HTTPX
@keep_open = true @keep_open = true
@state = :connected @state = :connected
else else
if @options.transport_options @path = addresses.first
# :nocov:
warn ":transport_options is deprecated, use :addresses instead"
@path = @options.transport_options[:path]
# :nocov:
else
@path = addresses.first
end
end end
@io ||= build_socket @io ||= build_socket
end end

View File

@ -24,26 +24,11 @@ module HTTPX
debug_stream << message debug_stream << message
end end
if Exception.instance_methods.include?(:full_message) def log_exception(ex, level: @options.debug_level, color: nil)
return unless @options.debug
def log_exception(ex, level: @options.debug_level, color: nil) return unless @options.debug_level >= level
return unless @options.debug
return unless @options.debug_level >= level
log(level: level, color: color) { ex.full_message }
end
else
def log_exception(ex, level: @options.debug_level, color: nil)
return unless @options.debug
return unless @options.debug_level >= level
message = +"#{ex.message} (#{ex.class})"
message << "\n" << ex.backtrace.join("\n") unless ex.backtrace.nil?
log(level: level, color: color) { message }
end
log(level: level, color: color) { ex.full_message }
end end
end end
end end

View File

@ -7,11 +7,10 @@ module HTTPX
BUFFER_SIZE = 1 << 14 BUFFER_SIZE = 1 << 14
WINDOW_SIZE = 1 << 14 # 16K WINDOW_SIZE = 1 << 14 # 16K
MAX_BODY_THRESHOLD_SIZE = (1 << 10) * 112 # 112K MAX_BODY_THRESHOLD_SIZE = (1 << 10) * 112 # 112K
CONNECT_TIMEOUT = 60
OPERATION_TIMEOUT = 60
KEEP_ALIVE_TIMEOUT = 20 KEEP_ALIVE_TIMEOUT = 20
SETTINGS_TIMEOUT = 10 SETTINGS_TIMEOUT = 10
READ_TIMEOUT = WRITE_TIMEOUT = REQUEST_TIMEOUT = Float::INFINITY CONNECT_TIMEOUT = READ_TIMEOUT = WRITE_TIMEOUT = 60
REQUEST_TIMEOUT = OPERATION_TIMEOUT = Float::INFINITY
# https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408 # https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
ip_address_families = begin ip_address_families = begin
@ -31,6 +30,9 @@ module HTTPX
:ssl => {}, :ssl => {},
:http2_settings => { settings_enable_push: 0 }, :http2_settings => { settings_enable_push: 0 },
:fallback_protocol => "http/1.1", :fallback_protocol => "http/1.1",
:supported_compression_formats => %w[gzip deflate],
:decompress_response_body => true,
:compress_request_body => true,
:timeout => { :timeout => {
connect_timeout: CONNECT_TIMEOUT, connect_timeout: CONNECT_TIMEOUT,
settings_timeout: SETTINGS_TIMEOUT, settings_timeout: SETTINGS_TIMEOUT,
@ -52,7 +54,6 @@ module HTTPX
:connection_class => Class.new(Connection), :connection_class => Class.new(Connection),
:options_class => Class.new(self), :options_class => Class.new(self),
:transport => nil, :transport => nil,
:transport_options => nil,
:addresses => nil, :addresses => nil,
:persistent => false, :persistent => false,
:resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym, :resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym,
@ -60,28 +61,6 @@ module HTTPX
:ip_families => ip_address_families, :ip_families => ip_address_families,
}.freeze }.freeze
begin
module HashExtensions
refine Hash do
def >=(other)
Hash[other] <= self
end
def <=(other)
other = Hash[other]
return false unless size <= other.size
each do |k, v|
v2 = other.fetch(k) { return false }
return false unless v2 == v
end
true
end
end
end
using HashExtensions
end unless Hash.method_defined?(:>=)
class << self class << self
def new(options = {}) def new(options = {})
# let enhanced options go through # let enhanced options go through
@ -100,38 +79,10 @@ module HTTPX
attr_reader(optname) attr_reader(optname)
end end
def def_option(optname, *args, &block)
if args.empty? && !block
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
def option_#{optname}(v); v; end # def option_smth(v); v; end
OUT
return
end
deprecated_def_option(optname, *args, &block)
end
def deprecated_def_option(optname, layout = nil, &interpreter)
warn "DEPRECATION WARNING: using `def_option(#{optname})` for setting options is deprecated. " \
"Define module OptionsMethods and `def option_#{optname}(val)` instead."
if layout
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
def option_#{optname}(value) # def option_origin(v)
#{layout} # URI(v)
end # end
OUT
elsif interpreter
define_method(:"option_#{optname}") do |value|
instance_exec(value, &interpreter)
end
end
end
end end
def initialize(options = {}) def initialize(options = {})
__initialize__(options) do_initialize(options)
freeze freeze
end end
@ -142,6 +93,7 @@ module HTTPX
@timeout.freeze @timeout.freeze
@headers.freeze @headers.freeze
@addresses.freeze @addresses.freeze
@supported_compression_formats.freeze
end end
def option_origin(value) def option_origin(value)
@ -157,14 +109,11 @@ module HTTPX
end end
def option_timeout(value) def option_timeout(value)
timeouts = Hash[value] Hash[value]
end
if timeouts.key?(:loop_timeout) def option_supported_compression_formats(value)
warn ":loop_timeout is deprecated, use :operation_timeout instead" Array(value).map(&:to_s)
timeouts[:operation_timeout] = timeouts.delete(:loop_timeout)
end
timeouts
end end
def option_max_concurrent_requests(value) def option_max_concurrent_requests(value)
@ -196,7 +145,10 @@ module HTTPX
end end
def option_body_threshold_size(value) def option_body_threshold_size(value)
Integer(value) bytes = Integer(value)
raise TypeError, ":body_threshold_size must be positive" unless bytes.positive?
bytes
end end
def option_transport(value) def option_transport(value)
@ -218,10 +170,13 @@ module HTTPX
params form json xml body ssl http2_settings params form json xml body ssl http2_settings
request_class response_class headers_class request_body_class request_class response_class headers_class request_body_class
response_body_class connection_class options_class response_body_class connection_class options_class
io fallback_protocol debug debug_level transport_options resolver_class resolver_options io fallback_protocol debug debug_level resolver_class resolver_options
compress_request_body decompress_response_body
persistent persistent
].each do |method_name| ].each do |method_name|
def_option(method_name) class_eval(<<-OUT, __FILE__, __LINE__ + 1)
def option_#{method_name}(v); v; end # def option_smth(v); v; end
OUT
end end
REQUEST_IVARS = %i[@params @form @xml @json @body].freeze REQUEST_IVARS = %i[@params @form @xml @json @body].freeze
@ -270,30 +225,15 @@ module HTTPX
end end
end end
if RUBY_VERSION > "2.4.0" def initialize_dup(other)
def initialize_dup(other) instance_variables.each do |ivar|
instance_variables.each do |ivar| instance_variable_set(ivar, other.instance_variable_get(ivar).dup)
instance_variable_set(ivar, other.instance_variable_get(ivar).dup)
end
end
else
def initialize_dup(other)
instance_variables.each do |ivar|
value = other.instance_variable_get(ivar)
value = case value
when Symbol, Numeric, TrueClass, FalseClass
value
else
value.dup
end
instance_variable_set(ivar, value)
end
end end
end end
private private
def __initialize__(options = {}) def do_initialize(options = {})
defaults = DEFAULT_OPTIONS.merge(options) defaults = DEFAULT_OPTIONS.merge(options)
defaults.each do |k, v| defaults.each do |k, v|
next if v.nil? next if v.nil?

25
lib/httpx/plugins/auth.rb Normal file
View 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

View File

@ -11,10 +11,6 @@ module HTTPX
@password = password @password = password
end end
def can_authenticate?(authenticate)
authenticate && /Basic .*/.match?(authenticate)
end
def authenticate(*) def authenticate(*)
"Basic #{Base64.strict_encode64("#{@user}:#{@password}")}" "Basic #{Base64.strict_encode64("#{@user}:#{@password}")}"
end end

View File

@ -8,8 +8,6 @@ module HTTPX
module Plugins module Plugins
module Authentication module Authentication
class Digest class Digest
using RegexpExtensions unless Regexp.method_defined?(:match?)
def initialize(user, password, hashed: false, **) def initialize(user, password, hashed: false, **)
@user = user @user = user
@password = password @password = password
@ -31,9 +29,8 @@ module HTTPX
# discard first token, it's Digest # discard first token, it's Digest
auth_info = authenticate[/^(\w+) (.*)/, 2] auth_info = authenticate[/^(\w+) (.*)/, 2]
params = Hash[auth_info.split(/ *, */) params = auth_info.split(/ *, */)
.map { |val| val.split("=") } .to_h { |val| val.split("=") }.transform_values { |v| v.delete("\"") }
.map { |k, v| [k, v.delete("\"")] }]
nonce = params["nonce"] nonce = params["nonce"]
nc = next_nonce nc = next_nonce

View File

@ -7,8 +7,6 @@ module HTTPX
module Plugins module Plugins
module Authentication module Authentication
class Ntlm class Ntlm
using RegexpExtensions unless Regexp.method_defined?(:match?)
def initialize(user, password, domain: nil) def initialize(user, password, domain: nil)
@user = user @user = user
@password = password @password = password

View File

@ -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

View File

@ -146,7 +146,6 @@ module HTTPX
def configure(klass) def configure(klass)
klass.plugin(:expect) klass.plugin(:expect)
klass.plugin(:compression)
end end
end end

View File

@ -5,26 +5,25 @@ module HTTPX
# #
# This plugin adds helper methods to implement HTTP Basic Auth (https://tools.ietf.org/html/rfc7617) # This plugin adds helper methods to implement HTTP Basic Auth (https://tools.ietf.org/html/rfc7617)
# #
# https://gitlab.com/os85/httpx/wikis/Authentication#basic-authentication # https://gitlab.com/os85/httpx/wikis/Authorization#basic-auth
# #
module BasicAuth module BasicAuth
class << self class << self
def load_dependencies(_klass) def load_dependencies(_klass)
require_relative "authentication/basic" require_relative "auth/basic"
end end
def configure(klass) def configure(klass)
klass.plugin(:authentication) klass.plugin(:auth)
end end
end end
module InstanceMethods module InstanceMethods
def basic_auth(user, password) def basic_auth(user, password)
authentication(Authentication::Basic.new(user, password).authenticate) authorization(Authentication::Basic.new(user, password).authenticate)
end end
alias_method :basic_authentication, :basic_auth
end end
end end
register_plugin :basic_authentication, BasicAuth register_plugin :basic_auth, BasicAuth
end end
end end

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -71,7 +71,7 @@ module HTTPX
end end
module OptionsMethods module OptionsMethods
def __initialize__(*) def do_initialize(*)
super super
return unless @headers.key?("cookie") return unless @headers.key?("cookie")

View File

@ -6,8 +6,6 @@ require "time"
module HTTPX module HTTPX
module Plugins::Cookies module Plugins::Cookies
module SetCookieParser module SetCookieParser
using(RegexpExtensions) unless Regexp.method_defined?(:match?)
# Whitespace. # Whitespace.
RE_WSP = /[ \t]+/.freeze RE_WSP = /[ \t]+/.freeze

View File

@ -5,7 +5,7 @@ module HTTPX
# #
# This plugin adds helper methods to implement HTTP Digest Auth (https://tools.ietf.org/html/rfc7616) # This plugin adds helper methods to implement HTTP Digest Auth (https://tools.ietf.org/html/rfc7616)
# #
# https://gitlab.com/os85/httpx/wikis/Authentication#authentication # https://gitlab.com/os85/httpx/wikis/Authorization#digest-auth
# #
module DigestAuth module DigestAuth
DigestError = Class.new(Error) DigestError = Class.new(Error)
@ -16,7 +16,7 @@ module HTTPX
end end
def load_dependencies(*) def load_dependencies(*)
require_relative "authentication/digest" require_relative "auth/digest"
end end
end end
@ -29,11 +29,11 @@ module HTTPX
end end
module InstanceMethods module InstanceMethods
def digest_authentication(user, password, hashed: false) def digest_auth(user, password, hashed: false)
with(digest: Authentication::Digest.new(user, password, hashed: hashed)) with(digest: Authentication::Digest.new(user, password, hashed: hashed))
end end
alias_method :digest_auth, :digest_authentication private
def send_requests(*requests) def send_requests(*requests)
requests.flat_map do |request| requests.flat_map do |request|
@ -57,6 +57,6 @@ module HTTPX
end end
end end
register_plugin :digest_authentication, DigestAuth register_plugin :digest_auth, DigestAuth
end end
end end

View File

@ -48,7 +48,27 @@ module HTTPX
return response unless REDIRECT_STATUS.include?(response.status) && response.headers.key?("location") return response unless REDIRECT_STATUS.include?(response.status) && response.headers.key?("location")
return response unless max_redirects.positive? return response unless max_redirects.positive?
retry_request = build_redirect_request(redirect_request, response, options) # build redirect request
redirect_uri = __get_location_from_response(response)
if response.status == 305 && options.respond_to?(:proxy)
# The requested resource MUST be accessed through the proxy given by
# the Location field. The Location field gives the URI of the proxy.
retry_options = options.merge(headers: redirect_request.headers,
proxy: { uri: redirect_uri },
body: redirect_request.body,
max_redirects: max_redirects - 1)
redirect_uri = redirect_request.uri
options = retry_options
else
# redirects are **ALWAYS** GET
retry_options = options.merge(headers: redirect_request.headers,
body: redirect_request.body,
max_redirects: max_redirects - 1)
end
retry_request = build_request("GET", redirect_uri, retry_options)
request.redirect_request = retry_request request.redirect_request = retry_request
@ -83,29 +103,6 @@ module HTTPX
nil nil
end end
def build_redirect_request(request, response, options)
redirect_uri = __get_location_from_response(response)
max_redirects = request.max_redirects
if response.status == 305 && options.respond_to?(:proxy)
# The requested resource MUST be accessed through the proxy given by
# the Location field. The Location field gives the URI of the proxy.
retry_options = options.merge(headers: request.headers,
proxy: { uri: redirect_uri },
body: request.body,
max_redirects: max_redirects - 1)
redirect_uri = request.url
else
# redirects are **ALWAYS** GET
retry_options = options.merge(headers: request.headers,
body: request.body,
max_redirects: max_redirects - 1)
end
build_request("GET", redirect_uri, retry_options)
end
def __get_location_from_response(response) def __get_location_from_response(response)
location_uri = URI(response.headers["location"]) location_uri = URI(response.headers["location"])
location_uri = response.uri.merge(location_uri) if location_uri.relative? location_uri = response.uri.merge(location_uri) if location_uri.relative?

View File

@ -49,20 +49,19 @@ module HTTPX
class << self class << self
def load_dependencies(*) def load_dependencies(*)
require "stringio" require "stringio"
require "httpx/plugins/grpc/grpc_encoding"
require "httpx/plugins/grpc/message" require "httpx/plugins/grpc/message"
require "httpx/plugins/grpc/call" require "httpx/plugins/grpc/call"
end end
def configure(klass) def configure(klass)
klass.plugin(:persistent) klass.plugin(:persistent)
klass.plugin(:compression)
klass.plugin(:stream) klass.plugin(:stream)
end end
def extra_options(options) def extra_options(options)
options.merge( options.merge(
fallback_protocol: "h2", fallback_protocol: "h2",
http2_settings: { wait_for_handshake: false },
grpc_rpcs: {}.freeze, grpc_rpcs: {}.freeze,
grpc_compression: false, grpc_compression: false,
grpc_deadline: DEADLINE grpc_deadline: DEADLINE
@ -108,9 +107,18 @@ module HTTPX
@trailing_metadata = Hash[trailers] @trailing_metadata = Hash[trailers]
super super
end end
end
def encoders module RequestBodyMethods
@options.encodings def initialize(headers, _)
super
if (compression = headers["grpc-encoding"])
deflater_body = self.class.initialize_deflater_body(@body, compression)
@body = Transcoder::GRPCEncoding.encode(deflater_body || @body, compressed: !deflater_body.nil?)
else
@body = Transcoder::GRPCEncoding.encode(@body, compressed: false)
end
end end
end end
@ -233,7 +241,7 @@ module HTTPX
uri.path = rpc_method uri.path = rpc_method
headers = HEADERS.merge( headers = HEADERS.merge(
"grpc-accept-encoding" => ["identity", *@options.encodings.keys] "grpc-accept-encoding" => ["identity", *@options.supported_compression_formats]
) )
unless deadline == Float::INFINITY unless deadline == Float::INFINITY
# convert to milliseconds # convert to milliseconds
@ -244,27 +252,13 @@ module HTTPX
headers = headers.merge(metadata) if metadata headers = headers.merge(metadata) if metadata
# prepare compressor # prepare compressor
deflater = nil
compression = @options.grpc_compression == true ? "gzip" : @options.grpc_compression compression = @options.grpc_compression == true ? "gzip" : @options.grpc_compression
if compression headers["grpc-encoding"] = compression if compression
headers["grpc-encoding"] = compression
deflater = @options.encodings[compression].deflater if @options.encodings.key?(compression)
end
headers.merge!(@options.call_credentials.call) if @options.call_credentials headers.merge!(@options.call_credentials.call) if @options.call_credentials
body = if input.respond_to?(:each) build_request("POST", uri, headers: headers, body: input)
Enumerator.new do |y|
input.each do |message|
y << Message.encode(message, deflater: deflater)
end
end
else
Message.encode(input, deflater: deflater)
end
build_request("POST", uri, headers: headers, body: body)
end end
end end
end end

View 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

View File

@ -12,57 +12,25 @@ module HTTPX
# decodes a unary grpc response # decodes a unary grpc response
def unary(response) def unary(response)
verify_status(response) verify_status(response)
decode(response.to_s, encodings: response.headers.get("grpc-encoding"), encoders: response.encoders)
decoder = Transcoder::GRPCEncoding.decode(response)
decoder.call(response.to_s)
end end
# lazy decodes a grpc stream response # lazy decodes a grpc stream response
def stream(response, &block) def stream(response, &block)
return enum_for(__method__, response) unless block return enum_for(__method__, response) unless block
decoder = Transcoder::GRPCEncoding.decode(response)
response.each do |frame| response.each do |frame|
decode(frame, encodings: response.headers.get("grpc-encoding"), encoders: response.encoders, &block) decoder.call(frame, &block)
end end
verify_status(response) verify_status(response)
end end
# encodes a single grpc message
def encode(bytes, deflater:)
if deflater
compressed_flag = 1
bytes = deflater.deflate(StringIO.new(bytes))
else
compressed_flag = 0
end
"".b << [compressed_flag, bytes.bytesize].pack("CL>") << bytes.to_s
end
# decodes a single grpc message
def decode(message, encodings:, encoders:)
until message.empty?
compressed, size = message.unpack("CL>")
data = message.byteslice(5..size + 5 - 1)
if compressed == 1
encodings.reverse_each do |algo|
next unless encoders.key?(algo)
inflater = encoders[algo].inflater(size)
data = inflater.inflate(data)
size = data.bytesize
end
end
return data unless block_given?
yield data
message = message.byteslice((size + 5)..-1)
end
end
def cancel(request) def cancel(request)
request.emit(:refuse, :client_cancellation) request.emit(:refuse, :client_cancellation)
end end

View File

@ -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

View File

@ -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

View File

@ -3,12 +3,12 @@
module HTTPX module HTTPX
module Plugins module Plugins
# #
# https://gitlab.com/os85/httpx/wikis/Authentication#ntlm-authentication # https://gitlab.com/os85/httpx/wikis/Authorization#ntlm-auth
# #
module NTLMAuth module NTLMAuth
class << self class << self
def load_dependencies(_klass) def load_dependencies(_klass)
require_relative "authentication/ntlm" require_relative "auth/ntlm"
end end
def extra_options(options) def extra_options(options)
@ -25,11 +25,11 @@ module HTTPX
end end
module InstanceMethods module InstanceMethods
def ntlm_authentication(user, password, domain = nil) def ntlm_auth(user, password, domain = nil)
with(ntlm: Authentication::Ntlm.new(user, password, domain: domain)) with(ntlm: Authentication::Ntlm.new(user, password, domain: domain))
end end
alias_method :ntlm_auth, :ntlm_authentication private
def send_requests(*requests) def send_requests(*requests)
requests.flat_map do |request| requests.flat_map do |request|
@ -55,6 +55,6 @@ module HTTPX
end end
end end
end end
register_plugin :ntlm_authentication, NTLMAuth register_plugin :ntlm_auth, NTLMAuth
end end
end end

View File

@ -8,7 +8,7 @@ module HTTPX
module OAuth module OAuth
class << self class << self
def load_dependencies(_klass) def load_dependencies(_klass)
require_relative "authentication/basic" require_relative "auth/basic"
end end
end end
@ -106,7 +106,7 @@ module HTTPX
end end
module InstanceMethods module InstanceMethods
def oauth_authentication(**args) def oauth_auth(**args)
with(oauth_session: OAuthSession.new(**args)) with(oauth_session: OAuthSession.new(**args))
end end

View File

@ -28,35 +28,6 @@ module HTTPX
def extra_options(options) def extra_options(options)
options.merge(supported_proxy_protocols: []) options.merge(supported_proxy_protocols: [])
end end
if URI::Generic.methods.include?(:use_proxy?)
def use_proxy?(*args)
URI::Generic.use_proxy?(*args)
end
else
# https://github.com/ruby/uri/blob/ae07f956a4bea00b4f54a75bd40b8fa918103eed/lib/uri/generic.rb
def use_proxy?(hostname, addr, port, no_proxy)
hostname = hostname.downcase
dothostname = ".#{hostname}"
no_proxy.scan(/([^:,\s]+)(?::(\d+))?/) do |p_host, p_port|
if !p_port || port == p_port.to_i
if p_host.start_with?(".")
return false if hostname.end_with?(p_host.downcase)
else
return false if dothostname.end_with?(".#{p_host.downcase}")
end
if addr
begin
return false if IPAddr.new(p_host).include?(addr)
rescue IPAddr::InvalidAddressError
next
end
end
end
end
true
end
end
end end
class Parameters class Parameters
@ -82,7 +53,7 @@ module HTTPX
auth_scheme = scheme.to_s.capitalize auth_scheme = scheme.to_s.capitalize
require_relative "authentication/#{scheme}" unless defined?(Authentication) && Authentication.const_defined?(auth_scheme, false) require_relative "auth/#{scheme}" unless defined?(Authentication) && Authentication.const_defined?(auth_scheme, false)
@authenticator = Authentication.const_get(auth_scheme).new(@username, @password, **extra) @authenticator = Authentication.const_get(auth_scheme).new(@username, @password, **extra)
end end
@ -162,8 +133,8 @@ module HTTPX
no_proxy = proxy[:no_proxy] no_proxy = proxy[:no_proxy]
no_proxy = no_proxy.join(",") if no_proxy.is_a?(Array) no_proxy = no_proxy.join(",") if no_proxy.is_a?(Array)
return super(request, connections, options.merge(proxy: nil)) unless Proxy.use_proxy?(uri.host, next_proxy.host, return super(request, connections, options.merge(proxy: nil)) unless URI::Generic.use_proxy?(uri.host, next_proxy.host,
next_proxy.port, no_proxy) next_proxy.port, no_proxy)
end end
proxy.merge(uri: next_proxy) proxy.merge(uri: next_proxy)

View File

@ -92,10 +92,6 @@ module HTTPX
@options = Options.new(options) @options = Options.new(options)
end end
def timeout
@options.timeout[:operation_timeout]
end
def close; end def close; end
def consume(*); end def consume(*); end

View File

@ -20,7 +20,7 @@ module HTTPX
class << self class << self
def load_dependencies(*) def load_dependencies(*)
require_relative "../authentication/socks5" require_relative "../auth/socks5"
end end
def extra_options(options) def extra_options(options)
@ -144,10 +144,6 @@ module HTTPX
@options = Options.new(options) @options = Options.new(options)
end end
def timeout
@options.timeout[:operation_timeout]
end
def close; end def close; end
def consume(*); end def consume(*); end

View File

@ -88,13 +88,12 @@ module HTTPX
request.retries.positive? && request.retries.positive? &&
__repeatable_request?(request, options) && __repeatable_request?(request, options) &&
( (
# rubocop:disable Style/MultilineTernaryOperator
options.retry_on ?
options.retry_on.call(response) :
( (
response.is_a?(ErrorResponse) && __retryable_error?(response.error) response.is_a?(ErrorResponse) && __retryable_error?(response.error)
) ||
(
options.retry_on && options.retry_on.call(response)
) )
# rubocop:enable Style/MultilineTernaryOperator
) )
__try_partial_retry(request, response) __try_partial_retry(request, response)
log { "failed to get response, #{request.retries} tries to go..." } log { "failed to get response, #{request.retries} tries to go..." }

View File

@ -94,6 +94,10 @@ module HTTPX
# https://gitlab.com/honeyryderchuck/httpx/wikis/Stream # https://gitlab.com/honeyryderchuck/httpx/wikis/Stream
# #
module Stream module Stream
def self.extra_options(options)
options.merge(timeout: { read_timeout: Float::INFINITY, operation_timeout: 60 })
end
module InstanceMethods module InstanceMethods
def request(*args, stream: false, **options) def request(*args, stream: false, **options)
return super(*args, **options) unless stream return super(*args, **options) unless stream
@ -139,12 +143,6 @@ module HTTPX
super super
end end
end end
def self.const_missing(const_name)
super unless const_name == :StreamResponse
warn "DEPRECATION WARNING: the class #{self}::StreamResponse is deprecated. Use HTTPX::StreamResponse instead."
HTTPX::StreamResponse
end
end end
register_plugin :stream, Stream register_plugin :stream, Stream
end end

View File

@ -1,304 +1,22 @@
# frozen_string_literal: true # frozen_string_literal: true
module HTTPX module HTTPX
begin module Punycode
require "idnx" module_function
module Punycode begin
module_function require "idnx"
def encode_hostname(hostname) def encode_hostname(hostname)
Idnx.to_punycode(hostname) Idnx.to_punycode(hostname)
end end
end rescue LoadError
rescue LoadError
# :nocov:
# -*- coding: utf-8 -*-
#--
# punycode.rb - PunyCode encoder for the Domain Name library
#
# Copyright (C) 2011-2017 Akinori MUSHA, All rights reserved.
#
# Ported from puny.c, a part of VeriSign XCode (encode/decode) IDN
# Library.
#
# Copyright (C) 2000-2002 Verisign Inc., All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1) Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2) Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3) Neither the name of the VeriSign Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# This software is licensed under the BSD open source license. For more
# information visit www.opensource.org.
#
# Authors:
# John Colosi (VeriSign)
# Srikanth Veeramachaneni (VeriSign)
# Nagesh Chigurupati (Verisign)
# Praveen Srinivasan(Verisign)
#++
module Punycode
BASE = 36
TMIN = 1
TMAX = 26
SKEW = 38
DAMP = 700
INITIAL_BIAS = 72
INITIAL_N = 0x80
DELIMITER = "-"
MAXINT = (1 << 32) - 1
LOBASE = BASE - TMIN
CUTOFF = LOBASE * TMAX / 2
RE_NONBASIC = /[^\x00-\x7f]/.freeze
# Returns the numeric value of a basic code point (for use in
# representing integers) in the range 0 to base-1, or nil if cp
# is does not represent a value.
DECODE_DIGIT = {}.tap do |map|
# ASCII A..Z map to 0..25
# ASCII a..z map to 0..25
(0..25).each { |i| map[65 + i] = map[97 + i] = i }
# ASCII 0..9 map to 26..35
(26..35).each { |i| map[22 + i] = i }
end
# Returns the basic code point whose value (when used for
# representing integers) is d, which must be in the range 0 to
# BASE-1. The lowercase form is used unless flag is true, in
# which case the uppercase form is used. The behavior is
# undefined if flag is nonzero and digit d has no uppercase
# form.
ENCODE_DIGIT = proc { |d, flag|
(d + 22 + (d < 26 ? 75 : 0) - (flag ? (1 << 5) : 0)).chr
# 0..25 map to ASCII a..z or A..Z
# 26..35 map to ASCII 0..9
}
DOT = "."
PREFIX = "xn--"
# Most errors we raise are basically kind of ArgumentError.
class ArgumentError < ::ArgumentError; end
class BufferOverflowError < ArgumentError; end
module_function
# Encode a +string+ in Punycode
def encode(string)
input = string.unpack("U*")
output = +""
# Initialize the state
n = INITIAL_N
delta = 0
bias = INITIAL_BIAS
# Handle the basic code points
input.each { |cp| output << cp.chr if cp < 0x80 }
h = b = output.length
# h is the number of code points that have been handled, b is the
# number of basic code points, and out is the number of characters
# that have been output.
output << DELIMITER if b > 0
# Main encoding loop
while h < input.length
# All non-basic code points < n have been handled already. Find
# the next larger one
m = MAXINT
input.each do |cp|
m = cp if (n...m) === cp
end
# Increase delta enough to advance the decoder's <n,i> state to
# <m,0>, but guard against overflow
delta += (m - n) * (h + 1)
raise BufferOverflowError if delta > MAXINT
n = m
input.each do |cp|
# AMC-ACE-Z can use this simplified version instead
if cp < n
delta += 1
raise BufferOverflowError if delta > MAXINT
elsif cp == n
# Represent delta as a generalized variable-length integer
q = delta
k = BASE
loop do
t = k <= bias ? TMIN : k - bias >= TMAX ? TMAX : k - bias
break if q < t
q, r = (q - t).divmod(BASE - t)
output << ENCODE_DIGIT[t + r, false]
k += BASE
end
output << ENCODE_DIGIT[q, false]
# Adapt the bias
delta = h == b ? delta / DAMP : delta >> 1
delta += delta / (h + 1)
bias = 0
while delta > CUTOFF
delta /= LOBASE
bias += BASE
end
bias += (LOBASE + 1) * delta / (delta + SKEW)
delta = 0
h += 1
end
end
delta += 1
n += 1
end
output
end
# Encode a hostname using IDN/Punycode algorithms
def encode_hostname(hostname) def encode_hostname(hostname)
hostname.match(RE_NONBASIC) || (return hostname) warn "#{hostname} cannot be converted to punycode. Install the " \
"\"idnx\" gem: https://github.com/HoneyryderChuck/idnx"
hostname.split(DOT).map do |name| hostname
if name.match(RE_NONBASIC)
PREFIX + encode(name)
else
name
end
end.join(DOT)
end
# Decode a +string+ encoded in Punycode
def decode(string)
# Initialize the state
n = INITIAL_N
i = 0
bias = INITIAL_BIAS
if j = string.rindex(DELIMITER)
b = string[0...j]
b.match(RE_NONBASIC) &&
raise(ArgumentError, "Illegal character is found in basic part: #{string.inspect}")
# Handle the basic code points
output = b.unpack("U*")
u = string[(j + 1)..-1]
else
output = []
u = string
end
# Main decoding loop: Start just after the last delimiter if any
# basic code points were copied; start at the beginning
# otherwise.
input = u.unpack("C*")
input_length = input.length
h = 0
out = output.length
while h < input_length
# Decode a generalized variable-length integer into delta,
# which gets added to i. The overflow checking is easier
# if we increase i as we go, then subtract off its starting
# value at the end to obtain delta.
oldi = i
w = 1
k = BASE
loop do
(digit = DECODE_DIGIT[input[h]]) ||
raise(ArgumentError, "Illegal character is found in non-basic part: #{string.inspect}")
h += 1
i += digit * w
raise BufferOverflowError if i > MAXINT
t = k <= bias ? TMIN : k - bias >= TMAX ? TMAX : k - bias
break if digit < t
w *= BASE - t
raise BufferOverflowError if w > MAXINT
k += BASE
(h < input_length) || raise(ArgumentError, "Malformed input given: #{string.inspect}")
end
# Adapt the bias
delta = oldi == 0 ? i / DAMP : (i - oldi) >> 1
delta += delta / (out + 1)
bias = 0
while delta > CUTOFF
delta /= LOBASE
bias += BASE
end
bias += (LOBASE + 1) * delta / (delta + SKEW)
# i was supposed to wrap around from out+1 to 0, incrementing
# n each time, so we'll fix that now:
q, i = i.divmod(out + 1)
n += q
raise BufferOverflowError if n > MAXINT
# Insert n at position i of the output:
output[i, 0] = n
out += 1
i += 1
end
output.pack("U*")
end
# Decode a hostname using IDN/Punycode algorithms
def decode_hostname(hostname)
hostname.gsub(/(\A|#{Regexp.quote(DOT)})#{Regexp.quote(PREFIX)}([^#{Regexp.quote(DOT)}]*)/o) do
Regexp.last_match(1) << decode(Regexp.last_match(2))
end
end end
end end
# :nocov:
end end
end end

View File

@ -19,10 +19,6 @@ module HTTPX
def_delegator :@body, :empty? def_delegator :@body, :empty?
def initialize(verb, uri, options = {}) def initialize(verb, uri, options = {})
if verb.is_a?(Symbol)
warn "DEPRECATION WARNING: Using symbols for `verb` is deprecated, and will not be supported in httpx 1.0. " \
"Use \"#{verb.to_s.upcase}\" instead."
end
@verb = verb.to_s.upcase @verb = verb.to_s.upcase
@options = Options.new(options) @options = Options.new(options)
@uri = Utils.to_uri(uri) @uri = Utils.to_uri(uri)
@ -69,16 +65,6 @@ module HTTPX
:w :w
end end
if RUBY_VERSION < "2.2"
URIParser = URI::DEFAULT_PARSER
def initialize_with_escape(verb, uri, options = {})
initialize_without_escape(verb, URIParser.escape(uri.to_s), options)
end
alias_method :initialize_without_escape, :initialize
alias_method :initialize, :initialize_with_escape
end
def merge_headers(h) def merge_headers(h)
@headers = @headers.merge(h) @headers = @headers.merge(h)
end end
@ -153,100 +139,6 @@ module HTTPX
end end
# :nocov: # :nocov:
class Body < SimpleDelegator
class << self
def new(_, options)
return options.body if options.body.is_a?(self)
super
end
end
def initialize(headers, options)
@headers = headers
@body = initialize_body(options)
return if @body.nil?
@headers["content-type"] ||= @body.content_type
@headers["content-length"] = @body.bytesize unless unbounded_body?
super(@body)
end
def each(&block)
return enum_for(__method__) unless block
return if @body.nil?
body = stream(@body)
if body.respond_to?(:read)
::IO.copy_stream(body, ProcIO.new(block))
elsif body.respond_to?(:each)
body.each(&block)
else
block[body.to_s]
end
end
def rewind
return if empty?
@body.rewind if @body.respond_to?(:rewind)
end
def empty?
return true if @body.nil?
return false if chunked?
@body.bytesize.zero?
end
def bytesize
return 0 if @body.nil?
@body.bytesize
end
def stream(body)
encoded = body
encoded = Transcoder::Chunker.encode(body.enum_for(:each)) if chunked?
encoded
end
def unbounded_body?
return @unbounded_body if defined?(@unbounded_body)
@unbounded_body = !@body.nil? && (chunked? || @body.bytesize == Float::INFINITY)
end
def chunked?
@headers["transfer-encoding"] == "chunked"
end
def chunk!
@headers.add("transfer-encoding", "chunked")
end
# :nocov:
def inspect
"#<HTTPX::Request::Body:#{object_id} " \
"#{unbounded_body? ? "stream" : "@bytesize=#{bytesize}"}>"
end
# :nocov:
private
def initialize_body(options)
if options.body
Transcoder::Body.encode(options.body)
elsif options.form
Transcoder::Form.encode(options.form)
elsif options.json
Transcoder::JSON.encode(options.json)
elsif options.xml
Transcoder::Xml.encode(options.xml)
end
end
end
def transition(nextstate) def transition(nextstate)
case nextstate case nextstate
when :idle when :idle
@ -284,16 +176,7 @@ module HTTPX
def expects? def expects?
@headers["expect"] == "100-continue" && @informational_status == 100 && !@response @headers["expect"] == "100-continue" && @informational_status == 100 && !@response
end end
class ProcIO
def initialize(block)
@block = block
end
def write(data)
@block.call(data.dup)
data.bytesize
end
end
end end
end end
require_relative "request/body"

145
lib/httpx/request/body.rb Normal file
View 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

View File

@ -9,7 +9,6 @@ module HTTPX
class Resolver::HTTPS < Resolver::Resolver class Resolver::HTTPS < Resolver::Resolver
extend Forwardable extend Forwardable
using URIExtensions using URIExtensions
using StringExtensions
module DNSExtensions module DNSExtensions
refine Resolv::DNS do refine Resolv::DNS do

View File

@ -8,20 +8,12 @@ module HTTPX
extend Forwardable extend Forwardable
using URIExtensions using URIExtensions
DEFAULTS = if RUBY_VERSION < "2.2" DEFAULTS = {
{ nameserver: nil,
**Resolv::DNS::Config.default_config_hash, **Resolv::DNS::Config.default_config_hash,
packet_size: 512, packet_size: 512,
timeouts: Resolver::RESOLVE_TIMEOUT, timeouts: Resolver::RESOLVE_TIMEOUT,
} }.freeze
else
{
nameserver: nil,
**Resolv::DNS::Config.default_config_hash,
packet_size: 512,
timeouts: Resolver::RESOLVE_TIMEOUT,
}
end.freeze
DNS_PORT = 53 DNS_PORT = 53

View File

@ -95,7 +95,7 @@ module HTTPX
end end
def resolve_error(hostname, ex = nil) def resolve_error(hostname, ex = nil)
return ex if ex.is_a?(ResolveError) return ex if ex.is_a?(ResolveError) || ex.is_a?(ResolveTimeoutError)
message = ex ? ex.message : "Can't resolve #{hostname}" message = ex ? ex.message : "Can't resolve #{hostname}"
error = ResolveError.new(message) error = ResolveError.new(message)

View File

@ -92,6 +92,12 @@ module HTTPX
resolve resolve
end end
def raise_timeout_error(interval)
error = HTTPX::ResolveTimeoutError.new(interval, "timed out while waiting on select")
error.set_backtrace(caller)
on_error(error)
end
private private
def transition(nextstate) def transition(nextstate)
@ -164,6 +170,7 @@ module HTTPX
Thread.current.report_on_exception = false Thread.current.report_on_exception = false
begin begin
addrs = if resolve_timeout addrs = if resolve_timeout
Timeout.timeout(resolve_timeout) do Timeout.timeout(resolve_timeout) do
__addrinfo_resolve(hostname, scheme) __addrinfo_resolve(hostname, scheme)
end end
@ -182,16 +189,11 @@ module HTTPX
@pipe_write.putc(DONE) unless @pipe_write.closed? @pipe_write.putc(DONE) unless @pipe_write.closed?
end end
end end
rescue Timeout::Error => e
ex = ResolveTimeoutError.new(resolve_timeout, e.message)
ex.set_backtrace(ex.backtrace)
@pipe_mutex.synchronize do
families.each do |family|
@ips.unshift([family, connection, ex])
@pipe_write.putc(ERROR) unless @pipe_write.closed?
end
end
rescue StandardError => e rescue StandardError => e
if e.is_a?(Timeout::Error)
e = ResolveTimeoutError.new(resolve_timeout, e.message)
e.set_backtrace(e.backtrace)
end
@pipe_mutex.synchronize do @pipe_mutex.synchronize do
families.each do |family| families.each do |family|
@ips.unshift([family, connection, e]) @ips.unshift([family, connection, e])

View File

@ -124,199 +124,6 @@ module HTTPX
content_length == "0" content_length == "0"
end end
end end
class Body
attr_reader :encoding
def initialize(response, options)
@response = response
@headers = response.headers
@options = options
@threshold_size = options.body_threshold_size
@window_size = options.window_size
@encoding = response.content_type.charset || Encoding::BINARY
@length = 0
@buffer = nil
@state = :idle
end
def initialize_dup(other)
super
@buffer = other.instance_variable_get(:@buffer).dup
end
def closed?
@state == :closed
end
def write(chunk)
return if @state == :closed
size = chunk.bytesize
@length += size
transition
@buffer.write(chunk)
@response.emit(:chunk_received, chunk)
size
end
def read(*args)
return unless @buffer
unless @reader
rewind
@reader = @buffer
end
@reader.read(*args)
end
def bytesize
@length
end
def each
return enum_for(__method__) unless block_given?
begin
if @buffer
rewind
while (chunk = @buffer.read(@window_size))
yield(chunk.force_encoding(@encoding))
end
end
ensure
close
end
end
def filename
return unless @headers.key?("content-disposition")
Utils.get_filename(@headers["content-disposition"])
end
def to_s
case @buffer
when StringIO
begin
@buffer.string.force_encoding(@encoding)
rescue ArgumentError
@buffer.string
end
when Tempfile
rewind
content = _with_same_buffer_pos { @buffer.read }
begin
content.force_encoding(@encoding)
rescue ArgumentError # ex: unknown encoding name - utf
content
end
else
"".b
end
end
alias_method :to_str, :to_s
def empty?
@length.zero?
end
def copy_to(dest)
return unless @buffer
rewind
if dest.respond_to?(:path) && @buffer.respond_to?(:path)
FileUtils.mv(@buffer.path, dest.path)
else
::IO.copy_stream(@buffer, dest)
end
end
# closes/cleans the buffer, resets everything
def close
if @buffer
@buffer.close
@buffer.unlink if @buffer.respond_to?(:unlink)
@buffer = nil
end
@length = 0
@state = :closed
end
def ==(other)
object_id == other.object_id || begin
if other.respond_to?(:read)
_with_same_buffer_pos { FileUtils.compare_stream(@buffer, other) }
else
to_s == other.to_s
end
end
end
# :nocov:
def inspect
"#<HTTPX::Response::Body:#{object_id} " \
"@state=#{@state} " \
"@length=#{@length}>"
end
# :nocov:
def rewind
return unless @buffer
# in case there's some reading going on
@reader = nil
@buffer.rewind
end
private
def transition
case @state
when :idle
if @length > @threshold_size
@state = :buffer
@buffer = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
else
@state = :memory
@buffer = StringIO.new("".b)
end
when :memory
# @type ivar @buffer: StringIO | Tempfile
if @length > @threshold_size
aux = @buffer
@buffer = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
aux.rewind
::IO.copy_stream(aux, @buffer)
# (this looks like a bug from Ruby < 2.3
@buffer.pos = aux.pos ##################
########################################
aux.close
@state = :buffer
end
end
nil unless %i[memory buffer].include?(@state)
end
def _with_same_buffer_pos
return yield unless @buffer && @buffer.respond_to?(:pos)
# @type ivar @buffer: StringIO | Tempfile
current_pos = @buffer.pos
@buffer.rewind
begin
yield
ensure
@buffer.pos = current_pos
end
end
end
end end
class ContentType class ContentType
@ -358,20 +165,8 @@ module HTTPX
log_exception(@error) log_exception(@error)
end end
def status def to_s
warn ":#{__method__} is deprecated, use :error.message instead" @error.full_message(highlight: false)
@error.message
end
if Exception.method_defined?(:full_message)
def to_s
@error.full_message(highlight: false)
end
else
def to_s
"#{@error.message} (#{@error.class})\n" \
"#{@error.backtrace.join("\n") if @error.backtrace}"
end
end end
def close def close
@ -388,4 +183,6 @@ module HTTPX
end end
end end
require "httpx/pmatch_extensions" if RUBY_VERSION >= "3.0.0" require_relative "response/body"
require_relative "response/buffer"
require_relative "pmatch_extensions" if RUBY_VERSION >= "3.0.0"

206
lib/httpx/response/body.rb Normal file
View 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

View 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

View File

@ -9,8 +9,6 @@ class HTTPX::Selector
private_constant :READABLE private_constant :READABLE
private_constant :WRITABLE private_constant :WRITABLE
using HTTPX::IOExtensions
def initialize def initialize
@selectables = [] @selectables = []
end end

View File

@ -339,16 +339,6 @@ module HTTPX
end end
self self
end end
# :nocov:
def plugins(pls)
warn ":#{__method__} is deprecated, use :plugin instead"
pls.each do |pl|
plugin(pl)
end
self
end
# :nocov:
end end
end end

View File

@ -2,8 +2,6 @@
module HTTPX module HTTPX
module Transcoder module Transcoder
using RegexpExtensions unless Regexp.method_defined?(:match?)
module_function module_function
def normalize_keys(key, value, cond = nil, &block) def normalize_keys(key, value, cond = nil, &block)
@ -90,3 +88,5 @@ require "httpx/transcoder/form"
require "httpx/transcoder/json" require "httpx/transcoder/json"
require "httpx/transcoder/xml" require "httpx/transcoder/xml"
require "httpx/transcoder/chunker" require "httpx/transcoder/chunker"
require "httpx/transcoder/deflate"
require "httpx/transcoder/gzip"

View File

@ -9,7 +9,6 @@ module HTTPX::Transcoder
module_function module_function
class Encoder class Encoder
using HTTPX::ArrayExtensions::Sum
extend Forwardable extend Forwardable
def_delegator :@raw, :to_s def_delegator :@raw, :to_s

View 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

View File

@ -2,57 +2,77 @@
require "forwardable" require "forwardable"
require "uri" require "uri"
require_relative "multipart"
module HTTPX::Transcoder module HTTPX
module Form module Transcoder
module_function module Form
module_function
PARAM_DEPTH_LIMIT = 32 PARAM_DEPTH_LIMIT = 32
class Encoder class Encoder
extend Forwardable extend Forwardable
def_delegator :@raw, :to_s def_delegator :@raw, :to_s
def_delegator :@raw, :to_str def_delegator :@raw, :to_str
def_delegator :@raw, :bytesize def_delegator :@raw, :bytesize
def initialize(form) def initialize(form)
@raw = form.each_with_object("".b) do |(key, val), buf| @raw = form.each_with_object("".b) do |(key, val), buf|
HTTPX::Transcoder.normalize_keys(key, val) do |k, v| HTTPX::Transcoder.normalize_keys(key, val) do |k, v|
buf << "&" unless buf.empty? buf << "&" unless buf.empty?
buf << URI.encode_www_form_component(k) buf << URI.encode_www_form_component(k)
buf << "=#{URI.encode_www_form_component(v.to_s)}" unless v.nil? buf << "=#{URI.encode_www_form_component(v.to_s)}" unless v.nil?
end
end
end
def content_type
"application/x-www-form-urlencoded"
end
end
module Decoder
module_function
def call(response, *)
URI.decode_www_form(response.to_s).each_with_object({}) do |(field, value), params|
HTTPX::Transcoder.normalize_query(params, field, value, PARAM_DEPTH_LIMIT)
end end
end end
end end
def content_type def encode(form)
"application/x-www-form-urlencoded" if multipart?(form)
Multipart::Encoder.new(form)
else
Encoder.new(form)
end
end end
end
module Decoder def decode(response)
module_function content_type = response.content_type.mime_type
def call(response, *) case content_type
URI.decode_www_form(response.to_s).each_with_object({}) do |(field, value), params| when "application/x-www-form-urlencoded"
HTTPX::Transcoder.normalize_query(params, field, value, PARAM_DEPTH_LIMIT) Decoder
when "multipart/form-data"
Multipart::Decoder.new(response)
else
raise Error, "invalid form mime type (#{content_type})"
end
end
def multipart?(data)
data.any? do |_, v|
Multipart::MULTIPART_VALUE_COND.call(v) ||
(v.respond_to?(:to_ary) && v.to_ary.any?(&Multipart::MULTIPART_VALUE_COND)) ||
(v.respond_to?(:to_hash) && v.to_hash.any? { |_, e| Multipart::MULTIPART_VALUE_COND.call(e) })
end end
end end
end end
def encode(form)
Encoder.new(form)
end
def decode(response)
content_type = response.content_type.mime_type
raise HTTPX::Error, "invalid form mime type (#{content_type})" unless content_type == "application/x-www-form-urlencoded"
Decoder
end
end end
end end

View 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

View File

@ -4,12 +4,10 @@ require "forwardable"
module HTTPX::Transcoder module HTTPX::Transcoder
module JSON module JSON
JSON_REGEX = %r{\bapplication/(?:vnd\.api\+)?json\b}i.freeze
using HTTPX::RegexpExtensions unless Regexp.method_defined?(:match?)
module_function module_function
JSON_REGEX = %r{\bapplication/(?:vnd\.api\+)?json\b}i.freeze
class Encoder class Encoder
extend Forwardable extend Forwardable

View 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

View 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

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
module HTTPX::Plugins module HTTPX
module Multipart module Transcoder::Multipart
class Encoder class Encoder
attr_reader :bytesize attr_reader :bytesize
@ -43,7 +43,7 @@ module HTTPX::Plugins
def to_parts(form) def to_parts(form)
@bytesize = 0 @bytesize = 0
params = form.each_with_object([]) do |(key, val), aux| params = form.each_with_object([]) do |(key, val), aux|
Multipart.normalize_keys(key, val) do |k, v| Transcoder.normalize_keys(key, val, MULTIPART_VALUE_COND) do |k, v|
next if v.nil? next if v.nil?
value, content_type, filename = Part.call(v) value, content_type, filename = Part.call(v)

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
module HTTPX module HTTPX
module Plugins::Multipart module Transcoder::Multipart
module MimeTypeDetector module MimeTypeDetector
module_function module_function

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
module HTTPX module HTTPX
module Plugins::Multipart module Transcoder::Multipart
module Part module Part
module_function module_function
@ -21,7 +21,8 @@ module HTTPX
value = value.open(File::RDONLY) if Object.const_defined?(:Pathname) && value.is_a?(Pathname) value = value.open(File::RDONLY) if Object.const_defined?(:Pathname) && value.is_a?(Pathname)
if value.is_a?(File) if value.respond_to?(:path) && value.respond_to?(:read)
# either a File, a Tempfile, or something else which has to quack like a file
filename ||= File.basename(value.path) filename ||= File.basename(value.path)
content_type ||= MimeTypeDetector.call(value, filename) || "application/octet-stream" content_type ||= MimeTypeDetector.call(value, filename) || "application/octet-stream"
[value, content_type, filename] [value, content_type, filename]

View 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

View 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

View File

@ -6,8 +6,6 @@ require "uri"
module HTTPX::Transcoder module HTTPX::Transcoder
module Xml module Xml
using HTTPX::RegexpExtensions
module_function module_function
MIME_TYPES = %r{\b(application|text)/(.+\+)?xml\b}.freeze MIME_TYPES = %r{\b(application|text)/(.+\+)?xml\b}.freeze

View File

@ -3,7 +3,6 @@
module HTTPX module HTTPX
module Utils module Utils
using URIExtensions using URIExtensions
using HTTPX::RegexpExtensions unless Regexp.method_defined?(:match?)
TOKEN = %r{[^\s()<>,;:\\"/\[\]?=]+}.freeze TOKEN = %r{[^\s()<>,;:\\"/\[\]?=]+}.freeze
VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/.freeze VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/.freeze
@ -55,31 +54,22 @@ module HTTPX
filename filename
end end
if RUBY_VERSION < "2.3" URIParser = URI::RFC2396_Parser.new
def to_uri(uri) def to_uri(uri)
URI(uri) return URI(uri) unless uri.is_a?(String) && !uri.ascii_only?
end
else uri = URI(URIParser.escape(uri))
URIParser = URI::RFC2396_Parser.new non_ascii_hostname = URIParser.unescape(uri.host)
def to_uri(uri) non_ascii_hostname.force_encoding(Encoding::UTF_8)
return URI(uri) unless uri.is_a?(String) && !uri.ascii_only?
uri = URI(URIParser.escape(uri)) idna_hostname = Punycode.encode_hostname(non_ascii_hostname)
non_ascii_hostname = URIParser.unescape(uri.host) uri.host = idna_hostname
uri.non_ascii_hostname = non_ascii_hostname
non_ascii_hostname.force_encoding(Encoding::UTF_8) uri
idna_hostname = Punycode.encode_hostname(non_ascii_hostname)
uri.host = idna_hostname
uri.non_ascii_hostname = non_ascii_hostname
uri
end
end end
end end
end end

View File

@ -2,22 +2,6 @@
require "objspace" require "objspace"
unless ObjectSpace.method_defined?(:memsize_of_all)
module ObjectSpace
module_function
def memsize_of_all(klass = false)
total = 0
total_mem = 0
ObjectSpace.each_object(klass) do |e|
total += 1
total_mem += ObjectSpace.memsize_of(e)
end
[total, total_mem]
end
end
end
module ProfilerHelpers module ProfilerHelpers
module_function module_function

View File

@ -9,8 +9,7 @@ class Bug_0_14_1_Test < Minitest::Test
def test_multipart_can_have_arbitrary_content_type def test_multipart_can_have_arbitrary_content_type
uri = "https://#{httpbin}/post" uri = "https://#{httpbin}/post"
response = HTTPX.plugin(:multipart) response = HTTPX.post(uri, form: {
.post(uri, form: {
image: { image: {
content_type: "image/png", content_type: "image/png",
body: File.new(fixture_file_path), body: File.new(fixture_file_path),

View File

@ -9,8 +9,7 @@ class Bug_0_14_2_Test < Minitest::Test
def test_multipart_can_have_arbitrary_filename def test_multipart_can_have_arbitrary_filename
uri = "https://#{httpbin}/post" uri = "https://#{httpbin}/post"
response = HTTPX.plugin(:multipart) response = HTTPX.post(uri, form: {
.post(uri, form: {
image: { image: {
filename: "weird-al-jankovic", filename: "weird-al-jankovic",
body: File.new(fixture_file_path), body: File.new(fixture_file_path),

View File

@ -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

View File

@ -12,18 +12,17 @@ module HTTPX
def with: (options) -> Session def with: (options) -> Session
| (options) { (Session) -> void } -> void | (options) { (Session) -> void } -> void
def plugin: (:authentication, ?options) -> Plugins::sessionAuthentication def plugin: (:auth, ?options) -> Plugins::sessionAuthorization
| (:basic_authentication, ?options) -> Plugins::sessionBasicAuth | (:basic_auth, ?options) -> Plugins::sessionBasicAuth
| (:digest_authentication, ?options) -> Plugins::sessionDigestAuth | (:digest_auth, ?options) -> Plugins::sessionDigestAuth
| (:ntlm_authentication, ?options) -> Plugins::sessionNTLMAuth | (:ntlm_auth, ?options) -> Plugins::sessionNTLMAuth
| (:aws_sdk_authentication, ?options) -> Plugins::sessionAwsSdkAuthentication | (:aws_sdk_authentication, ?options) -> Plugins::sessionAwsSdkAuthentication
| (:compression, ?options) -> Session | (:brotli, ?options) -> Session
| (:cookies, ?options) -> Plugins::sessionCookies | (:cookies, ?options) -> Plugins::sessionCookies
| (:expect, ?options) -> Session | (:expect, ?options) -> Session
| (:follow_redirects, ?options) -> Plugins::sessionFollowRedirects | (:follow_redirects, ?options) -> Plugins::sessionFollowRedirects
| (:upgrade, ?options) -> Session | (:upgrade, ?options) -> Session
| (:h2c, ?options) -> Session | (:h2c, ?options) -> Session
| (:multipart, ?options) -> Session
| (:persistent, ?options) -> Plugins::sessionPersistent | (:persistent, ?options) -> Plugins::sessionPersistent
| (:proxy, ?options) -> (Plugins::sessionProxy & Plugins::httpProxy) | (:proxy, ?options) -> (Plugins::sessionProxy & Plugins::httpProxy)
| (:push_promise, ?options) -> Plugins::sessionPushPromise | (:push_promise, ?options) -> Plugins::sessionPushPromise

View File

@ -36,7 +36,6 @@ module HTTPX
@keep_alive_timeout: Numeric? @keep_alive_timeout: Numeric?
@timeout: Numeric? @timeout: Numeric?
@current_timeout: Numeric? @current_timeout: Numeric?
@total_timeout: Numeric?
@io: TCP | SSL | UNIX @io: TCP | SSL | UNIX
@parser: HTTP1 | HTTP2 | _Parser @parser: HTTP1 | HTTP2 | _Parser
@connected_at: Float @connected_at: Float

View File

@ -17,9 +17,6 @@ module HTTPX
def initialize: (Numeric timeout, String message) -> untyped def initialize: (Numeric timeout, String message) -> untyped
end end
class TotalTimeoutError < TimeoutError
end
class ConnectTimeoutError < TimeoutError class ConnectTimeoutError < TimeoutError
end end

View File

@ -11,13 +11,11 @@ module HTTPX
SETTINGS_TIMEOUT: Integer SETTINGS_TIMEOUT: Integer
DEFAULT_OPTIONS: Hash[Symbol, untyped] DEFAULT_OPTIONS: Hash[Symbol, untyped]
type timeout_type = :connect_timeout | :settings_timeout | :operation_timeout | :keep_alive_timeout | :total_timeout | :read_timeout | :write_timeout | :request_timeout type timeout_type = :connect_timeout | :settings_timeout | :operation_timeout | :keep_alive_timeout | :read_timeout | :write_timeout | :request_timeout
type timeout = Hash[timeout_type, Numeric] type timeout = Hash[timeout_type, Numeric]
def self.new: (?options) -> instance def self.new: (?options) -> instance
def self.def_option: (Symbol, ?String) -> void
| (Symbol) { (*nil) -> untyped } -> void
# headers # headers
attr_reader uri: URI? attr_reader uri: URI?
@ -48,12 +46,18 @@ module HTTPX
# transport # transport
attr_reader transport: "unix" | nil attr_reader transport: "unix" | nil
# transport_options
attr_reader transport_options: Hash[untyped, untyped]?
# addresses # addresses
attr_reader addresses: Array[ipaddr]? attr_reader addresses: Array[ipaddr]?
# supported_compression_formats
attr_reader supported_compression_formats: Array[String]
# compress_request_body
attr_reader compress_request_body: bool
# decompress_response_body
attr_reader decompress_response_body: bool
# params # params
attr_reader params: Transcoder::urlencoded_input? attr_reader params: Transcoder::urlencoded_input?
@ -124,9 +128,9 @@ module HTTPX
REQUEST_IVARS: Array[Symbol] REQUEST_IVARS: Array[Symbol]
def initialize: (?options options) -> untyped def initialize: (?options options) -> void
def __initialize__: (?options options) -> untyped def do_initialize: (?options options) -> void
end end
type options = Options | Hash[Symbol, untyped] type options = Options | Hash[Symbol, untyped]

13
sig/plugins/auth.rbs Normal file
View 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

View File

@ -5,8 +5,6 @@ module HTTPX
@user: String @user: String
@password: String @password: String
def can_authenticate?: (String? authenticate) -> boolish
def authenticate: (*untyped) -> String def authenticate: (*untyped) -> String
private private

Some files were not shown because too many files have changed in this diff Show More