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:
./spec.sh jruby 9.0.0.0
allow_failure: true
test ruby 2/4:
<<: *test_settings
only:
- master
script:
./spec.sh ruby 2.4
test ruby 2/5:
<<: *test_settings
only:
- master
script:
./spec.sh ruby 2.5
test ruby 2/6:
<<: *test_settings
only:
- master
script:
./spec.sh ruby 2.6
test ruby 2/7:
<<: *test_settings
script:

View File

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

View File

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

View File

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

27
Gemfile
View File

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

View File

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

View File

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

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'
services:
httpx:
image: jruby:9.3
image: jruby:9.4
environment:
- JRUBY_OPTS=--debug
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 "oga"
http = HTTPX.plugin(:compression).plugin(:persistent).with(timeout: { operation_timeut: 5, connect_timeout: 5})
http = HTTPX.plugin(:persistent).with(timeout: { operation_timeut: 5, connect_timeout: 5})
PAGES = (ARGV.first || 10).to_i
pages = PAGES.times.map do |page|

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

25
lib/httpx/plugins/auth.rb Normal file
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
end
def can_authenticate?(authenticate)
authenticate && /Basic .*/.match?(authenticate)
end
def authenticate(*)
"Basic #{Base64.strict_encode64("#{@user}:#{@password}")}"
end

View File

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

View File

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

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)
klass.plugin(:expect)
klass.plugin(:compression)
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)
#
# https://gitlab.com/os85/httpx/wikis/Authentication#basic-authentication
# https://gitlab.com/os85/httpx/wikis/Authorization#basic-auth
#
module BasicAuth
class << self
def load_dependencies(_klass)
require_relative "authentication/basic"
require_relative "auth/basic"
end
def configure(klass)
klass.plugin(:authentication)
klass.plugin(:auth)
end
end
module InstanceMethods
def basic_auth(user, password)
authentication(Authentication::Basic.new(user, password).authenticate)
authorization(Authentication::Basic.new(user, password).authenticate)
end
alias_method :basic_authentication, :basic_auth
end
end
register_plugin :basic_authentication, BasicAuth
register_plugin :basic_auth, BasicAuth
end
end

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
module OptionsMethods
def __initialize__(*)
def do_initialize(*)
super
return unless @headers.key?("cookie")

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

145
lib/httpx/request/body.rb Normal file
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
extend Forwardable
using URIExtensions
using StringExtensions
module DNSExtensions
refine Resolv::DNS do

View File

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

View File

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

View File

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

View File

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

206
lib/httpx/response/body.rb Normal file
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 :WRITABLE
using HTTPX::IOExtensions
def initialize
@selectables = []
end

View File

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

View File

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

View File

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

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 JSON
JSON_REGEX = %r{\bapplication/(?:vnd\.api\+)?json\b}i.freeze
using HTTPX::RegexpExtensions unless Regexp.method_defined?(:match?)
module_function
JSON_REGEX = %r{\bapplication/(?:vnd\.api\+)?json\b}i.freeze
class Encoder
extend Forwardable

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

View File

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

View File

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

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 Xml
using HTTPX::RegexpExtensions
module_function
MIME_TYPES = %r{\b(application|text)/(.+\+)?xml\b}.freeze

View File

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

View File

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

View File

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

View File

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

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
| (options) { (Session) -> void } -> void
def plugin: (:authentication, ?options) -> Plugins::sessionAuthentication
| (:basic_authentication, ?options) -> Plugins::sessionBasicAuth
| (:digest_authentication, ?options) -> Plugins::sessionDigestAuth
| (:ntlm_authentication, ?options) -> Plugins::sessionNTLMAuth
def plugin: (:auth, ?options) -> Plugins::sessionAuthorization
| (:basic_auth, ?options) -> Plugins::sessionBasicAuth
| (:digest_auth, ?options) -> Plugins::sessionDigestAuth
| (:ntlm_auth, ?options) -> Plugins::sessionNTLMAuth
| (:aws_sdk_authentication, ?options) -> Plugins::sessionAwsSdkAuthentication
| (:compression, ?options) -> Session
| (:brotli, ?options) -> Session
| (:cookies, ?options) -> Plugins::sessionCookies
| (:expect, ?options) -> Session
| (:follow_redirects, ?options) -> Plugins::sessionFollowRedirects
| (:upgrade, ?options) -> Session
| (:h2c, ?options) -> Session
| (:multipart, ?options) -> Session
| (:persistent, ?options) -> Plugins::sessionPersistent
| (:proxy, ?options) -> (Plugins::sessionProxy & Plugins::httpProxy)
| (:push_promise, ?options) -> Plugins::sessionPushPromise

View File

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

View File

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

View File

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

13
sig/plugins/auth.rbs Normal file
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
@password: String
def can_authenticate?: (String? authenticate) -> boolish
def authenticate: (*untyped) -> String
private

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