httpx 1.6.0 → 1.6.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69e60280aa195822960c54a6e90e29a2dfc56cd400332fb39fa9747e14028595
4
- data.tar.gz: 4395c88ede0b567c52c1cb67afcb11535cf4c0f797210b05b3771e884cb77d69
3
+ metadata.gz: 7c039169fa319d4e42cff7cb34d8a9cc7fc571e62565f673c32436645780222c
4
+ data.tar.gz: 6affb65b4fa71aa813cffc498034e0c6a6609bcdb0fca9d0ae9df738d4786d1d
5
5
  SHA512:
6
- metadata.gz: f0bdc947b9b401a5fdecd664ca857d75a36f4837d945cd7d4aff723e4ba2397c48faf5fb16b8a36265c8b82caaa772ad08776771ce48078000fc8e882ffe296a
7
- data.tar.gz: 953d4930cdb8939af913fbdc16c034ffa2cd53e59c95bae61210e1b1a8e6afb82e3e0f83d590141acdecb1161e2cd067e9d0fde43bab696e222e3b955659a8b3
6
+ metadata.gz: 21be43709d51f7c50d1624923bc1657c9f096b84686612ff70f660af320d71d94d28a52ccef05f27558d74f22e1fd307944ca8ddf8b6c74c0b322e83eaad9b19
7
+ data.tar.gz: c551d2b32a71c5bbf099283b3577abe80d5005f702af5edefb79f445296c37c8fb9afb3d52b18db254a0c4ddd115d5f45953549f9b3c99c90ff8b86aedd6cb91
@@ -0,0 +1,17 @@
1
+ # 1.6.1
2
+
3
+ ## Improvements
4
+
5
+ * `:oauth` plugin: `.oauth_session` can be called with an `:audience` parameter, which has the effect of adding it as an extra form body parameter of the token request.
6
+
7
+ ## Bugfixes
8
+
9
+ * options: when freezing the options, skip freezing `:debug`; it's usually a file/IO/stream object (stdout, stderr...), which makes it error when log messages are written.
10
+ * tcp: fixed adding IPv6 addresses to a tcp object when IPv4 connection probe is ongoing so that the next try uses the first ipv6 address.
11
+ * tcp: reorder addresses on reconnection, so ipv6 is tried first in case it is still valid.
12
+ * tcp: make sure ip index is decremented on error, so the next tried IP may be a valid one.
13
+ * tcp: do not reattempt connecting if there are no available addresses to connect. This may happen in a fiber-aware context, where fiber A waits on connection, fiber B reconnects as a result on an error or GOAWAY frame and waits on the resolver DNS answer, and when context is passed back to fiber B, it should go back to the invalidate the response and try again while waiting on the resolver as well.
14
+ * ssl: on connection coalescing, do not merge the ssl sessions, as these are frozen post-initialization.
15
+ * http2: all received GOAWAY frames emit goaway error and teardown the connection independent of the error code (it was only doing it for `:noerror`, but others may appear).
16
+ * do not check at require time whether the network is multi-homed; instead, defer it to first use and cache (this can break environments which block access to certain syscalls during boot time).
17
+ * options: do not ignore when user sets `:ip_families` in name resolution.
@@ -0,0 +1,11 @@
1
+ # 1.6.2
2
+
3
+ ## Bugfixes
4
+
5
+ * revert of behaviour introduced in 1.6.1 around handling of `:ip_families` for name resolution.
6
+ * when no option is passed, do not assume no IPv6 connectivity if no available non-local IP is found, as the local network may still be reachable under `[::]`.
7
+ * bail out connection if the tcp connection was established but the ssl session failed.
8
+ * when alpn negotiation succeeded, this could still initialize the HTTP/2 connection and put bytes in the buffer, which would cause a busy loop trying to write to a non-open socket.
9
+ * datadog: fix initialization of spans in non-connection related errors
10
+ * past code was relying on the error being around DNS, but other errors could pop; the fix was moving the init time setup early to the session, when a request is first passed ot the associated connection.
11
+ * it can also fail earlier, so provide a workaround for that as well.
@@ -180,7 +180,7 @@ module Datadog::Tracing
180
180
  end
181
181
 
182
182
  module RequestMethods
183
- attr_reader :init_time
183
+ attr_accessor :init_time
184
184
 
185
185
  # intercepts request initialization to inject the tracing logic.
186
186
  def initialize(*)
@@ -193,26 +193,30 @@ module Datadog::Tracing
193
193
  RequestTracer.call(self)
194
194
  end
195
195
 
