httpx 0.22.2 → 0.22.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +7 -7
- data/doc/release_notes/0_22_2.md +1 -1
- data/doc/release_notes/0_22_3.md +55 -0
- data/lib/httpx/adapters/faraday.rb +8 -3
- data/lib/httpx/connection/http2.rb +1 -1
- data/lib/httpx/connection.rb +23 -13
- data/lib/httpx/extensions.rb +1 -0
- data/lib/httpx/io/tcp.rb +1 -5
- data/lib/httpx/io/udp.rb +4 -5
- data/lib/httpx/options.rb +2 -2
- data/lib/httpx/plugins/compression/gzip.rb +1 -1
- data/lib/httpx/plugins/compression.rb +3 -3
- data/lib/httpx/plugins/grpc/message.rb +1 -1
- data/lib/httpx/plugins/multipart/decoder.rb +7 -57
- data/lib/httpx/plugins/retries.rb +1 -1
- data/lib/httpx/pool.rb +38 -2
- data/lib/httpx/resolver/https.rb +14 -1
- data/lib/httpx/resolver/native.rb +13 -21
- data/lib/httpx/resolver/resolver.rb +1 -6
- data/lib/httpx/resolver.rb +8 -0
- data/lib/httpx/response.rb +6 -0
- data/lib/httpx/utils.rb +30 -0
- data/lib/httpx/version.rb +1 -1
- data/lib/httpx.rb +6 -0
- data/sig/connection.rbs +3 -3
- data/sig/plugins/multipart.rbs +0 -2
- data/sig/pool.rbs +3 -1
- data/sig/resolver/https.rbs +3 -2
- data/sig/resolver/native.rbs +2 -1
- data/sig/resolver/resolver.rbs +3 -1
- data/sig/resolver.rbs +1 -1
- data/sig/response.rbs +2 -0
- data/sig/utils.rbs +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 39a428f7a8a6747513b56a5e2b4216569363b71baa273d1904b3805f6385e1aa
|
4
|
+
data.tar.gz: 900178247910015b23dcc199df570dadde117d3837efecc5b7100e6478054a77
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 77d450b37b3dacbc998b4a659d2ddfe80ae0fcf38cbbad79a2ab8df10522f0cc69e57979960559503bd9a00eb775b1e664f3ada81d8aaad0a1ae29765ff4868c
|
7
|
+
data.tar.gz: 40d9e2b9cb187f0610d5985128227aedc6ed2c3cde55adcb69e6abd20fd9048d96fb05357e320bdb90442bb3553cacca5aa0368e5455344034d654c902412318
|
data/README.md
CHANGED
@@ -139,13 +139,13 @@ All Rubies greater or equal to 2.1, and always latest JRuby and Truffleruby.
|
|
139
139
|
**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.
|
140
140
|
|
141
141
|
## Resources
|
142
|
-
| |
|
143
|
-
| ------------- |
|
144
|
-
| Website | https://
|
145
|
-
| Documentation | https://
|
146
|
-
| Wiki | https://
|
147
|
-
| CI | https://gitlab.com/os85/httpx/pipelines
|
148
|
-
| Rubygems | https://rubygems.org/gems/httpx
|
142
|
+
| | |
|
143
|
+
| ------------- | ------------------------------------------------------ |
|
144
|
+
| Website | https://honeyryderchuck.gitlab.io/httpx/ |
|
145
|
+
| Documentation | https://honeyryderchuck.gitlab.io/httpx/rdoc/ |
|
146
|
+
| Wiki | https://honeyryderchuck.gitlab.io/httpx/wiki/home.html |
|
147
|
+
| CI | https://gitlab.com/os85/httpx/pipelines |
|
148
|
+
| Rubygems | https://rubygems.org/gems/httpx |
|
149
149
|
|
150
150
|
## Caveats
|
151
151
|
|
data/doc/release_notes/0_22_2.md
CHANGED
@@ -0,0 +1,55 @@
|
|
1
|
+
# 0.22.3
|
2
|
+
|
3
|
+
## Features
|
4
|
+
|
5
|
+
### HTTPX::Response::Body#filename
|
6
|
+
|
7
|
+
A new method, `.filename` can be called on response bodies, to get the filename referenced by the server for the received payload (usually in the "Content-Disposition" header).
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
response = HTTPX.get(url)
|
11
|
+
response.raise_for_status
|
12
|
+
filename = response.body.filename
|
13
|
+
# you can do, for example:
|
14
|
+
response.body.copy_to("/home/files/#{filename}")
|
15
|
+
```
|
16
|
+
|
17
|
+
## Improvements
|
18
|
+
|
19
|
+
### Loading integrations by default
|
20
|
+
|
21
|
+
Integrations will be loaded by default, as long as the dependency being integrated is already available:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
require "ddtrace"
|
25
|
+
require "httpx"
|
26
|
+
|
27
|
+
HTTPX.get(... # request will be traced via the datadog integration
|
28
|
+
```
|
29
|
+
|
30
|
+
### Faraday: better error handling
|
31
|
+
|
32
|
+
The `faraday` adapter will not raise errors anymore, when used in parallel mode. This fixes the difference in behaviour with the equivalent `typhoeus` parallel adapter, which does not raise errors in such cases as well. This behaviour will exclude 4xx and 5xx HTTP responses, which will not be considered errors in the `faraday` adapter.
|
33
|
+
|
34
|
+
If errors occur in parallel mode, these'll be available in `env[:error]`. Users can check it in two ways:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
response.status == 0
|
38
|
+
# or
|
39
|
+
!response.env[:error].nil?
|
40
|
+
```
|
41
|
+
|
42
|
+
## Bugfixes
|
43
|
+
|
44
|
+
* unix socket: handle the error when the path for the unix sock is invalid, which was causing an endless loop.
|
45
|
+
|
46
|
+
### IPv6 / Happy eyeballs v2
|
47
|
+
|
48
|
+
* the `native` resolver will now use IPv6 nameservers with zone identifier to perform DNS queries. This bug was being ignored prior to ruby 3.1 due to some pre-filtering on the nameservere which were covering misuse of the `uri` dependency for this use case.
|
49
|
+
* Happy Eyeballs v2 handshake error on connection establishment for the first IP will now ignore it, in case an ongoing connecting for the second IP is happening. This fixes a case where both IPv4 and IPv6 addresses are served for a given domain, but only one of them can be connected to (i.e. if connection via IPv6 fails, the IPv4 one should still proceed to completion).
|
50
|
+
* the `native` resolver won't try querying DNS name candidates, if the resolver sends an empty answer with an error code different from "domain not found".
|
51
|
+
* fix error of Happy Eyeballs v2 handshake, where the resulting connection would coalesce with an already open one for the same IP **before** requests were merged to the coalesced connection, resulting in no requests being sent and the client hanging.
|
52
|
+
|
53
|
+
## Chore
|
54
|
+
|
55
|
+
* fixed error message on wrong type of parameter for the `compression_threshold_size` option from the `:compression` plugin.
|
@@ -210,9 +210,13 @@ module Faraday
|
|
210
210
|
if parallel?(env)
|
211
211
|
handler = env[:parallel_manager].enqueue(env)
|
212
212
|
handler.on_response do |response|
|
213
|
-
response.
|
214
|
-
|
215
|
-
|
213
|
+
if response.is_a?(::HTTPX::Response)
|
214
|
+
save_response(env, response.status, response.body.to_s, response.headers, response.reason) do |response_headers|
|
215
|
+
response_headers.merge!(response.headers)
|
216
|
+
end
|
217
|
+
else
|
218
|
+
env[:error] = response.error
|
219
|
+
save_response(env, 0, "", {}, nil)
|
216
220
|
end
|
217
221
|
end
|
218
222
|
return handler
|
@@ -229,6 +233,7 @@ module Faraday
|
|
229
233
|
request.response_on_data = env.request.on_data if env.request.stream_response?
|
230
234
|
|
231
235
|
response = session.request(request)
|
236
|
+
# do not call #raise_for_status for HTTP 4xx or 5xx, as faraday has a middleware for that.
|
232
237
|
response.raise_for_status unless response.is_a?(::HTTPX::Response)
|
233
238
|
save_response(env, response.status, response.body.to_s, response.headers, response.reason) do |response_headers|
|
234
239
|
response_headers.merge!(response.headers)
|
data/lib/httpx/connection.rb
CHANGED
@@ -45,10 +45,12 @@ module HTTPX
|
|
45
45
|
|
46
46
|
def_delegator :@write_buffer, :empty?
|
47
47
|
|
48
|
-
attr_reader :io, :origin, :origins, :state, :pending, :options
|
48
|
+
attr_reader :type, :io, :origin, :origins, :state, :pending, :options
|
49
49
|
|
50
50
|
attr_writer :timers
|
51
51
|
|
52
|
+
attr_accessor :family
|
53
|
+
|
52
54
|
def initialize(type, uri, options)
|
53
55
|
@type = type
|
54
56
|
@origins = [uri.origin]
|
@@ -76,13 +78,6 @@ module HTTPX
|
|
76
78
|
self.addresses = @options.addresses if @options.addresses
|
77
79
|
end
|
78
80
|
|
79
|
-
def clone_new_connection
|
80
|
-
new_conn = self.class.new(@type, @origin, @options)
|
81
|
-
once(:open, &new_conn.method(:reset))
|
82
|
-
new_conn.once(:open, &method(:close))
|
83
|
-
new_conn
|
84
|
-
end
|
85
|
-
|
86
81
|
# this is a semi-private method, to be used by the resolver
|
87
82
|
# to initiate the io object.
|
88
83
|
def addresses=(addrs)
|
@@ -121,7 +116,10 @@ module HTTPX
|
|
121
116
|
|
122
117
|
return false unless connection.addresses
|
123
118
|
|
124
|
-
|
119
|
+
(
|
120
|
+
(open? && @origin == connection.origin) ||
|
121
|
+
!(@io.addresses & connection.addresses).empty?
|
122
|
+
) && @options == connection.options
|
125
123
|
end
|
126
124
|
|
127
125
|
# coalescable connections need to be mergeable!
|
@@ -226,6 +224,14 @@ module HTTPX
|
|
226
224
|
@parser.close if @parser
|
227
225
|
end
|
228
226
|
|
227
|
+
# bypasses the state machine to force closing of connections still connecting.
|
228
|
+
# **only** used for Happy Eyeballs v2.
|
229
|
+
def force_reset
|
230
|
+
@state = :closing
|
231
|
+
transition(:closed)
|
232
|
+
emit(:close)
|
233
|
+
end
|
234
|
+
|
229
235
|
def reset
|
230
236
|
transition(:closing)
|
231
237
|
transition(:closed)
|
@@ -316,7 +322,7 @@ module HTTPX
|
|
316
322
|
# * the number of inflight requests
|
317
323
|
# * the number of pending requests
|
318
324
|
# * whether the write buffer has bytes (i.e. for close handshake)
|
319
|
-
if @pending.
|
325
|
+
if @pending.empty? && @inflight.zero? && @write_buffer.empty?
|
320
326
|
log(level: 3) { "NO MORE REQUESTS..." }
|
321
327
|
return
|
322
328
|
end
|
@@ -360,7 +366,7 @@ module HTTPX
|
|
360
366
|
break if @state == :closing || @state == :closed
|
361
367
|
|
362
368
|
# exit #consume altogether if all outstanding requests have been dealt with
|
363
|
-
return if @pending.
|
369
|
+
return if @pending.empty? && @inflight.zero?
|
364
370
|
end unless ((ints = interests).nil? || ints == :w || @state == :closing) && !epiped
|
365
371
|
|
366
372
|
#
|
@@ -526,11 +532,13 @@ module HTTPX
|
|
526
532
|
Errno::EHOSTUNREACH,
|
527
533
|
Errno::EINVAL,
|
528
534
|
Errno::ENETUNREACH,
|
529
|
-
Errno::EPIPE
|
535
|
+
Errno::EPIPE,
|
536
|
+
Errno::ENOENT,
|
537
|
+
SocketError => e
|
530
538
|
# connect errors, exit gracefully
|
531
539
|
error = ConnectionError.new(e.message)
|
532
540
|
error.set_backtrace(e.backtrace)
|
533
|
-
handle_error(error)
|
541
|
+
connecting? && callbacks(:connect_error).any? ? emit(:connect_error, error) : handle_error(error)
|
534
542
|
@state = :closed
|
535
543
|
emit(:close)
|
536
544
|
rescue TLSError => e
|
@@ -549,6 +557,8 @@ module HTTPX
|
|
549
557
|
return if @state == :closed
|
550
558
|
|
551
559
|
@io.connect
|
560
|
+
emit(:tcp_open, self) if @io.state == :connected
|
561
|
+
|
552
562
|
return unless @io.connected?
|
553
563
|
|
554
564
|
@connected_at = Utils.now
|
data/lib/httpx/extensions.rb
CHANGED
data/lib/httpx/io/tcp.rb
CHANGED
@@ -76,11 +76,7 @@ module HTTPX
|
|
76
76
|
Errno::EADDRNOTAVAIL,
|
77
77
|
Errno::EHOSTUNREACH,
|
78
78
|
SocketError => e
|
79
|
-
if @ip_index <= 0
|
80
|
-
error = ConnectionError.new(e.message)
|
81
|
-
error.set_backtrace(e.backtrace)
|
82
|
-
raise error
|
83
|
-
end
|
79
|
+
raise e if @ip_index <= 0
|
84
80
|
|
85
81
|
log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
|
86
82
|
@ip_index -= 1
|
data/lib/httpx/io/udp.rb
CHANGED
@@ -6,11 +6,10 @@ module HTTPX
|
|
6
6
|
class UDP
|
7
7
|
include Loggable
|
8
8
|
|
9
|
-
def initialize(
|
10
|
-
|
11
|
-
@
|
12
|
-
@
|
13
|
-
@io = UDPSocket.new(ip.family)
|
9
|
+
def initialize(ip, port, options)
|
10
|
+
@host = ip
|
11
|
+
@port = port
|
12
|
+
@io = UDPSocket.new(IPAddr.new(ip).family)
|
14
13
|
@options = options
|
15
14
|
end
|
16
15
|
|
data/lib/httpx/options.rb
CHANGED
@@ -15,7 +15,7 @@ module HTTPX
|
|
15
15
|
# https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
|
16
16
|
ip_address_families = begin
|
17
17
|
list = Socket.ip_address_list
|
18
|
-
if list.any? { |a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
|
18
|
+
if list.any? { |a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? && !a.ipv6_unique_local? }
|
19
19
|
[Socket::AF_INET6, Socket::AF_INET]
|
20
20
|
else
|
21
21
|
[Socket::AF_INET]
|
@@ -100,7 +100,7 @@ module HTTPX
|
|
100
100
|
end
|
101
101
|
|
102
102
|
def def_option(optname, *args, &block)
|
103
|
-
if args.
|
103
|
+
if args.empty? && !block
|
104
104
|
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
105
105
|
def option_#{optname}(v); v; end # def option_smth(v); v; end
|
106
106
|
OUT
|
@@ -30,7 +30,7 @@ module HTTPX
|
|
30
30
|
module OptionsMethods
|
31
31
|
def option_compression_threshold_size(value)
|
32
32
|
bytes = Integer(value)
|
33
|
-
raise TypeError, ":
|
33
|
+
raise TypeError, ":compression_threshold_size must be positive" unless bytes.positive?
|
34
34
|
|
35
35
|
bytes
|
36
36
|
end
|
@@ -138,7 +138,7 @@ module HTTPX
|
|
138
138
|
def each(&blk)
|
139
139
|
return enum_for(__method__) unless blk
|
140
140
|
|
141
|
-
return deflate(&blk) if @buffer.size.zero?
|
141
|
+
return deflate(&blk) if @buffer.size.zero? # rubocop:disable Style/ZeroLengthPredicate
|
142
142
|
|
143
143
|
@buffer.rewind
|
144
144
|
@buffer.each(&blk)
|
@@ -152,7 +152,7 @@ module HTTPX
|
|
152
152
|
private
|
153
153
|
|
154
154
|
def deflate(&blk)
|
155
|
-
return unless @buffer.size.zero?
|
155
|
+
return unless @buffer.size.zero? # rubocop:disable Style/ZeroLengthPredicate
|
156
156
|
|
157
157
|
@body.rewind
|
158
158
|
@deflater.deflate(@body, @buffer, chunk_size: 16_384, &blk)
|
@@ -5,10 +5,6 @@ require "delegate"
|
|
5
5
|
|
6
6
|
module HTTPX::Plugins
|
7
7
|
module Multipart
|
8
|
-
using HTTPX::RegexpExtensions unless Regexp.method_defined?(:match?)
|
9
|
-
|
10
|
-
CRLF = "\r\n"
|
11
|
-
|
12
8
|
class FilePart < SimpleDelegator
|
13
9
|
attr_reader :original_filename, :content_type
|
14
10
|
|
@@ -20,32 +16,14 @@ module HTTPX::Plugins
|
|
20
16
|
end
|
21
17
|
end
|
22
18
|
|
23
|
-
TOKEN = %r{[^\s()<>,;:\\"/\[\]?=]+}.freeze
|
24
|
-
VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/.freeze
|
25
|
-
CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i.freeze
|
26
|
-
BROKEN_QUOTED = /^#{CONDISP}.*;\s*filename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i.freeze
|
27
|
-
BROKEN_UNQUOTED = /^#{CONDISP}.*;\s*filename=(#{TOKEN})/i.freeze
|
28
|
-
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{CRLF}/ni.freeze
|
29
|
-
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni.freeze
|
30
|
-
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{CRLF}]*)/ni.freeze
|
31
|
-
# Updated definitions from RFC 2231
|
32
|
-
ATTRIBUTE_CHAR = %r{[^ \t\v\n\r)(><@,;:\\"/\[\]?='*%]}.freeze
|
33
|
-
ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/.freeze
|
34
|
-
SECTION = /\*[0-9]+/.freeze
|
35
|
-
REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/.freeze
|
36
|
-
REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/.freeze
|
37
|
-
EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/.freeze
|
38
|
-
EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/.freeze
|
39
|
-
EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/.freeze
|
40
|
-
EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/.freeze
|
41
|
-
EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9-]*'[a-zA-Z0-9-]*'#{EXTENDED_OTHER_VALUE}*/.freeze
|
42
|
-
EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/.freeze
|
43
|
-
EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/.freeze
|
44
|
-
DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/.freeze
|
45
|
-
RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i.freeze
|
46
|
-
|
47
19
|
class Decoder
|
20
|
+
include HTTPX::Utils
|
21
|
+
|
22
|
+
CRLF = "\r\n"
|
48
23
|
BOUNDARY_RE = /;\s*boundary=([^;]+)/i.freeze
|
24
|
+
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{CRLF}/ni.freeze
|
25
|
+
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni.freeze
|
26
|
+
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{CRLF}]*)/ni.freeze
|
49
27
|
WINDOW_SIZE = 2 << 14
|
50
28
|
|
51
29
|
def initialize(response)
|
@@ -102,7 +80,7 @@ module HTTPX::Plugins
|
|
102
80
|
name = head[MULTIPART_CONTENT_ID, 1]
|
103
81
|
end
|
104
82
|
|
105
|
-
filename = get_filename(head)
|
83
|
+
filename = HTTPX::Utils.get_filename(head)
|
106
84
|
|
107
85
|
name = filename || +"#{content_type || "text/plain"}[]" if name.nil? || name.empty?
|
108
86
|
|
@@ -154,34 +132,6 @@ module HTTPX::Plugins
|
|
154
132
|
raise Error, "parsing should have been over by now"
|
155
133
|
end until @buffer.empty?
|
156
134
|
end
|
157
|
-
|
158
|
-
def get_filename(head)
|
159
|
-
filename = nil
|
160
|
-
case head
|
161
|
-
when RFC2183
|
162
|
-
params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
|
163
|
-
|
164
|
-
if (filename = params["filename"])
|
165
|
-
filename = Regexp.last_match(1) if filename =~ /^"(.*)"$/
|
166
|
-
elsif (filename = params["filename*"])
|
167
|
-
encoding, _, filename = filename.split("'", 3)
|
168
|
-
end
|
169
|
-
when BROKEN_QUOTED, BROKEN_UNQUOTED
|
170
|
-
filename = Regexp.last_match(1)
|
171
|
-
end
|
172
|
-
|
173
|
-
return unless filename
|
174
|
-
|
175
|
-
filename = URI::DEFAULT_PARSER.unescape(filename) if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
|
176
|
-
|
177
|
-
filename.scrub!
|
178
|
-
|
179
|
-
filename = filename.gsub(/\\(.)/, '\1') unless /\\[^\\"]/.match?(filename)
|
180
|
-
|
181
|
-
filename.force_encoding ::Encoding.find(encoding) if encoding
|
182
|
-
|
183
|
-
filename
|
184
|
-
end
|
185
135
|
end
|
186
136
|
end
|
187
137
|
end
|
@@ -26,7 +26,7 @@ module HTTPX
|
|
26
26
|
ConnectionError,
|
27
27
|
Connection::HTTP2::GoawayError,
|
28
28
|
].freeze
|
29
|
-
DEFAULT_JITTER = ->(interval) { interval * (
|
29
|
+
DEFAULT_JITTER = ->(interval) { interval * ((rand + 1) * 0.5) }
|
30
30
|
|
31
31
|
if ENV.key?("HTTPX_NO_JITTER")
|
32
32
|
def self.extra_options(options)
|
data/lib/httpx/pool.rb
CHANGED
@@ -72,7 +72,7 @@ module HTTPX
|
|
72
72
|
end
|
73
73
|
|
74
74
|
def init_connection(connection, _options)
|
75
|
-
resolve_connection(connection)
|
75
|
+
resolve_connection(connection) unless connection.family
|
76
76
|
connection.timers = @timers
|
77
77
|
connection.on(:open) do
|
78
78
|
@connected_connections += 1
|
@@ -116,18 +116,53 @@ module HTTPX
|
|
116
116
|
# resolve a name (not the same as name being an IP, yet)
|
117
117
|
# 2. when the connection is initialized with an external already open IO.
|
118
118
|
#
|
119
|
+
connection.once(:connect_error, &connection.method(:handle_error))
|
119
120
|
on_resolver_connection(connection)
|
120
121
|
return
|
121
122
|
end
|
122
123
|
|
123
124
|
find_resolver_for(connection) do |resolver|
|
124
|
-
resolver << connection
|
125
|
+
resolver << try_clone_connection(connection, resolver.family)
|
125
126
|
next if resolver.empty?
|
126
127
|
|
127
128
|
select_connection(resolver)
|
128
129
|
end
|
129
130
|
end
|
130
131
|
|
132
|
+
def try_clone_connection(connection, family)
|
133
|
+
connection.family ||= family
|
134
|
+
|
135
|
+
return connection if connection.family == family
|
136
|
+
|
137
|
+
new_connection = connection.class.new(connection.type, connection.origin, connection.options)
|
138
|
+
new_connection.family = family
|
139
|
+
|
140
|
+
connection.once(:tcp_open) { new_connection.force_reset }
|
141
|
+
connection.once(:connect_error) do |err|
|
142
|
+
if new_connection.connecting?
|
143
|
+
new_connection.merge(connection)
|
144
|
+
else
|
145
|
+
connection.handle_error(err)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
new_connection.once(:tcp_open) do |new_conn|
|
150
|
+
new_conn.merge(connection)
|
151
|
+
connection.force_reset
|
152
|
+
end
|
153
|
+
new_connection.once(:connect_error) do |err|
|
154
|
+
if connection.connecting?
|
155
|
+
# main connection has the requests
|
156
|
+
connection.merge(new_connection)
|
157
|
+
else
|
158
|
+
new_connection.handle_error(err)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
init_connection(new_connection, connection.options)
|
163
|
+
new_connection
|
164
|
+
end
|
165
|
+
|
131
166
|
def on_resolver_connection(connection)
|
132
167
|
@connections << connection unless @connections.include?(connection)
|
133
168
|
found_connection = @connections.find do |ch|
|
@@ -187,6 +222,7 @@ module HTTPX
|
|
187
222
|
def coalesce_connections(conn1, conn2)
|
188
223
|
return register_connection(conn2) unless conn1.coalescable?(conn2)
|
189
224
|
|
225
|
+
conn2.emit(:tcp_open, conn1)
|
190
226
|
conn1.merge(conn2)
|
191
227
|
@connections.delete(conn2)
|
192
228
|
end
|
data/lib/httpx/resolver/https.rb
CHANGED
@@ -136,9 +136,22 @@ module HTTPX
|
|
136
136
|
emit_resolve_error(connection, connection.origin.host, e)
|
137
137
|
return
|
138
138
|
end
|
139
|
-
|
139
|
+
|
140
|
+
if answers.nil?
|
141
|
+
# Indicates no such domain was found.
|
142
|
+
|
140
143
|
host = @requests.delete(request)
|
141
144
|
connection = @queries.delete(host)
|
145
|
+
|
146
|
+
emit_resolve_error(connection) unless @queries.value?(connection)
|
147
|
+
elsif answers.empty?
|
148
|
+
# no address found, eliminate candidates
|
149
|
+
host = @requests.delete(request)
|
150
|
+
connection = @queries.delete(host)
|
151
|
+
|
152
|
+
# eliminate other candidates
|
153
|
+
@queries.delete_if { |_, conn| connection == conn }
|
154
|
+
|
142
155
|
emit_resolve_error(connection)
|
143
156
|
return
|
144
157
|
|
@@ -21,21 +21,7 @@ module HTTPX
|
|
21
21
|
packet_size: 512,
|
22
22
|
timeouts: Resolver::RESOLVE_TIMEOUT,
|
23
23
|
}
|
24
|
-
end
|
25
|
-
|
26
|
-
# nameservers for ipv6 are misconfigured in certain systems;
|
27
|
-
# this can use an unexpected endless loop
|
28
|
-
# https://gitlab.com/honeyryderchuck/httpx/issues/56
|
29
|
-
DEFAULTS[:nameserver].select! do |nameserver|
|
30
|
-
begin
|
31
|
-
IPAddr.new(nameserver)
|
32
|
-
true
|
33
|
-
rescue IPAddr::InvalidAddressError
|
34
|
-
false
|
35
|
-
end
|
36
|
-
end if DEFAULTS[:nameserver]
|
37
|
-
|
38
|
-
DEFAULTS.freeze
|
24
|
+
end.freeze
|
39
25
|
|
40
26
|
DNS_PORT = 53
|
41
27
|
|
@@ -215,7 +201,8 @@ module HTTPX
|
|
215
201
|
raise ex
|
216
202
|
end
|
217
203
|
|
218
|
-
if addresses.nil?
|
204
|
+
if addresses.nil?
|
205
|
+
# Indicates no such domain was found.
|
219
206
|
hostname, connection = @queries.first
|
220
207
|
@queries.delete(hostname)
|
221
208
|
@timeouts.delete(hostname)
|
@@ -224,6 +211,14 @@ module HTTPX
|
|
224
211
|
@connections.delete(connection)
|
225
212
|
raise NativeResolveError.new(connection, connection.origin.host)
|
226
213
|
end
|
214
|
+
elsif addresses.empty?
|
215
|
+
# no address found, eliminate candidates
|
216
|
+
_, connection = @queries.first
|
217
|
+
candidates = @queries.select { |_, conn| connection == conn }.keys
|
218
|
+
@queries.delete_if { |hs, _| candidates.include?(hs) }
|
219
|
+
@timeouts.delete_if { |hs, _| candidates.include?(hs) }
|
220
|
+
@connections.delete(connection)
|
221
|
+
raise NativeResolveError.new(connection, connection.origin.host)
|
227
222
|
else
|
228
223
|
address = addresses.first
|
229
224
|
name = address["name"]
|
@@ -309,11 +304,8 @@ module HTTPX
|
|
309
304
|
|
310
305
|
ip, port = @nameserver[@ns_index]
|
311
306
|
port ||= DNS_PORT
|
312
|
-
|
313
|
-
|
314
|
-
type = IO.registry(uri.scheme)
|
315
|
-
log { "resolver: server: #{uri}..." }
|
316
|
-
@io = type.new(uri, [IPAddr.new(ip)], @options)
|
307
|
+
log { "resolver: server: #{ip}:#{port}..." }
|
308
|
+
@io = UDP.new(ip, port, @options)
|
317
309
|
end
|
318
310
|
|
319
311
|
def transition(nextstate)
|
@@ -54,7 +54,7 @@ module HTTPX
|
|
54
54
|
# double emission check
|
55
55
|
return if connection.addresses && !addresses.intersect?(connection.addresses)
|
56
56
|
|
57
|
-
log { "resolver: answer #{connection.origin.host}: #{addresses.inspect}" }
|
57
|
+
log { "resolver: answer #{FAMILY_TYPES[RECORD_TYPES[family]]} #{connection.origin.host}: #{addresses.inspect}" }
|
58
58
|
if @pool && # if triggered by early resolve, pool may not be here yet
|
59
59
|
!connection.io &&
|
60
60
|
connection.options.ip_families.size > 1 &&
|
@@ -73,11 +73,6 @@ module HTTPX
|
|
73
73
|
private
|
74
74
|
|
75
75
|
def emit_resolved_connection(connection, addresses)
|
76
|
-
if connection.io && connection.connecting? && @pool
|
77
|
-
new_connection = connection.clone_new_connection
|
78
|
-
@pool.init_connection(new_connection, connection.options)
|
79
|
-
connection = new_connection
|
80
|
-
end
|
81
76
|
connection.addresses = addresses
|
82
77
|
|
83
78
|
emit(:resolve, connection)
|
data/lib/httpx/resolver.rb
CHANGED
@@ -108,7 +108,15 @@ module HTTPX
|
|
108
108
|
|
109
109
|
def decode_dns_answer(payload)
|
110
110
|
message = Resolv::DNS::Message.decode(payload)
|
111
|
+
|
112
|
+
# no domain was found
|
113
|
+
return if message.rcode == Resolv::DNS::RCode::NXDomain
|
114
|
+
|
111
115
|
addresses = []
|
116
|
+
|
117
|
+
# TODO: raise an "other dns OtherResolvError" type of error
|
118
|
+
return addresses if message.rcode != Resolv::DNS::RCode::NoError
|
119
|
+
|
112
120
|
message.each_answer do |question, _, value|
|
113
121
|
case value
|
114
122
|
when Resolv::DNS::Resource::IN::CNAME
|
data/lib/httpx/response.rb
CHANGED
data/lib/httpx/utils.rb
CHANGED
@@ -3,6 +3,12 @@
|
|
3
3
|
module HTTPX
|
4
4
|
module Utils
|
5
5
|
using URIExtensions
|
6
|
+
using HTTPX::RegexpExtensions unless Regexp.method_defined?(:match?)
|
7
|
+
|
8
|
+
TOKEN = %r{[^\s()<>,;:\\"/\[\]?=]+}.freeze
|
9
|
+
VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/.freeze
|
10
|
+
FILENAME_REGEX = /\s*filename=(#{VALUE})/.freeze
|
11
|
+
FILENAME_EXTENSION_REGEX = /\s*filename\*=(#{VALUE})/.freeze
|
6
12
|
|
7
13
|
module_function
|
8
14
|
|
@@ -25,6 +31,30 @@ module HTTPX
|
|
25
31
|
time - Time.now
|
26
32
|
end
|
27
33
|
|
34
|
+
def get_filename(header, _prefix_regex = nil)
|
35
|
+
filename = nil
|
36
|
+
case header
|
37
|
+
when FILENAME_REGEX
|
38
|
+
filename = Regexp.last_match(1)
|
39
|
+
filename = Regexp.last_match(1) if filename =~ /^"(.*)"$/
|
40
|
+
when FILENAME_EXTENSION_REGEX
|
41
|
+
filename = Regexp.last_match(1)
|
42
|
+
encoding, _, filename = filename.split("'", 3)
|
43
|
+
end
|
44
|
+
|
45
|
+
return unless filename
|
46
|
+
|
47
|
+
filename = URI::DEFAULT_PARSER.unescape(filename) if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
|
48
|
+
|
49
|
+
filename.scrub!
|
50
|
+
|
51
|
+
filename = filename.gsub(/\\(.)/, '\1') unless /\\[^\\"]/.match?(filename)
|
52
|
+
|
53
|
+
filename.force_encoding ::Encoding.find(encoding) if encoding
|
54
|
+
|
55
|
+
filename
|
56
|
+
end
|
57
|
+
|
28
58
|
if RUBY_VERSION < "2.3"
|
29
59
|
|
30
60
|
def to_uri(uri)
|
data/lib/httpx/version.rb
CHANGED
data/lib/httpx.rb
CHANGED
@@ -67,3 +67,9 @@ end
|
|
67
67
|
|
68
68
|
require "httpx/session"
|
69
69
|
require "httpx/session_extensions"
|
70
|
+
|
71
|
+
# load integrations when possible
|
72
|
+
|
73
|
+
require "httpx/adapters/datadog" if defined?(DDTrace) || defined?(Datadog)
|
74
|
+
require "httpx/adapters/sentry" if defined?(Sentry)
|
75
|
+
require "httpx/adapters/webmock" if defined?(WebMock)
|
data/sig/connection.rbs
CHANGED
@@ -21,6 +21,7 @@ module HTTPX
|
|
21
21
|
|
22
22
|
BUFFER_SIZE: Integer
|
23
23
|
|
24
|
+
attr_reader type: io_type
|
24
25
|
attr_reader origin: URI::Generic
|
25
26
|
attr_reader origins: Array[String]
|
26
27
|
attr_reader state: Symbol
|
@@ -28,7 +29,8 @@ module HTTPX
|
|
28
29
|
attr_reader options: Options
|
29
30
|
attr_writer timers: Timers
|
30
31
|
|
31
|
-
|
32
|
+
attr_accessor family: Integer?
|
33
|
+
|
32
34
|
@window_size: Integer
|
33
35
|
@read_buffer: Buffer
|
34
36
|
@write_buffer: Buffer
|
@@ -36,8 +38,6 @@ module HTTPX
|
|
36
38
|
@keep_alive_timeout: Numeric?
|
37
39
|
@total_timeout: Numeric?
|
38
40
|
|
39
|
-
def clone_new_connection: () -> instance
|
40
|
-
|
41
41
|
def addresses: () -> Array[ipaddr]?
|
42
42
|
|
43
43
|
def addresses=: (Array[ipaddr]) -> void
|
data/sig/plugins/multipart.rbs
CHANGED
data/sig/pool.rbs
CHANGED
data/sig/resolver/https.rbs
CHANGED
@@ -6,7 +6,8 @@ module HTTPX
|
|
6
6
|
DEFAULTS: Hash[Symbol, untyped]
|
7
7
|
FAMILY_TYPES: Hash[singleton(Resolv::DNS::Resource), String]
|
8
8
|
|
9
|
-
|
9
|
+
attr_reader family: ip_family
|
10
|
+
|
10
11
|
@options: Options
|
11
12
|
@requests: Hash[Request, String]
|
12
13
|
@connections: Array[Connection]
|
@@ -33,7 +34,7 @@ module HTTPX
|
|
33
34
|
|
34
35
|
def build_request: (String hostname) -> Request
|
35
36
|
|
36
|
-
def decode_response_body: (Response) -> Array[dns_result]
|
37
|
+
def decode_response_body: (Response) -> Array[dns_result]?
|
37
38
|
end
|
38
39
|
end
|
39
40
|
end
|
data/sig/resolver/native.rbs
CHANGED
data/sig/resolver/resolver.rbs
CHANGED
@@ -6,7 +6,7 @@ module HTTPX
|
|
6
6
|
|
7
7
|
RECORD_TYPES: Hash[Integer, singleton(Resolv::DNS::Resource)]
|
8
8
|
|
9
|
-
attr_reader family: ip_family
|
9
|
+
attr_reader family: ip_family?
|
10
10
|
|
11
11
|
@record_type: singleton(Resolv::DNS::Resource)
|
12
12
|
@options: Options
|
@@ -24,6 +24,8 @@ module HTTPX
|
|
24
24
|
|
25
25
|
private
|
26
26
|
|
27
|
+
def emit_resolved_connection: (Connection connection, Array[IPAddr] addresses) -> void
|
28
|
+
|
27
29
|
def initialize: (ip_family? family, options options) -> void
|
28
30
|
|
29
31
|
def early_resolve: (Connection connection, ?hostname: String) -> void
|
data/sig/resolver.rbs
CHANGED
data/sig/response.rbs
CHANGED
data/sig/utils.rbs
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: httpx
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.22.
|
4
|
+
version: 0.22.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tiago Cardoso
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-01-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: http-2-next
|
@@ -91,6 +91,7 @@ extra_rdoc_files:
|
|
91
91
|
- doc/release_notes/0_22_0.md
|
92
92
|
- doc/release_notes/0_22_1.md
|
93
93
|
- doc/release_notes/0_22_2.md
|
94
|
+
- doc/release_notes/0_22_3.md
|
94
95
|
- doc/release_notes/0_2_0.md
|
95
96
|
- doc/release_notes/0_2_1.md
|
96
97
|
- doc/release_notes/0_3_0.md
|
@@ -174,6 +175,7 @@ files:
|
|
174
175
|
- doc/release_notes/0_22_0.md
|
175
176
|
- doc/release_notes/0_22_1.md
|
176
177
|
- doc/release_notes/0_22_2.md
|
178
|
+
- doc/release_notes/0_22_3.md
|
177
179
|
- doc/release_notes/0_2_0.md
|
178
180
|
- doc/release_notes/0_2_1.md
|
179
181
|
- doc/release_notes/0_3_0.md
|