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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 373c83e6252fb0439bfe307cacc18514741c9b8eaa1b9d055e1da59015c7a8f3
4
- data.tar.gz: bc20567c842a6b6bd7d450fc6edd5ee1eb509940ffddab643a0c6a3b8ad48522
3
+ metadata.gz: 39a428f7a8a6747513b56a5e2b4216569363b71baa273d1904b3805f6385e1aa
4
+ data.tar.gz: 900178247910015b23dcc199df570dadde117d3837efecc5b7100e6478054a77
5
5
  SHA512:
6
- metadata.gz: ff4fd33b7614c410fb30b4916ad430fbf2091d43d6bec5dfa3784564a4824491baa89f33e491db6a4a76c5cf8f4e9e07e2d5192a1214ee0f8283e345b3b1387e
7
- data.tar.gz: 4da51a3015ef7a7f32f8e24b7e14324acda8c6a735cd3038e0a7a224376c60775ed533ee936ba58ad51639adcc16906a8ef8b637d9ef84733ac4546ed4ddd7de
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://os85.gitlab.io/httpx/ |
145
- | Documentation | https://os85.gitlab.io/httpx/rdoc/ |
146
- | Wiki | https://os85.gitlab.io/httpx/wiki/home.html |
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
 
@@ -1,4 +1,4 @@
1
- # 0.22.1
1
+ # 0.22.2
2
2
 
3
3
  ## Chore
4
4
 
@@ -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.raise_for_status
214
- save_response(env, response.status, response.body.to_s, response.headers, response.reason) do |response_headers|
215
- response_headers.merge!(response.headers)
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)
@@ -365,7 +365,7 @@ module HTTPX
365
365
  ex.set_backtrace(caller)
366
366
  handle_error(ex)
367
367
  end
368
- return unless is_connection_closed && @streams.size.zero?
368
+ return unless is_connection_closed && @streams.empty?
369
369
 
370
370
  emit(:close, is_connection_closed)
371
371
  end
@@ -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
- !(@io.addresses & connection.addresses).empty? && @options == connection.options
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.size.zero? && @inflight.zero? && @write_buffer.empty?
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.size.zero? && @inflight.zero?
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 => e
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
@@ -160,6 +160,7 @@ module HTTPX
160
160
  module URIExtensions
161
161
  # uri 0.11 backport, ships with ruby 3.1
162
162
  refine URI::Generic do
163
+
163
164
  def non_ascii_hostname
164
165
  @non_ascii_hostname
165
166
  end
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(uri, _, options)
10
- ip = IPAddr.new(uri.host)
11
- @host = ip.to_s
12
- @port = uri.port
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.size.zero? && !block
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
@@ -56,7 +56,7 @@ module HTTPX
56
56
 
57
57
  class Inflater
58
58
  def initialize(bytesize)
59
- @inflater = Zlib::Inflate.new(32 + Zlib::MAX_WBITS)
59
+ @inflater = Zlib::Inflate.new(Zlib::MAX_WBITS + 32)
60
60
  @bytesize = bytesize
61
61
  @buffer = nil
62
62
  end
@@ -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, ":expect_threshold_size must be positive" unless bytes.positive?
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)
@@ -57,7 +57,7 @@ module HTTPX
57
57
 
58
58
  yield data
59
59
 
60
- message = message.byteslice((5 + size)..-1)
60
+ message = message.byteslice((size + 5)..-1)
61
61
  end
62
62
  end
63
63
 
@@ -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 * (0.5 * (1 + rand)) }
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
@@ -136,9 +136,22 @@ module HTTPX
136
136
  emit_resolve_error(connection, connection.origin.host, e)
137
137
  return
138
138
  end
139
- if answers.nil? || answers.empty?
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? || addresses.empty?
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
- uri = URI::Generic.build(scheme: "udp", port: port)
313
- uri.hostname = ip
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)
@@ -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
@@ -181,6 +181,12 @@ module HTTPX
181
181
  end
182
182
  end
183
183
 
184
+ def filename
185
+ return unless @headers.key?("content-disposition")
186
+
187
+ Utils.get_filename(@headers["content-disposition"])
188
+ end
189
+
184
190
  def to_s
185
191
  case @buffer
186
192
  when StringIO
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- VERSION = "0.22.2"
4
+ VERSION = "0.22.3"
5
5
  end
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
- @type: io_type
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
@@ -68,8 +68,6 @@ module HTTPX
68
68
  def initialize: (Response response) -> void
69
69
 
70
70
  def parse: () -> void
71
-
72
- def get_filename: (String head) -> String?
73
71
  end
74
72
 
75
73
  class FilePart # < SimpleDelegator
data/sig/pool.rbs CHANGED
@@ -22,7 +22,9 @@ module HTTPX
22
22
 
23
23
  private
24
24
 
25
- def initialize: () -> untyped
25
+ def initialize: () -> void
26
+
27
+ def try_clone_connection: (Connection connection, Integer? family) -> Connection
26
28
 
27
29
  def resolve_connection: (Connection) -> void
28
30
 
@@ -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
- @family: ip_family
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
@@ -7,7 +7,8 @@ module HTTPX
7
7
  DEFAULTS: Hash[Symbol, untyped]
8
8
  DNS_PORT: Integer
9
9
 
10
- @family: ip_family
10
+ attr_reader family: ip_family
11
+
11
12
  @options: Options
12
13
  @ns_index: Integer
13
14
  @nameserver: Array[String]?
@@ -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
@@ -30,6 +30,6 @@ module HTTPX
30
30
 
31
31
  def self?.encode_dns_query: (String hostname, ?type: dns_resource) -> String
32
32
 
33
- def self?.decode_dns_answer: (String) -> Array[dns_result]
33
+ def self?.decode_dns_answer: (String) -> Array[dns_result]?
34
34
  end
35
35
  end
data/sig/response.rbs CHANGED
@@ -64,6 +64,8 @@ module HTTPX
64
64
  def each: () { (String) -> void } -> void
65
65
  | () -> Enumerable[String]
66
66
 
67
+ def filename: () -> String?
68
+
67
69
  def bytesize: () -> (Integer | Float)
68
70
  def empty?: () -> bool
69
71
  def copy_to: (String | File | _Writer destination) -> void
data/sig/utils.rbs CHANGED
@@ -9,5 +9,7 @@ module HTTPX
9
9
  def self?.elapsed_time: (Integer | Float monotonic_time) -> Float
10
10
 
11
11
  def self?.to_uri: (generic_uri uri) -> URI::Generic
12
+
13
+ def self?.get_filename: (String header) -> String?
12
14
  end
13
15
  end
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.2
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: 2022-12-23 00:00:00.000000000 Z
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