196
- def response=(response)
197
- if response.is_a?(::HTTPX::ErrorResponse) && response.error.respond_to?(:connection)
198
- # handles the case when the +error+ happened during name resolution, which means
199
- # that the tracing start point hasn't been triggered yet; in such cases, the approximate
200
- # initial resolving time is collected from the connection, and used as span start time,
201
- # and the tracing object in inserted before the on response callback is called.
202
- @init_time = response.error.connection.init_time
203
- end
196
+ def response=(*)
197
+ # init_time should be set when it's send to a connection.
198
+ # However, there are situations where connection initialization fails.
199
+ # Example is the :ssrf_filter plugin, which raises an error on
200
+ # initialize if the host is an IP which matches against the known set.
201
+ # in such cases, we'll just set here right here.
202
+ @init_time ||= ::Datadog::Core::Utils::Time.now.utc
203
+
204
204
  super
205
205
  end
206
206
  end
207
207
 
208
208
  module ConnectionMethods
209
- attr_reader :init_time
210
-
211
209
  def initialize(*)
212
210
  super
213
211
 
214
212
  @init_time = ::Datadog::Core::Utils::Time.now.utc
215
213
  end
214
+
215
+ def send(request)
216
+ request.init_time ||= @init_time
217
+
218
+ super
219
+ end
216
220
  end
217
221
  end
218
222
 
@@ -23,8 +23,8 @@ module HTTPX
23
23
  end
24
24
 
25
25
  class GoawayError < Error
26
- def initialize
27
- super(0, :no_error)
26
+ def initialize(code = :no_error)
27
+ super(0, code)
28
28
  end
29
29
  end
30
30
 
@@ -385,12 +385,10 @@ module HTTPX
385
385
  while (request = @pending.shift)
386
386
  emit(:error, request, error)
387
387
  end
388
- when :no_error
389
- ex = GoawayError.new
388
+ else
389
+ ex = GoawayError.new(error)
390
390
  @pending.unshift(*@streams.keys)
391
391
  teardown
392
- else
393
- ex = Error.new(0, error)
394
392
  end
395
393
 
396
394
  if ex
@@ -44,7 +44,7 @@ module HTTPX
44
44
 
45
45
  attr_accessor :current_session, :family
46
46
 
47
- protected :sibling
47
+ protected :ssl_session, :sibling
48
48
 
49
49
  def initialize(uri, options)
50
50
  @current_session = @current_selector =
@@ -177,7 +177,7 @@ module HTTPX
177
177
 
178
178
  def merge(connection)
179
179
  @origins |= connection.instance_variable_get(:@origins)
180
- if connection.ssl_session
180
+ if @ssl_session.nil? && connection.ssl_session
181
181
  @ssl_session = connection.ssl_session
182
182
  @io.session_new_cb do |sess|
183
183
  @ssl_session = sess
@@ -243,6 +243,10 @@ module HTTPX
243
243
  case @state
244
244
  when :idle
245
245
  connect
246
+
247
+ # when opening the tcp or ssl socket fails
248
+ return if @state == :closed
249
+
246
250
  consume
247
251
  when :closed
248
252
  return
data/lib/httpx/io/tcp.rb CHANGED
@@ -14,7 +14,10 @@ module HTTPX
14
14
 
15
15
  def initialize(origin, addresses, options)
16
16
  @state = :idle
17
+ @keep_open = false
17
18
  @addresses = []
19
+ @ip_index = -1
20
+ @ip = nil
18
21
  @hostname = origin.host
19
22
  @options = options
20
23
  @fallback_protocol = @options.fallback_protocol
@@ -53,20 +56,24 @@ module HTTPX
53
56
  @addresses = [*@addresses[0, ip_index], *addrs, *@addresses[ip_index..-1]]
54
57
  else
55
58
  @addresses.unshift(*addrs)
56
- @ip_index += addrs.size if @ip_index
57
59
  end
60
+ @ip_index += addrs.size
58
61
  end
59
62
 
60
63
  # eliminates expired entries and returns whether there are still any left.
61
64
  def addresses?
62
65
  prev_addr_size = @addresses.size
63
66
 
64
- @addresses.delete_if(&:expired?)
65
-
66
- unless (decr = prev_addr_size - @addresses.size).zero?
67
- @ip_index = @addresses.size - decr
67
+ @addresses.delete_if(&:expired?).sort! do |addr1, addr2|
68
+ if addr1.ipv6?
69
+ addr2.ipv6? ? 0 : 1
70
+ else
71
+ addr2.ipv6? ? -1 : 0
72
+ end
68
73
  end
69
74
 
75
+ @ip_index = @addresses.size - 1 if prev_addr_size != @addresses.size
76
+
70
77
  @addresses.any?
