httpx 1.6.0 → 1.6.1

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: 947d1ad4fd269f30707292be54687f7be2054147fb3fa471c76395e92c55820e
4
+ data.tar.gz: d6e321ace4211e8bf8215eff799b5c98aad627990db5b9b2cc091fe5b1b95df7
5
5
  SHA512:
6
- metadata.gz: f0bdc947b9b401a5fdecd664ca857d75a36f4837d945cd7d4aff723e4ba2397c48faf5fb16b8a36265c8b82caaa772ad08776771ce48078000fc8e882ffe296a
7
- data.tar.gz: 953d4930cdb8939af913fbdc16c034ffa2cd53e59c95bae61210e1b1a8e6afb82e3e0f83d590141acdecb1161e2cd067e9d0fde43bab696e222e3b955659a8b3
6
+ metadata.gz: bd87db725b6da121b79f266a8a309816f5afea4ec1890a14720c16a667ea32632fdbdd9d74249b337672315fd2ac16c1488d8aa13f4e031b33751e1625f2c9a5
7
+ data.tar.gz: 00027b822dff3985519aba56eb7b3bd28d0d81eacbdeb9d49f6e778023d60ae3eaf1a2469cf11287da12c0dde2a23b1557da33e77310ea6733e03e4f73b822e2
@@ -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.
@@ -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
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 || Resolver.supported_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.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
@@ -85,7 +91,11 @@ module HTTPX
85
91
  end
86
92
 
87
93
  def lazy_resolve(connection)
94
+ ip_families = connection.options.ip_families || Resolver.supported_ip_families
95
+
88
96
  @resolvers.each do |resolver|
97
+ next unless ip_families.include?(resolver.family)
98
+
89
99
  resolver << @current_session.try_clone_connection(connection, @current_selector, resolver.family)
90
100
  next if resolver.empty?
91
101
 
@@ -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.1"
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
@@ -156,6 +156,7 @@ 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
159
160
  files:
160
161
  - LICENSE.txt
161
162
  - README.md
@@ -284,6 +285,7 @@ files:
284
285
  - doc/release_notes/1_5_0.md
285
286
  - doc/release_notes/1_5_1.md
286
287
  - doc/release_notes/1_6_0.md
288
+ - doc/release_notes/1_6_1.md
287
289
  - lib/httpx.rb
288
290
  - lib/httpx/adapters/datadog.rb
289
291
  - lib/httpx/adapters/faraday.rb