httpx 1.1.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -5
- data/doc/release_notes/1_1_4.md +1 -1
- data/doc/release_notes/1_1_5.md +12 -0
- data/doc/release_notes/1_2_0.md +49 -0
- data/lib/httpx/adapters/webmock.rb +29 -8
- data/lib/httpx/altsvc.rb +57 -2
- data/lib/httpx/chainable.rb +40 -29
- data/lib/httpx/connection/http1.rb +27 -22
- data/lib/httpx/connection/http2.rb +7 -3
- data/lib/httpx/connection.rb +45 -60
- data/lib/httpx/extensions.rb +0 -15
- data/lib/httpx/options.rb +84 -27
- data/lib/httpx/plugins/aws_sigv4.rb +2 -2
- data/lib/httpx/plugins/basic_auth.rb +1 -1
- data/lib/httpx/plugins/callbacks.rb +91 -0
- data/lib/httpx/plugins/circuit_breaker.rb +2 -0
- data/lib/httpx/plugins/cookies.rb +19 -9
- data/lib/httpx/plugins/digest_auth.rb +1 -1
- data/lib/httpx/plugins/follow_redirects.rb +11 -0
- data/lib/httpx/plugins/grpc/call.rb +2 -3
- data/lib/httpx/plugins/grpc/grpc_encoding.rb +1 -0
- data/lib/httpx/plugins/grpc.rb +2 -2
- data/lib/httpx/plugins/h2c.rb +20 -8
- data/lib/httpx/plugins/proxy/socks4.rb +2 -2
- data/lib/httpx/plugins/proxy/socks5.rb +2 -2
- data/lib/httpx/plugins/proxy.rb +14 -32
- data/lib/httpx/plugins/rate_limiter.rb +1 -1
- data/lib/httpx/plugins/retries.rb +4 -0
- data/lib/httpx/plugins/ssrf_filter.rb +142 -0
- data/lib/httpx/plugins/stream.rb +9 -4
- data/lib/httpx/plugins/upgrade/h2.rb +1 -1
- data/lib/httpx/plugins/upgrade.rb +1 -1
- data/lib/httpx/plugins/webdav.rb +1 -1
- data/lib/httpx/pool.rb +32 -28
- data/lib/httpx/request/body.rb +3 -3
- data/lib/httpx/request.rb +12 -3
- data/lib/httpx/resolver/https.rb +3 -2
- data/lib/httpx/resolver/native.rb +1 -0
- data/lib/httpx/resolver/resolver.rb +17 -6
- data/lib/httpx/response.rb +1 -1
- data/lib/httpx/session.rb +13 -82
- data/lib/httpx/timers.rb +3 -10
- data/lib/httpx/transcoder.rb +1 -1
- data/lib/httpx/version.rb +1 -1
- data/sig/altsvc.rbs +33 -0
- data/sig/chainable.rbs +1 -0
- data/sig/connection/http1.rbs +2 -1
- data/sig/connection.rbs +16 -16
- data/sig/options.rbs +10 -2
- data/sig/plugins/callbacks.rbs +38 -0
- data/sig/plugins/cookies.rbs +2 -0
- data/sig/plugins/follow_redirects.rbs +2 -0
- data/sig/plugins/proxy/socks4.rbs +2 -1
- data/sig/plugins/proxy/socks5.rbs +2 -1
- data/sig/plugins/proxy.rbs +11 -1
- data/sig/plugins/stream.rbs +24 -22
- data/sig/pool.rbs +1 -3
- data/sig/resolver/resolver.rbs +3 -1
- data/sig/session.rbs +4 -4
- metadata +12 -4
data/lib/httpx/plugins/h2c.rb
CHANGED
@@ -4,9 +4,9 @@ module HTTPX
|
|
4
4
|
module Plugins
|
5
5
|
#
|
6
6
|
# This plugin adds support for upgrading a plaintext HTTP/1.1 connection to HTTP/2
|
7
|
-
# (https://
|
7
|
+
# (https://datatracker.ietf.org/doc/html/rfc7540#section-3.2)
|
8
8
|
#
|
9
|
-
# https://gitlab.com/os85/httpx/wikis/Upgrade#h2c
|
9
|
+
# https://gitlab.com/os85/httpx/wikis/Connection-Upgrade#h2c
|
10
10
|
#
|
11
11
|
module H2C
|
12
12
|
VALID_H2C_VERBS = %w[GET OPTIONS HEAD].freeze
|
@@ -73,22 +73,34 @@ module HTTPX
|
|
73
73
|
@inflight -= prev_parser.requests.size
|
74
74
|
end
|
75
75
|
|
76
|
-
|
77
|
-
@parser = H2CParser.new(@write_buffer, parser_options)
|
76
|
+
@parser = H2CParser.new(@write_buffer, @options)
|
78
77
|
set_parser_callbacks(@parser)
|
79
78
|
@inflight += 1
|
80
79
|
@parser.upgrade(request, response)
|
81
80
|
@upgrade_protocol = "h2c"
|
82
81
|
|
83
|
-
if request.options.max_concurrent_requests != @options.max_concurrent_requests
|
84
|
-
@options = @options.merge(max_concurrent_requests: nil)
|
85
|
-
end
|
86
|
-
|
87
82
|
prev_parser.requests.each do |req|
|
88
83
|
req.transition(:idle)
|
89
84
|
send(req)
|
90
85
|
end
|
91
86
|
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def send_request_to_parser(request)
|
91
|
+
super
|
92
|
+
|
93
|
+
return unless request.headers["upgrade"] == "h2c" && parser.is_a?(Connection::HTTP1)
|
94
|
+
|
95
|
+
max_concurrent_requests = parser.max_concurrent_requests
|
96
|
+
|
97
|
+
return if max_concurrent_requests == 1
|
98
|
+
|
99
|
+
parser.max_concurrent_requests = 1
|
100
|
+
request.once(:response) do
|
101
|
+
parser.max_concurrent_requests = max_concurrent_requests
|
102
|
+
end
|
103
|
+
end
|
92
104
|
end
|
93
105
|
end
|
94
106
|
register_plugin(:h2c, H2C)
|
@@ -4,7 +4,7 @@ require "resolv"
|
|
4
4
|
require "ipaddr"
|
5
5
|
|
6
6
|
module HTTPX
|
7
|
-
class Socks4Error <
|
7
|
+
class Socks4Error < HTTPProxyError; end
|
8
8
|
|
9
9
|
module Plugins
|
10
10
|
module Proxy
|
@@ -85,7 +85,7 @@ module HTTPX
|
|
85
85
|
end
|
86
86
|
|
87
87
|
class SocksParser
|
88
|
-
include Callbacks
|
88
|
+
include HTTPX::Callbacks
|
89
89
|
|
90
90
|
def initialize(buffer, options)
|
91
91
|
@buffer = buffer
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module HTTPX
|
4
|
-
class Socks5Error <
|
4
|
+
class Socks5Error < HTTPProxyError; end
|
5
5
|
|
6
6
|
module Plugins
|
7
7
|
module Proxy
|
@@ -137,7 +137,7 @@ module HTTPX
|
|
137
137
|
end
|
138
138
|
|
139
139
|
class SocksParser
|
140
|
-
include Callbacks
|
140
|
+
include HTTPX::Callbacks
|
141
141
|
|
142
142
|
def initialize(buffer, options)
|
143
143
|
@buffer = buffer
|
data/lib/httpx/plugins/proxy.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module HTTPX
|
4
|
-
class HTTPProxyError <
|
4
|
+
class HTTPProxyError < ConnectionError; end
|
5
5
|
|
6
6
|
module Plugins
|
7
7
|
#
|
@@ -143,7 +143,7 @@ module HTTPX
|
|
143
143
|
proxy = Parameters.new(**proxy_opts)
|
144
144
|
|
145
145
|
proxy_options = options.merge(proxy: proxy)
|
146
|
-
connection = pool.find_connection(uri, proxy_options) ||
|
146
|
+
connection = pool.find_connection(uri, proxy_options) || init_connection(uri, proxy_options)
|
147
147
|
unless connections.nil? || connections.include?(connection)
|
148
148
|
connections << connection
|
149
149
|
set_connection_callbacks(connection, connections, options)
|
@@ -151,19 +151,15 @@ module HTTPX
|
|
151
151
|
connection
|
152
152
|
end
|
153
153
|
|
154
|
-
def build_connection(uri, options)
|
155
|
-
proxy = options.proxy
|
156
|
-
return super unless proxy
|
157
|
-
|
158
|
-
init_connection("tcp", uri, options)
|
159
|
-
end
|
160
|
-
|
161
154
|
def fetch_response(request, connections, options)
|
162
155
|
response = super
|
163
156
|
|
164
|
-
if response.is_a?(ErrorResponse) &&
|
165
|
-
__proxy_error?(response) && !@_proxy_uris.empty?
|
157
|
+
if response.is_a?(ErrorResponse) && proxy_error?(request, response)
|
166
158
|
@_proxy_uris.shift
|
159
|
+
|
160
|
+
# return last error response if no more proxies to try
|
161
|
+
return response if @_proxy_uris.empty?
|
162
|
+
|
167
163
|
log { "failed connecting to proxy, trying next..." }
|
168
164
|
request.transition(:idle)
|
169
165
|
send_request(request, connections, options)
|
@@ -172,13 +168,7 @@ module HTTPX
|
|
172
168
|
response
|
173
169
|
end
|
174
170
|
|
175
|
-
def
|
176
|
-
return if options.proxy
|
177
|
-
|
178
|
-
super
|
179
|
-
end
|
180
|
-
|
181
|
-
def __proxy_error?(response)
|
171
|
+
def proxy_error?(_request, response)
|
182
172
|
error = response.error
|
183
173
|
case error
|
184
174
|
when NativeResolveError
|
@@ -235,14 +225,6 @@ module HTTPX
|
|
235
225
|
end
|
236
226
|
end
|
237
227
|
|
238
|
-
def send(request)
|
239
|
-
return super unless (
|
240
|
-
@options.proxy && @state != :idle && connecting?
|
241
|
-
)
|
242
|
-
|
243
|
-
(@proxy_pending ||= []) << request
|
244
|
-
end
|
245
|
-
|
246
228
|
def connecting?
|
247
229
|
return super unless @options.proxy
|
248
230
|
|
@@ -271,6 +253,12 @@ module HTTPX
|
|
271
253
|
|
272
254
|
private
|
273
255
|
|
256
|
+
def initialize_type(uri, options)
|
257
|
+
return super unless options.proxy
|
258
|
+
|
259
|
+
"tcp"
|
260
|
+
end
|
261
|
+
|
274
262
|
def connect
|
275
263
|
return super unless @options.proxy
|
276
264
|
|
@@ -278,12 +266,6 @@ module HTTPX
|
|
278
266
|
when :idle
|
279
267
|
transition(:connecting)
|
280
268
|
when :connected
|
281
|
-
if @proxy_pending
|
282
|
-
while (req = @proxy_pendind.shift)
|
283
|
-
send(req)
|
284
|
-
end
|
285
|
-
end
|
286
|
-
|
287
269
|
transition(:open)
|
288
270
|
end
|
289
271
|
end
|
@@ -9,7 +9,7 @@ module HTTPX
|
|
9
9
|
# * when the server is unavailable (503);
|
10
10
|
# * when a 3xx request comes with a "retry-after" value
|
11
11
|
#
|
12
|
-
# https://gitlab.com/os85/httpx/wikis/
|
12
|
+
# https://gitlab.com/os85/httpx/wikis/Rate-Limiter
|
13
13
|
#
|
14
14
|
module RateLimiter
|
15
15
|
class << self
|
@@ -132,6 +132,10 @@ module HTTPX
|
|
132
132
|
RETRYABLE_ERRORS.any? { |klass| ex.is_a?(klass) }
|
133
133
|
end
|
134
134
|
|
135
|
+
def proxy_error?(request, response)
|
136
|
+
super && !request.retries.positive?
|
137
|
+
end
|
138
|
+
|
135
139
|
#
|
136
140
|
# Atttempt to set the request to perform a partial range request.
|
137
141
|
# This happens if the peer server accepts byte-range requests, and
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
class ServerSideRequestForgeryError < Error; end
|
5
|
+
|
6
|
+
module Plugins
|
7
|
+
#
|
8
|
+
# This plugin adds support for preventing Server-Side Request Forgery attacks.
|
9
|
+
#
|
10
|
+
# https://gitlab.com/os85/httpx/wikis/Server-Side-Request-Forgery-Filter
|
11
|
+
#
|
12
|
+
module SsrfFilter
|
13
|
+
module IPAddrExtensions
|
14
|
+
refine IPAddr do
|
15
|
+
def prefixlen
|
16
|
+
mask_addr = @mask_addr
|
17
|
+
raise "Invalid mask" if mask_addr.zero?
|
18
|
+
|
19
|
+
mask_addr >>= 1 while (mask_addr & 0x1).zero?
|
20
|
+
|
21
|
+
length = 0
|
22
|
+
while mask_addr & 0x1 == 0x1
|
23
|
+
length += 1
|
24
|
+
mask_addr >>= 1
|
25
|
+
end
|
26
|
+
|
27
|
+
length
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
using IPAddrExtensions
|
33
|
+
|
34
|
+
# https://en.wikipedia.org/wiki/Reserved_IP_addresses
|
35
|
+
IPV4_BLACKLIST = [
|
36
|
+
IPAddr.new("0.0.0.0/8"), # Current network (only valid as source address)
|
37
|
+
IPAddr.new("10.0.0.0/8"), # Private network
|
38
|
+
IPAddr.new("100.64.0.0/10"), # Shared Address Space
|
39
|
+
IPAddr.new("127.0.0.0/8"), # Loopback
|
40
|
+
IPAddr.new("169.254.0.0/16"), # Link-local
|
41
|
+
IPAddr.new("172.16.0.0/12"), # Private network
|
42
|
+
IPAddr.new("192.0.0.0/24"), # IETF Protocol Assignments
|
43
|
+
IPAddr.new("192.0.2.0/24"), # TEST-NET-1, documentation and examples
|
44
|
+
IPAddr.new("192.88.99.0/24"), # IPv6 to IPv4 relay (includes 2002::/16)
|
45
|
+
IPAddr.new("192.168.0.0/16"), # Private network
|
46
|
+
IPAddr.new("198.18.0.0/15"), # Network benchmark tests
|
47
|
+
IPAddr.new("198.51.100.0/24"), # TEST-NET-2, documentation and examples
|
48
|
+
IPAddr.new("203.0.113.0/24"), # TEST-NET-3, documentation and examples
|
49
|
+
IPAddr.new("224.0.0.0/4"), # IP multicast (former Class D network)
|
50
|
+
IPAddr.new("240.0.0.0/4"), # Reserved (former Class E network)
|
51
|
+
IPAddr.new("255.255.255.255"), # Broadcast
|
52
|
+
].freeze
|
53
|
+
|
54
|
+
IPV6_BLACKLIST = ([
|
55
|
+
IPAddr.new("::1/128"), # Loopback
|
56
|
+
IPAddr.new("64:ff9b::/96"), # IPv4/IPv6 translation (RFC 6052)
|
57
|
+
IPAddr.new("100::/64"), # Discard prefix (RFC 6666)
|
58
|
+
IPAddr.new("2001::/32"), # Teredo tunneling
|
59
|
+
IPAddr.new("2001:10::/28"), # Deprecated (previously ORCHID)
|
60
|
+
IPAddr.new("2001:20::/28"), # ORCHIDv2
|
61
|
+
IPAddr.new("2001:db8::/32"), # Addresses used in documentation and example source code
|
62
|
+
IPAddr.new("2002::/16"), # 6to4
|
63
|
+
IPAddr.new("fc00::/7"), # Unique local address
|
64
|
+
IPAddr.new("fe80::/10"), # Link-local address
|
65
|
+
IPAddr.new("ff00::/8"), # Multicast
|
66
|
+
] + IPV4_BLACKLIST.flat_map do |ipaddr|
|
67
|
+
prefixlen = ipaddr.prefixlen
|
68
|
+
|
69
|
+
ipv4_compatible = ipaddr.ipv4_compat.mask(96 + prefixlen)
|
70
|
+
ipv4_mapped = ipaddr.ipv4_mapped.mask(80 + prefixlen)
|
71
|
+
|
72
|
+
[ipv4_compatible, ipv4_mapped]
|
73
|
+
end).freeze
|
74
|
+
|
75
|
+
class << self
|
76
|
+
def extra_options(options)
|
77
|
+
options.merge(allowed_schemes: %w[https http])
|
78
|
+
end
|
79
|
+
|
80
|
+
def unsafe_ip_address?(ipaddr)
|
81
|
+
range = ipaddr.to_range
|
82
|
+
return true if range.first != range.last
|
83
|
+
|
84
|
+
return IPV6_BLACKLIST.any? { |r| r.include?(ipaddr) } if ipaddr.ipv6?
|
85
|
+
|
86
|
+
IPV4_BLACKLIST.any? { |r| r.include?(ipaddr) } # then it's IPv4
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
module OptionsMethods
|
91
|
+
def option_allowed_schemes(value)
|
92
|
+
Array(value)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
module InstanceMethods
|
97
|
+
def send_requests(*requests)
|
98
|
+
responses = requests.map do |request|
|
99
|
+
next if @options.allowed_schemes.include?(request.uri.scheme)
|
100
|
+
|
101
|
+
error = ServerSideRequestForgeryError.new("#{request.uri} URI scheme not allowed")
|
102
|
+
error.set_backtrace(caller)
|
103
|
+
response = ErrorResponse.new(request, error, request.options)
|
104
|
+
request.emit(:response, response)
|
105
|
+
response
|
106
|
+
end
|
107
|
+
allowed_requests = requests.select { |req| responses[requests.index(req)].nil? }
|
108
|
+
allowed_responses = super(*allowed_requests)
|
109
|
+
allowed_responses.each_with_index do |res, idx|
|
110
|
+
req = allowed_requests[idx]
|
111
|
+
responses[requests.index(req)] = res
|
112
|
+
end
|
113
|
+
|
114
|
+
responses
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
module ConnectionMethods
|
119
|
+
def initialize(*)
|
120
|
+
begin
|
121
|
+
super
|
122
|
+
rescue ServerSideRequestForgeryError => e
|
123
|
+
# may raise when IPs are passed as options via :addresses
|
124
|
+
throw(:resolve_error, e)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def addresses=(addrs)
|
129
|
+
addrs = addrs.map { |addr| addr.is_a?(IPAddr) ? addr : IPAddr.new(addr) }
|
130
|
+
|
131
|
+
addrs.reject!(&SsrfFilter.method(:unsafe_ip_address?))
|
132
|
+
|
133
|
+
raise ServerSideRequestForgeryError, "#{@origin.host} has no public IP addresses" if addrs.empty?
|
134
|
+
|
135
|
+
super
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
register_plugin :ssrf_filter, SsrfFilter
|
141
|
+
end
|
142
|
+
end
|
data/lib/httpx/plugins/stream.rb
CHANGED
@@ -5,6 +5,7 @@ module HTTPX
|
|
5
5
|
def initialize(request, session)
|
6
6
|
@request = request
|
7
7
|
@session = session
|
8
|
+
@response = nil
|
8
9
|
end
|
9
10
|
|
10
11
|
def each(&block)
|
@@ -25,7 +26,7 @@ module HTTPX
|
|
25
26
|
def each_line
|
26
27
|
return enum_for(__method__) unless block_given?
|
27
28
|
|
28
|
-
line =
|
29
|
+
line = "".b
|
29
30
|
|
30
31
|
each do |chunk|
|
31
32
|
line << chunk
|
@@ -36,6 +37,8 @@ module HTTPX
|
|
36
37
|
line = line.byteslice(idx + 1..-1)
|
37
38
|
end
|
38
39
|
end
|
40
|
+
|
41
|
+
yield line unless line.empty?
|
39
42
|
end
|
40
43
|
|
41
44
|
# This is a ghost method. It's to be used ONLY internally, when processing streams
|
@@ -58,8 +61,10 @@ module HTTPX
|
|
58
61
|
private
|
59
62
|
|
60
63
|
def response
|
61
|
-
@response
|
62
|
-
|
64
|
+
return @response if @response
|
65
|
+
|
66
|
+
@request.response || begin
|
67
|
+
@response = @session.request(@request)
|
63
68
|
end
|
64
69
|
end
|
65
70
|
|
@@ -78,7 +83,7 @@ module HTTPX
|
|
78
83
|
#
|
79
84
|
# This plugin adds support for stream response (text/event-stream).
|
80
85
|
#
|
81
|
-
# https://gitlab.com/
|
86
|
+
# https://gitlab.com/os85/httpx/wikis/Stream
|
82
87
|
#
|
83
88
|
module Stream
|
84
89
|
def self.extra_options(options)
|
@@ -6,7 +6,7 @@ module HTTPX
|
|
6
6
|
# This plugin adds support for upgrading an HTTP/1.1 connection to HTTP/2
|
7
7
|
# via an Upgrade: h2 response declaration
|
8
8
|
#
|
9
|
-
# https://gitlab.com/
|
9
|
+
# https://gitlab.com/os85/httpx/wikis/Connection-Upgrade#h2
|
10
10
|
#
|
11
11
|
module H2
|
12
12
|
class << self
|
@@ -6,7 +6,7 @@ module HTTPX
|
|
6
6
|
# This plugin helps negotiating a new protocol from an HTTP/1.1 connection, via the
|
7
7
|
# Upgrade header.
|
8
8
|
#
|
9
|
-
# https://gitlab.com/
|
9
|
+
# https://gitlab.com/os85/httpx/wikis/Upgrade
|
10
10
|
#
|
11
11
|
module Upgrade
|
12
12
|
class << self
|
data/lib/httpx/plugins/webdav.rb
CHANGED
data/lib/httpx/pool.rb
CHANGED
@@ -17,8 +17,6 @@ module HTTPX
|
|
17
17
|
@timers = Timers.new
|
18
18
|
@selector = Selector.new
|
19
19
|
@connections = []
|
20
|
-
@eden_connections = []
|
21
|
-
@connected_connections = 0
|
22
20
|
end
|
23
21
|
|
24
22
|
def empty?
|
@@ -45,16 +43,15 @@ module HTTPX
|
|
45
43
|
connection.emit(:error, e)
|
46
44
|
end
|
47
45
|
rescue Exception # rubocop:disable Lint/RescueException
|
48
|
-
@connections.each(&:
|
46
|
+
@connections.each(&:force_reset)
|
49
47
|
raise
|
50
48
|
end
|
51
49
|
|
52
50
|
def close(connections = @connections)
|
53
51
|
return if connections.empty?
|
54
52
|
|
55
|
-
@eden_connections.clear
|
56
53
|
connections = connections.reject(&:inflight?)
|
57
|
-
connections.each(&:
|
54
|
+
connections.each(&:terminate)
|
58
55
|
next_tick until connections.none? { |c| c.state != :idle && @connections.include?(c) }
|
59
56
|
|
60
57
|
# close resolvers
|
@@ -68,22 +65,36 @@ module HTTPX
|
|
68
65
|
resolver.close unless resolver.closed?
|
69
66
|
end
|
70
67
|
# for https resolver
|
71
|
-
resolver_connections.each(&:
|
68
|
+
resolver_connections.each(&:terminate)
|
72
69
|
next_tick until resolver_connections.none? { |c| c.state != :idle && @connections.include?(c) }
|
73
70
|
end
|
74
71
|
|
75
72
|
def init_connection(connection, _options)
|
76
|
-
resolve_connection(connection) unless connection.family
|
77
73
|
connection.timers = @timers
|
78
|
-
connection.on(:open) do
|
79
|
-
@connected_connections += 1
|
80
|
-
end
|
81
74
|
connection.on(:activate) do
|
82
75
|
select_connection(connection)
|
83
76
|
end
|
77
|
+
connection.on(:exhausted) do
|
78
|
+
case connection.state
|
79
|
+
when :closed
|
80
|
+
connection.idling
|
81
|
+
@connections << connection
|
82
|
+
select_connection(connection)
|
83
|
+
when :closing
|
84
|
+
connection.once(:close) do
|
85
|
+
connection.idling
|
86
|
+
@connections << connection
|
87
|
+
select_connection(connection)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
84
91
|
connection.on(:close) do
|
85
92
|
unregister_connection(connection)
|
86
93
|
end
|
94
|
+
connection.on(:terminate) do
|
95
|
+
unregister_connection(connection, true)
|
96
|
+
end
|
97
|
+
resolve_connection(connection) unless connection.family
|
87
98
|
end
|
88
99
|
|
89
100
|
def deactivate(connections)
|
@@ -102,16 +113,15 @@ module HTTPX
|
|
102
113
|
connection.match?(uri, options)
|
103
114
|
end
|
104
115
|
|
105
|
-
unless conn
|
106
|
-
@eden_connections.delete_if do |connection|
|
107
|
-
is_expired = connection.expired?
|
108
|
-
conn = connection if conn.nil? && !is_expired && connection.match?(uri, options)
|
109
|
-
is_expired
|
110
|
-
end
|
116
|
+
return unless conn
|
111
117
|
|
112
|
-
|
118
|
+
case conn.state
|
119
|
+
when :closed
|
120
|
+
conn.idling
|
121
|
+
select_connection(conn)
|
122
|
+
when :closing
|
123
|
+
conn.once(:close) do
|
113
124
|
conn.idling
|
114
|
-
@connections << conn
|
115
125
|
select_connection(conn)
|
116
126
|
end
|
117
127
|
end
|
@@ -151,7 +161,7 @@ module HTTPX
|
|
151
161
|
|
152
162
|
return connection if connection.family == family
|
153
163
|
|
154
|
-
new_connection = connection.class.new(connection.
|
164
|
+
new_connection = connection.class.new(connection.origin, connection.options)
|
155
165
|
new_connection.family = family
|
156
166
|
|
157
167
|
connection.once(:tcp_open) { new_connection.force_reset }
|
@@ -218,18 +228,12 @@ module HTTPX
|
|
218
228
|
end
|
219
229
|
|
220
230
|
def register_connection(connection)
|
221
|
-
if connection.state == :open
|
222
|
-
# if open, an IO was passed upstream, therefore
|
223
|
-
# consider it connected already.
|
224
|
-
@connected_connections += 1
|
225
|
-
end
|
226
231
|
select_connection(connection)
|
227
232
|
end
|
228
233
|
|
229
|
-
def unregister_connection(connection)
|
230
|
-
@connections.delete(connection)
|
231
|
-
|
232
|
-
@connected_connections -= 1 if deselect_connection(connection)
|
234
|
+
def unregister_connection(connection, cleanup = !connection.used?)
|
235
|
+
@connections.delete(connection) if cleanup
|
236
|
+
deselect_connection(connection)
|
233
237
|
end
|
234
238
|
|
235
239
|
def select_connection(connection)
|
data/lib/httpx/request/body.rb
CHANGED
@@ -70,9 +70,9 @@ module HTTPX
|
|
70
70
|
|
71
71
|
# sets the body to yield using chunked trannsfer encoding format.
|
72
72
|
def stream(body)
|
73
|
-
|
74
|
-
|
75
|
-
|
73
|
+
return body unless chunked?
|
74
|
+
|
75
|
+
Transcoder::Chunker.encode(body.enum_for(:each))
|
76
76
|
end
|
77
77
|
|
78
78
|
# returns whether the body yields infinitely.
|
data/lib/httpx/request.rb
CHANGED
@@ -119,10 +119,19 @@ module HTTPX
|
|
119
119
|
def response=(response)
|
120
120
|
return unless response
|
121
121
|
|
122
|
-
if response.is_a?(Response) && response.status
|
123
|
-
|
124
|
-
|
122
|
+
if response.is_a?(Response) && response.status < 200
|
123
|
+
# deal with informational responses
|
124
|
+
|
125
|
+
if response.status == 100 && @headers.key?("expect")
|
126
|
+
@informational_status = response.status
|
127
|
+
return
|
128
|
+
end
|
129
|
+
|
130
|
+
# 103 Early Hints advertises resources in document to browsers.
|
131
|
+
# not very relevant for an HTTP client, discard.
|
132
|
+
return if response.status >= 103
|
125
133
|
end
|
134
|
+
|
126
135
|
@response = response
|
127
136
|
|
128
137
|
emit(:response_started, response)
|
data/lib/httpx/resolver/https.rb
CHANGED
@@ -27,7 +27,7 @@ module HTTPX
|
|
27
27
|
use_get: false,
|
28
28
|
}.freeze
|
29
29
|
|
30
|
-
def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close
|
30
|
+
def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close, :terminate
|
31
31
|
|
32
32
|
def initialize(_, options)
|
33
33
|
super
|
@@ -50,6 +50,7 @@ module HTTPX
|
|
50
50
|
if @uri_addresses.empty?
|
51
51
|
ex = ResolveError.new("Can't resolve DNS server #{@uri.host}")
|
52
52
|
ex.set_backtrace(caller)
|
53
|
+
connection.force_reset
|
53
54
|
throw(:resolve_error, ex)
|
54
55
|
end
|
55
56
|
|
@@ -67,7 +68,7 @@ module HTTPX
|
|
67
68
|
def resolver_connection
|
68
69
|
@resolver_connection ||= @pool.find_connection(@uri, @options) || begin
|
69
70
|
@building_connection = true
|
70
|
-
connection = @options.connection_class.new(
|
71
|
+
connection = @options.connection_class.new(@uri, @options.merge(ssl: { alpn_protocols: %w[h2] }))
|
71
72
|
@pool.init_connection(connection, @options)
|
72
73
|
# only explicity emit addresses if connection didn't pre-resolve, i.e. it's not an IP.
|
73
74
|
emit_addresses(connection, @family, @uri_addresses) unless connection.addresses
|
@@ -38,6 +38,8 @@ module HTTPX
|
|
38
38
|
|
39
39
|
def close; end
|
40
40
|
|
41
|
+
alias_method :terminate, :close
|
42
|
+
|
41
43
|
def closed?
|
42
44
|
true
|
43
45
|
end
|
@@ -65,20 +67,29 @@ module HTTPX
|
|
65
67
|
unless connection.state == :closed ||
|
66
68
|
# double emission check
|
67
69
|
(connection.addresses && addresses.intersect?(connection.addresses))
|
68
|
-
emit_resolved_connection(connection, addresses)
|
70
|
+
emit_resolved_connection(connection, addresses, early_resolve)
|
69
71
|
end
|
70
72
|
end
|
71
73
|
else
|
72
|
-
emit_resolved_connection(connection, addresses)
|
74
|
+
emit_resolved_connection(connection, addresses, early_resolve)
|
73
75
|
end
|
74
76
|
end
|
75
77
|
|
76
78
|
private
|
77
79
|
|
78
|
-
def emit_resolved_connection(connection, addresses)
|
79
|
-
|
80
|
-
|
81
|
-
|
80
|
+
def emit_resolved_connection(connection, addresses, early_resolve)
|
81
|
+
begin
|
82
|
+
connection.addresses = addresses
|
83
|
+
|
84
|
+
emit(:resolve, connection)
|
85
|
+
rescue StandardError => e
|
86
|
+
if early_resolve
|
87
|
+
connection.force_reset
|
88
|
+
throw(:resolve_error, e)
|
89
|
+
else
|
90
|
+
emit(:error, connection, e)
|
91
|
+
end
|
92
|
+
end
|
82
93
|
end
|
83
94
|
|
84
95
|
def early_resolve(connection, hostname: connection.origin.host)
|
data/lib/httpx/response.rb
CHANGED