71
78
  end
72
79
 
@@ -81,6 +88,17 @@ module HTTPX
81
88
  def connect
82
89
  return unless closed?
83
90
 
91
+ if @addresses.empty?
92
+ # an idle connection trying to connect with no available addresses is a connection
93
+ # out of the initial context which is back to the DNS resolution loop. This may
94
+ # happen in a fiber-aware context where a connection reconnects with expired addresses,
95
+ # and context is passed back to a fiber on the same connection while waiting for the
96
+ # DNS answer.
97
+ log { "tried connecting while resolving, skipping..." }
98
+
99
+ return
100
+ end
101
+
84
102
  if !@io || @io.closed?
85
103
  transition(:idle)
86
104
  @io = build_socket
@@ -88,29 +106,33 @@ module HTTPX
88
106
  try_connect
89
107
  rescue Errno::EHOSTUNREACH,
90
108
  Errno::ENETUNREACH => e
91
- raise e if @ip_index <= 0
109
+ @ip_index -= 1
110
+
111
+ raise e if @ip_index.negative?
92
112
 
93
113
  log { "failed connecting to #{@ip} (#{e.message}), evict from cache and trying next..." }
94
114
  Resolver.cached_lookup_evict(@hostname, @ip)
95
115
 
96
- @ip_index -= 1
97
116
  @io = build_socket
98
117
  retry
99
118
  rescue Errno::ECONNREFUSED,
100
119
  Errno::EADDRNOTAVAIL,
101
120
  SocketError,
102
121
  IOError => e
103
- raise e if @ip_index <= 0
122
+ @ip_index -= 1
123
+
124
+ raise e if @ip_index.negative?
104
125
 
105
126
  log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
106
- @ip_index -= 1
107
127
  @io = build_socket
108
128
  retry
109
129
  rescue Errno::ETIMEDOUT => e
110
- raise ConnectTimeoutError.new(@options.timeout[:connect_timeout], e.message) if @ip_index <= 0
130
+ @ip_index -= 1
131
+
132
+ raise ConnectTimeoutError.new(@options.timeout[:connect_timeout], e.message) if @ip_index.negative?
111
133
 
112
134
  log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
113
- @ip_index -= 1
135
+
114
136
  @io = build_socket
115
137
  retry
116
138
  end
data/lib/httpx/options.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "socket"
4
-
5
3
  module HTTPX
6
4
  # Contains a set of options which are passed and shared across from session to its requests or
7
5
  # responses.
@@ -151,6 +149,14 @@ module HTTPX
151
149
 
152
150
  def freeze
153
151
  self.class.options_names.each do |ivar|
152
+ # avoid freezing debug option, as when it's set, it's usually an
153
+ # object which cannot be frozen, like stderr or stdout. It's a
154
+ # documented exception then, and still does not defeat the purpose
155
+ # here, which is to make option objects shareable across ractors,
156
+ # and in most cases debug should be nil, or one of the objects
157
+ # which will eventually be shareable, like STDOUT or STDERR.
158
+ next if ivar == :debug
159
+
154
160
  instance_variable_get(:"@#{ivar}").freeze
155
161
  end
156
162
  super
@@ -406,18 +412,6 @@ module HTTPX
406
412
  end
407
413
  end
408
414
 
409
- # https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
410
- ip_address_families = begin
411
- list = Socket.ip_address_list
412
- if list.any? { |a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
413
- [Socket::AF_INET6, Socket::AF_INET]
414
- else
415
- [Socket::AF_INET]
416
- end
417
- rescue NotImplementedError
418
- [Socket::AF_INET]
419
- end.freeze
420
-
421
415
  DEFAULT_OPTIONS = {
422
416
  :max_requests => Float::INFINITY,
423
417
  :debug => nil,
@@ -462,7 +456,7 @@ module HTTPX
462
456
  :resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym,
463
457
  :resolver_options => { cache: true }.freeze,
464
458
  :pool_options => EMPTY_HASH,
465
- :ip_families => ip_address_families,
459
+ :ip_families => nil,
466
460
  :close_on_fork => false,
467
461
  }.freeze
468
462
  end
@@ -16,7 +16,7 @@ module HTTPX
16
16
  SUPPORTED_AUTH_METHODS = %w[client_secret_basic client_secret_post].freeze
17
17
 
18
18
  class OAuthSession
19
- attr_reader :grant_type, :client_id, :client_secret, :access_token, :refresh_token, :scope
19
+ attr_reader :grant_type, :client_id, :client_secret, :access_token, :refresh_token, :scope, :audience
20
20
 
21
21
  def initialize(
22
22
  issuer:,
@@ -25,6 +25,7 @@ module HTTPX
25
25
  access_token: nil,
26
26
  refresh_token: nil,
27
27
  scope: nil,
28
+ audience: nil,
28
29
  token_endpoint: nil,
29
30
  response_type: nil,
30
31
  grant_type: nil,
@@ -41,6 +42,7 @@ module HTTPX
41
42
  when Array
42
43
  scope
43
44
  end
45
+ @audience = audience
44
46
  @access_token = access_token
45
47
  @refresh_token = refresh_token
46
48
  @token_endpoint_auth_method = String(token_endpoint_auth_method) if token_endpoint_auth_method
@@ -125,7 +127,11 @@ module HTTPX
125
127
  grant_type = oauth_session.grant_type
126
128
 
127
129
  headers = {}
128
- form_post = { "grant_type" => grant_type, "scope" => Array(oauth_session.scope).join(" ") }.compact
130
+ form_post = {
131
+ "grant_type" => grant_type,
132
+ "scope" => Array(oauth_session.scope).join(" "),
133
+ "audience" => oauth_session.audience,
134
+ }.compact
129
135
 
130
136
  # auth
131
137
  case oauth_session.token_endpoint_auth_method
@@ -15,7 +15,9 @@ module HTTPX
15
15
  @options = options
16
16
  @resolver_options = @options.resolver_options
17
17
 
18
- @resolvers = options.ip_families.map do |ip_family|
18
+ ip_families = options.ip_families || Resolver.supported_ip_families
19
+
20
+ @resolvers = ip_families.map do |ip_family|
19
21
  resolver = resolver_type.new(ip_family, options)
20
22
  resolver.multi = self
21
23
  resolver
@@ -67,8 +69,12 @@ module HTTPX
67
69
  addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
68
70
  return false unless addresses
69
71
 
72
+ ip_families = connection.options.ip_families
73
+
70
74
  resolved = false
71
75
  addresses.group_by(&:family).sort { |(f1, _), (f2, _)| f2 <=> f1 }.each do |family, addrs|
76
+ next unless ip_families.nil? || ip_families.include?(family)
77
+
72
78
  # try to match the resolver by family. However, there are cases where that's not possible, as when
73
79
  # the system does not have IPv6 connectivity, but it does support IPv6 via loopback/link-local.
74
80
  resolver = @resolvers.find { |r| r.family == family } || @resolvers.first
@@ -79,12 +79,18 @@ module HTTPX
79
79
  "answer #{connection.peer.host}: #{addresses.inspect} (early resolve: #{early_resolve})"
80
80
  end
81
81
 
82
- if !early_resolve && # do not apply resolution delay for non-dns name resolution
83
- @current_selector && # just in case...
84
- family == Socket::AF_INET && # resolution delay only applies to IPv4
85
- !connection.io && # connection already has addresses and initiated/ended handshake
86
- connection.options.ip_families.size > 1 && # no need to delay if not supporting dual stack IP
87
- addresses.first.to_s != connection.peer.host.to_s # connection URL host is already the IP (early resolve included perhaps?)
82
+ # do not apply resolution delay for non-dns name resolution
83
+ if !early_resolve &&
84
+ # just in case...
85
+ @current_selector &&
86
+ # resolution delay only applies to IPv4
87
+ family == Socket::AF_INET &&
88
+ # connection already has addresses and initiated/ended handshake
89
+ !connection.io &&
90
+ # no need to delay if not supporting dual stack / multi-homed IP
91
+ (connection.options.ip_families || Resolver.supported_ip_families).size > 1 &&
92
+ # connection URL host is already the IP (early resolve included perhaps?)
93
+ addresses.first.to_s != connection.peer.host.to_s
88
94
  log { "resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: applying resolution delay..." }
89
95
 
90
96
  @current_selector.after(0.05) do
@@ -187,7 +187,9 @@ module HTTPX
187
187
 
188
188
  transition(:open)
189
189
 
190
- connection.options.ip_families.each do |family|
190
+ ip_families = connection.options.ip_families || Resolver.supported_ip_families
191
+
192
+ ip_families.each do |family|
191
193
  @queries << [family, connection]
192
194
  end
193
195
  async_resolve(connection, hostname, scheme)
@@ -195,7 +197,7 @@ module HTTPX
195
197
  end
196
198
 
197
199
  def async_resolve(connection, hostname, scheme)
198
- families = connection.options.ip_families
200
+ families = connection.options.ip_families || Resolver.supported_ip_families
199
201
  log { "resolver: query for #{hostname}" }
200
202
  timeouts = @timeouts[connection.peer.host]
201
203
  resolve_timeout = timeouts.first
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "socket"
3
4
  require "resolv"
4
5
 
5
6
  module HTTPX
@@ -22,6 +23,20 @@ module HTTPX
22
23
 
23
24
  module_function
24
25
 
26
+ def supported_ip_families
27
+ @supported_ip_families ||= begin
28
+ # https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
29
+ list = Socket.ip_address_list
30
+ if list.any? { |a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
31
+ [Socket::AF_INET6, Socket::AF_INET]
32
+ else
33
+ [Socket::AF_INET]
34
+ end
35
+ rescue NotImplementedError
36
+ [Socket::AF_INET]
37
+ end.freeze
38
+ end
39
+
25
40
  def resolver_for(resolver_type, options)
26
41
  case resolver_type
27
42
  when Symbol
data/lib/httpx/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- VERSION = "1.6.0"
4
+ VERSION = "1.6.2"
5
5
  end
@@ -106,7 +106,7 @@ module HTTPX
106
106
  end
107
107
 
108
108
  class GoawayError < Error
109
- def initialize: () -> void
109
+ def initialize: (?Symbol code) -> void
110
110
  end
111
111
 
112
112
  class PingError < Error
data/sig/options.rbs CHANGED
@@ -130,7 +130,7 @@ module HTTPX
130
130
  attr_reader pool_options: pool_options
131
131
 
132
132
  # ip_families
133
- attr_reader ip_families: Array[ip_family]
133
+ attr_reader ip_families: Array[ip_family]?
134
134
 
135
135
  def ==: (Options other) -> bool
136
136
 
@@ -195,7 +195,7 @@ module HTTPX
195
195
 
196
196
  def option_addresses: (ipaddr | _ToAry[ipaddr] value) -> Array[ipaddr]
197
197
 
198
- def option_ip_families: (Integer | _ToAry[Integer] value) -> Array[Integer]
198
+ def option_ip_families: (ip_family | _ToAry[ip_family] value) -> Array[ip_family]
199
199
  end
200
200
 
201
201
  type options = Options | Hash[Symbol, untyped]
@@ -27,7 +27,21 @@ module HTTPX
27
27
 
28
28
  attr_reader scope: Array[String]?
29
29
 
30
- def initialize: (issuer: uri, client_id: String, client_secret: String, ?access_token: String?, ?refresh_token: String?, ?scope: (Array[String] | String)?, ?token_endpoint: String?, ?response_type: String?, ?grant_type: String?, ?token_endpoint_auth_method: ::String) -> void
30
+ attr_reader audience: String?
31
+
32
+ def initialize: (
33
+ issuer: uri,
34
+ client_id: String,
35
+ client_secret: String,
36
+ ?access_token: String?,
37
+ ?refresh_token: String?,
38
+ ?scope: (Array[String] | String)?,
39
+ ?token_endpoint: String?,
40
+ ?response_type: String?,
41
+ ?grant_type: String?,
42
+ ?token_endpoint_auth_method: ::String,
43
+ ?audience: ::String
44
+ ) -> void
31
45
 
32
46
  def token_endpoint: () -> String
33
47
 
data/sig/resolver.rbs CHANGED
@@ -23,6 +23,8 @@ module HTTPX
23
23
 
24
24
  def self?.hosts_resolve: (String hostname) -> Array[Entry]?
25
25
 
26
+ def self?.supported_ip_families: () -> Array[ip_family]
27
+
26
28
  def self?.resolver_for: (Symbol | singleton(Resolver) resolver_type, Options options) -> singleton(Resolver)
27
29
 
28
30
  def self?.cached_lookup: (String hostname) -> Array[Entry]?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: httpx
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 1.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
@@ -156,6 +156,8 @@ extra_rdoc_files:
156
156
  - doc/release_notes/1_5_0.md
157
157
  - doc/release_notes/1_5_1.md
158
158
  - doc/release_notes/1_6_0.md
159
+ - doc/release_notes/1_6_1.md
160
+ - doc/release_notes/1_6_2.md
159
161
  files:
160
162
  - LICENSE.txt
161
163
  - README.md
@@ -284,6 +286,8 @@ files:
284
286
  - doc/release_notes/1_5_0.md
285
287
  - doc/release_notes/1_5_1.md
286
288
  - doc/release_notes/1_6_0.md
289
+ - doc/release_notes/1_6_1.md
290
+ - doc/release_notes/1_6_2.md
287
291
  - lib/httpx.rb
288
292
  - lib/httpx/adapters/datadog.rb
289
293
  - lib/httpx/adapters/faraday.rb