httpx 1.6.2 → 1.6.3

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_6_3.md +47 -0
  3. data/lib/httpx/adapters/sentry.rb +1 -1
  4. data/lib/httpx/connection/http1.rb +9 -9
  5. data/lib/httpx/connection/http2.rb +14 -15
  6. data/lib/httpx/connection.rb +115 -102
  7. data/lib/httpx/extensions.rb +0 -14
  8. data/lib/httpx/io/ssl.rb +1 -1
  9. data/lib/httpx/loggable.rb +12 -2
  10. data/lib/httpx/options.rb +20 -0
  11. data/lib/httpx/plugins/callbacks.rb +15 -1
  12. data/lib/httpx/plugins/digest_auth.rb +1 -1
  13. data/lib/httpx/plugins/proxy/http.rb +37 -9
  14. data/lib/httpx/plugins/response_cache/file_store.rb +1 -0
  15. data/lib/httpx/plugins/response_cache.rb +13 -2
  16. data/lib/httpx/plugins/stream_bidi.rb +15 -6
  17. data/lib/httpx/pool.rb +53 -19
  18. data/lib/httpx/request.rb +3 -13
  19. data/lib/httpx/resolver/https.rb +35 -19
  20. data/lib/httpx/resolver/multi.rb +8 -27
  21. data/lib/httpx/resolver/native.rb +46 -38
  22. data/lib/httpx/resolver/resolver.rb +45 -28
  23. data/lib/httpx/resolver/system.rb +63 -39
  24. data/lib/httpx/selector.rb +35 -20
  25. data/lib/httpx/session.rb +18 -28
  26. data/lib/httpx/transcoder/deflate.rb +13 -8
  27. data/lib/httpx/transcoder/utils/body_reader.rb +1 -2
  28. data/lib/httpx/transcoder/utils/deflater.rb +1 -2
  29. data/lib/httpx/version.rb +1 -1
  30. data/sig/connection.rbs +12 -3
  31. data/sig/loggable.rbs +5 -1
  32. data/sig/options.rbs +5 -1
  33. data/sig/plugins/callbacks.rbs +3 -0
  34. data/sig/plugins/stream_bidi.rbs +3 -5
  35. data/sig/resolver/https.rbs +2 -0
  36. data/sig/resolver/multi.rbs +0 -9
  37. data/sig/resolver/native.rbs +0 -2
  38. data/sig/resolver/resolver.rbs +9 -8
  39. data/sig/resolver/system.rbs +4 -2
  40. data/sig/selector.rbs +2 -0
  41. data/sig/session.rbs +5 -3
  42. metadata +3 -1
@@ -34,7 +34,7 @@ module HTTPX
34
34
  klass = klass.superclass
35
35
  end
36
36
 
37
- message = +"(pid:#{Process.pid}, " \
37
+ message = +"(time:#{Time.now.utc}, pid:#{Process.pid}, " \
38
38
  "tid:#{Thread.current.object_id}, " \
39
39
  "fid:#{Fiber.current.object_id}, " \
40
40
  "self:#{class_name}##{object_id}) "
@@ -47,7 +47,17 @@ module HTTPX
47
47
  log(level: level, color: color, debug_level: debug_level, debug: debug) { ex.full_message }
48
48
  end
49
49
 
50
- def log_redact(text, should_redact = @options.debug_redact)
50
+ def log_redact_headers(text)
51
+ log_redact(text, @options.debug_redact == :headers)
52
+ end
53
+
54
+ def log_redact_body(text)
55
+ log_redact(text, @options.debug_redact == :body)
56
+ end
57
+
58
+ def log_redact(text, should_redact)
59
+ should_redact ||= @options.debug_redact == true
60
+
51
61
  return text.to_s unless should_redact
52
62
 
53
63
  "[REDACTED]"
data/lib/httpx/options.rb CHANGED
@@ -13,6 +13,9 @@ module HTTPX
13
13
  CONNECT_TIMEOUT = READ_TIMEOUT = WRITE_TIMEOUT = 60
14
14
  REQUEST_TIMEOUT = OPERATION_TIMEOUT = nil
15
15
 
16
+ # default value used for "user-agent" header, when not overridden.
17
+ USER_AGENT = "httpx.rb/#{VERSION}".freeze # rubocop:disable Style/RedundantFreeze
18
+
16
19
  @options_names = []
17
20
 
18
21
  class << self
@@ -144,6 +147,7 @@ module HTTPX
144
147
  instance_variable_set(:"@#{k}", value)
145
148
  end
146
149
 
150
+ do_initialize
147
151
  freeze
148
152
  end
149
153
 
@@ -369,6 +373,8 @@ module HTTPX
369
373
  end
370
374
 
371
375
  def option_headers(value)
376
+ value = value.dup if value.frozen?
377
+
372
378
  headers_class.new(value)
373
379
  end
374
380
 
@@ -395,6 +401,20 @@ module HTTPX
395
401
  Array(value)
396
402
  end
397
403
 
404
+ # called after all options are initialized
405
+ def do_initialize
406
+ hs = @headers
407
+
408
+ # initialized default request headers
409
+ hs["user-agent"] = USER_AGENT unless hs.key?("user-agent")
410
+ hs["accept"] = "*/*" unless hs.key?("accept")
411
+ if hs.key?("range")
412
+ hs.delete("accept-encoding")
413
+ else
414
+ hs["accept-encoding"] = supported_compression_formats unless hs.key?("accept-encoding")
415
+ end
416
+ end
417
+
398
418
  def access_option(obj, k, ivar_map)
399
419
  case obj
400
420
  when Hash
@@ -64,7 +64,7 @@ module HTTPX
64
64
 
65
65
  emit_or_callback_error(:connection_opened, connection.origin, connection.io.socket)
66
66
  end
67
- connection.on(:close) do
67
+ connection.on(:callback_connection_closed) do
68
68
  next unless connection.current_session == self
69
69
 
70
70
  emit_or_callback_error(:connection_closed, connection.origin) if connection.used?
@@ -121,6 +121,20 @@ module HTTPX
121
121
  raise e.cause
122
122
  end
123
123
  end
124
+
125
+ module ConnectionMethods
126
+ private
127
+
128
+ def disconnect
129
+ return if @exhausted
130
+
131
+ return unless @current_session && @current_selector
132
+
133
+ emit(:callback_connection_closed)
134
+
135
+ super
136
+ end
137
+ end
124
138
  end
125
139
  register_plugin :callbacks, Callbacks
126
140
  end
@@ -48,7 +48,7 @@ module HTTPX
48
48
 
49
49
  probe_response = wrap { super(request).first }
50
50
 
51
- return probe_response unless probe_response.is_a?(Response)
51
+ return ([probe_response] * requests.size) unless probe_response.is_a?(Response)
52
52
 
53
53
  if probe_response.status == 401 && digest.can_authenticate?(probe_response.headers["www-authenticate"])
54
54
  request.transition(:idle)
@@ -47,6 +47,17 @@ module HTTPX
47
47
  super || @state == :connecting || @state == :connected
48
48
  end
49
49
 
50
+ def force_close(*)
51
+ if @state == :connecting
52
+ # proxy connect related requests should not be reenqueed
53
+ @parser.reset!
54
+ @inflight -= @parser.pending.size
55
+ @parser.pending.clear
56
+ end
57
+
58
+ super
59
+ end
60
+
50
61
  private
51
62
 
52
63
  def handle_transition(nextstate)
@@ -64,23 +75,40 @@ module HTTPX
64
75
  parser = @parser
65
76
  parser.extend(ProxyParser)
66
77
  parser.on(:response, &method(:__http_on_connect))
67
- parser.on(:close) do |force|
78
+ parser.on(:close) do
68
79
  next unless @parser
69
80
 
70
- if force
71
- reset
72
- emit(:terminate)
73
- end
81
+ reset
82
+ disconnect
74
83
  end
75
84
  parser.on(:reset) do
76
85
  if parser.empty?
77
86
  reset
78
87
  else
79
- transition(:closing)
80
- transition(:closed)
88
+ enqueue_pending_requests_from_parser(parser)
89
+
90
+ initial_state = @state
91
+
92
+ reset
93
+
94
+ if @pending.empty?
95
+ @parser = nil
96
+ next
97
+ end
98
+ # keep parser state around due to proxy auth protocol;
99
+ # intermediate authenticated request is already inside
100
+ # the parser
101
+ parser = nil
102
+
103
+ if initial_state == :connecting
104
+ parser = @parser
105
+ @parser.reset
106
+ end
107
+
108
+ idling
109
+
110
+ @parser = parser
81
111
 
82
- parser.reset if @parser
83
- transition(:idle)
84
112
  transition(:connecting)
85
113
  end
86
114
  end
@@ -130,6 +130,7 @@ module HTTPX::Plugins
130
130
  response = request.options.response_class.new(request, status, version, response_headers)
131
131
  response.original_request = original_request
132
132
  response.finish!
133
+ response.mark_as_cached!
133
134
 
134
135
  IO.copy_stream(f, response.body)
135
136
 
@@ -118,7 +118,10 @@ module HTTPX
118
118
 
119
119
  response.copy_from_cached!
120
120
  elsif request.cacheable_verb? && ResponseCache.cacheable_response?(response)
121
- request.options.response_cache_store.set(request, response) unless response.cached?
121
+ unless response.cached?
122
+ log { "caching response for #{request.uri}..." }
123
+ request.options.response_cache_store.set(request, response)
124
+ end
122
125
  end
123
126
 
124
127
  response
@@ -204,7 +207,7 @@ module HTTPX
204
207
  # returns a unique cache key as a String identifying this request
205
208
  def response_cache_key
206
209
  @response_cache_key ||= begin
207
- keys = [@verb, @uri]
210
+ keys = [@verb, @uri.merge(path)]
208
211
 
209
212
  @options.supported_vary_headers.each do |field|
210
213
  value = @headers[field]
@@ -327,6 +330,14 @@ module HTTPX
327
330
  Time.now
328
331
  end
329
332
  end
333
+
334
+ module ResponseBodyMethods
335
+ def decode_chunk(chunk)
336
+ return chunk if @response.cached?
337
+
338
+ super
339
+ end
340
+ end
330
341
  end
331
342
  register_plugin :response_cache, ResponseCache
332
343
  end
@@ -16,7 +16,7 @@ module HTTPX
16
16
  # The streams keeps send DATA frames while there's data; when they're ain't,
17
17
  # the stream is kept open; it must be explicitly closed by the end user.
18
18
  #
19
- class HTTP2Bidi < Connection::HTTP2
19
+ module HTTP2Methods
20
20
  def initialize(*)
21
21
  super
22
22
  @lock = Thread::Mutex.new
@@ -117,8 +117,11 @@ module HTTPX
117
117
  # which allows it to be registered in the selector alongside actual HTTP-based
118
118
  # HTTPX::Connection objects.
119
119
  class Signal
120
+ attr_reader :error
121
+
120
122
  def initialize
121
123
  @closed = false
124
+ @error = nil
122
125
  @pipe_read, @pipe_write = IO.pipe
123
126
  end
124
127
 
@@ -159,6 +162,11 @@ module HTTPX
159
162
  @closed = true
160
163
  end
161
164
 
165
+ def on_error(error)
166
+ @error = error
167
+ terminate
168
+ end
169
+
162
170
  # noop (the owner connection will take of it)
163
171
  def handle_socket_timeout(interval); end
164
172
  end
@@ -193,6 +201,7 @@ module HTTPX
193
201
 
194
202
  def deselect_connection(connection, *)
195
203
  super
204
+
196
205
  connection.signal = nil
197
206
  end
198
207
  end
@@ -294,14 +303,14 @@ module HTTPX
294
303
  super
295
304
  end
296
305
 
297
- private
298
-
299
- def parser_type(protocol)
300
- return HTTP2Bidi if protocol == "h2"
306
+ def call
307
+ return super unless (error = @signal.error)
301
308
 
302
- super
309
+ on_error(error)
303
310
  end
304
311
 
312
+ private
313
+
305
314
  def set_parser_callbacks(parser)
306
315
  super
307
316
  parser.on(:flush_buffer) do
data/lib/httpx/pool.rb CHANGED
@@ -8,7 +8,6 @@ require "httpx/resolver"
8
8
 
9
9
  module HTTPX
10
10
  class Pool
11
- using ArrayExtensions::FilterMap
12
11
  using URIExtensions
13
12
 
14
13
  POOL_TIMEOUT = 5
@@ -51,32 +50,53 @@ module HTTPX
51
50
  acquire_connection(uri, options) || begin
52
51
  if @connections_counter == @max_connections
53
52
  # this takes precedence over per-origin
54
- @max_connections_cond.wait(@connection_mtx, @pool_timeout)
55
53
 
56
- if (conn = acquire_connection(uri, options))
57
- return conn
58
- end
54
+ expires_at = Utils.now + @pool_timeout
59
55
 
60
- if @connections_counter == @max_connections
61
- # if no matching usable connection was found, the pool will make room and drop a closed connection. if none is found,
62
- # this means that all of them are persistent or being used, so raise a timeout error.
63
- conn = @connections.find { |c| c.state == :closed }
56
+ loop do
57
+ @max_connections_cond.wait(@connection_mtx, @pool_timeout)
64
58
 
65
- raise PoolTimeoutError.new(@pool_timeout,
66
- "Timed out after #{@pool_timeout} seconds while waiting for a connection") unless conn
59
+ if (conn = acquire_connection(uri, options))
60
+ return conn
61
+ end
62
+
63
+ # if one can afford to create a new connection, do it
64
+ break unless @connections_counter == @max_connections
65
+
66
+ # if no matching usable connection was found, the pool will make room and drop a closed connection.
67
+ if (conn = @connections.find { |c| c.state == :closed })
68
+ drop_connection(conn)
69
+ break
70
+ end
71
+
72
+ # happens when a condition was signalled, but another thread snatched the available connection before
73
+ # context was passed back here.
74
+ next if Utils.now < expires_at
67
75
 
68
- drop_connection(conn)
76
+ raise PoolTimeoutError.new(@pool_timeout,
77
+ "Timed out after #{@pool_timeout} seconds while waiting for a connection")
69
78
  end
70
79
 
71
80
  end
72
81
 
73
82
  if @origin_counters[uri.origin] == @max_connections_per_origin
74
83
 
75
- @origin_conds[uri.origin].wait(@connection_mtx, @pool_timeout)
84
+ expires_at = Utils.now + @pool_timeout
85
+
86
+ loop do
87
+ @origin_conds[uri.origin].wait(@connection_mtx, @pool_timeout)
88
+
89
+ if (conn = acquire_connection(uri, options))
90
+ return conn
91
+ end
92
+
93
+ # happens when a condition was signalled, but another thread snatched the available connection before
94
+ # context was passed back here.
95
+ next if Utils.now < expires_at
76
96
 
77
- return acquire_connection(uri, options) ||
78
- raise(PoolTimeoutError.new(@pool_timeout,
79
- "Timed out after #{@pool_timeout} seconds while waiting for a connection to #{uri.origin}"))
97
+ raise(PoolTimeoutError.new(@pool_timeout,
98
+ "Timed out after #{@pool_timeout} seconds while waiting for a connection to #{uri.origin}"))
99
+ end
80
100
  end
81
101
 
82
102
  @connections_counter += 1
@@ -91,6 +111,13 @@ module HTTPX
91
111
  return if connection.options.io
92
112
 
93
113
  @connection_mtx.synchronize do
114
+ if connection.coalesced? || connection.state == :idle
115
+ # when connections coalesce
116
+ drop_connection(connection)
117
+
118
+ return
119
+ end
120
+
94
121
  @connections << connection
95
122
 
96
123
  @max_connections_cond.signal
@@ -128,10 +155,16 @@ module HTTPX
128
155
  end
129
156
 
130
157
  def checkin_resolver(resolver)
131
- @resolver_mtx.synchronize do
132
- resolvers = @resolvers[resolver.class]
158
+ resolver_class = resolver.class
159
+
160
+ resolver = resolver.multi
133
161
 
134
- resolver = resolver.multi
162
+ # a multi requires all sub-resolvers being closed in order to be
163
+ # correctly checked back in.
164
+ return unless resolver.closed?
165
+
166
+ @resolver_mtx.synchronize do
167
+ resolvers = @resolvers[resolver_class]
135
168
 
136
169
  resolvers << resolver unless resolvers.include?(resolver)
137
170
  end
@@ -140,6 +173,7 @@ module HTTPX
140
173
  # :nocov:
141
174
  def inspect
142
175
  "#<#{self.class}:#{object_id} " \
176
+ "@max_connections=#{@max_connections} " \
143
177
  "@max_connections_per_origin=#{@max_connections_per_origin} " \
144
178
  "@pool_timeout=#{@pool_timeout} " \
145
179
  "@connections=#{@connections.size}>"
data/lib/httpx/request.rb CHANGED
@@ -14,9 +14,6 @@ module HTTPX
14
14
 
15
15
  ALLOWED_URI_SCHEMES = %w[https http].freeze
16
16
 
17
- # default value used for "user-agent" header, when not overridden.
18
- USER_AGENT = "httpx.rb/#{VERSION}".freeze # rubocop:disable Style/RedundantFreeze
19
-
20
17
  # the upcased string HTTP verb for this request.
21
18
  attr_reader :verb
22
19
 
@@ -75,16 +72,6 @@ module HTTPX
75
72
  @headers = options.headers.dup
76
73
  merge_headers(params.delete(:headers)) if params.key?(:headers)
77
74
 
78
- @headers["user-agent"] ||= USER_AGENT
79
- @headers["accept"] ||= "*/*"
80
-
81
- # forego compression in the Range request case
82
- if @headers.key?("range")
83
- @headers.delete("accept-encoding")
84
- else
85
- @headers["accept-encoding"] ||= options.supported_compression_formats
86
- end
87
-
88
75
  @query_params = params.delete(:params) if params.key?(:params)
89
76
 
90
77
  @body = options.request_body_class.new(@headers, options, **params)
@@ -166,6 +153,9 @@ module HTTPX
166
153
  # merges +h+ into the instance of HTTPX::Headers of the request.
167
154
  def merge_headers(h)
168
155
  @headers = @headers.merge(h)
156
+ return unless @headers.key?("range")
157
+
158
+ @headers.delete("accept-encoding")
169
159
  end
170
160
 
171
161
  # the URI scheme of the request +uri+.
@@ -30,7 +30,8 @@ module HTTPX
30
30
  use_get: false,
31
31
  }.freeze
32
32
 
33
- def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close, :terminate, :inflight?, :handle_socket_timeout
33
+ def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close,
34
+ :closed?, :deactivate, :terminate, :inflight?, :handle_socket_timeout
34
35
 
35
36
  def initialize(_, options)
36
37
  super
@@ -52,29 +53,24 @@ module HTTPX
52
53
  if @uri_addresses.empty?
53
54
  ex = ResolveError.new("Can't resolve DNS server #{@uri.host}")
54
55
  ex.set_backtrace(caller)
55
- connection.force_reset
56
+ connection.force_close
56
57
  throw(:resolve_error, ex)
57
58
  end
58
59
 
59
60
  resolve(connection)
60
61
  end
61
62
 
62
- # This is already indirectly monitored bt the HTTP connection. In order to skip
63
- # monitoring, this method returns <tt>true</tt>.
64
- def closed?
65
- true
66
- end
67
-
68
- def empty?
69
- true
70
- end
71
-
72
63
  def resolver_connection
73
64
  # TODO: leaks connection object into the pool
74
- @resolver_connection ||= @current_session.find_connection(@uri, @current_selector,
75
- @options.merge(ssl: { alpn_protocols: %w[h2] })).tap do |conn|
76
- emit_addresses(conn, @family, @uri_addresses) unless conn.addresses
77
- end
65
+ @resolver_connection ||=
66
+ @current_session.find_connection(
67
+ @uri,
68
+ @current_selector,
69
+ @options.merge(resolver_class: :system, ssl: { alpn_protocols: %w[h2] })
70
+ ).tap do |conn|
71
+ emit_addresses(conn, @family, @uri_addresses) unless conn.addresses
72
+ conn.on(:force_closed, &method(:force_close))
73
+ end
78
74
  end
79
75
 
80
76
  private
@@ -111,7 +107,9 @@ module HTTPX
111
107
  @connections << connection
112
108
  rescue ResolveError, Resolv::DNS::EncodeError => e
113
109
  reset_hostname(hostname)
110
+ throw(:resolve_error, e) if connection.pending.empty?
114
111
  emit_resolve_error(connection, connection.peer.host, e)
112
+ close_or_resolve
115
113
  end
116
114
  end
117
115
 
@@ -121,6 +119,7 @@ module HTTPX
121
119
  hostname = @requests.delete(request)
122
120
  connection = reset_hostname(hostname)
123
121
  emit_resolve_error(connection, connection.peer.host, e)
122
+ close_or_resolve
124
123
  else
125
124
  # @type var response: HTTPX::Response
126
125
  parse(request, response)
@@ -147,6 +146,7 @@ module HTTPX
147
146
 
148
147
  unless @queries.value?(connection)
149
148
  emit_resolve_error(connection)
149
+ close_or_resolve
150
150
  return
151
151
  end
152
152
 
@@ -156,10 +156,12 @@ module HTTPX
156
156
  connection = reset_hostname(host)
157
157
 
158
158
  emit_resolve_error(connection)
159
+ close_or_resolve
159
160
  when :decode_error
160
161
  host = @requests.delete(request)
161
162
  connection = reset_hostname(host)
162
163
  emit_resolve_error(connection, connection.peer.host, result)
164
+ close_or_resolve
163
165
  end
164
166
  end
165
167
 
@@ -169,6 +171,7 @@ module HTTPX
169
171
  host = @requests.delete(request)
170
172
  connection = reset_hostname(host)
171
173
  emit_resolve_error(connection)
174
+ close_or_resolve
172
175
  return
173
176
 
174
177
  else
@@ -207,9 +210,7 @@ module HTTPX
207
210
  catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |a| Resolver::Entry.new(a["data"], a["TTL"]) }) }
208
211
  end
209
212
  end
210
- return if @connections.empty?
211
-
212
- resolve
213
+ close_or_resolve(true)
213
214
  end
214
215
 
215
216
  def build_request(hostname)
@@ -252,5 +253,20 @@ module HTTPX
252
253
 
253
254
  connection
254
255
  end
256
+
257
+ def close_or_resolve(should_deactivate = false)
258
+ # drop already closed connections
259
+ @connections.shift until @connections.empty? || @connections.first.state != :closed
260
+
261
+ if (@connections - @queries.values).empty?
262
+ if should_deactivate
263
+ deactivate
264
+ else
265
+ disconnect
266
+ end
267
+ else
268
+ resolve
269
+ end
270
+ end
255
271
  end
256
272
  end
@@ -5,9 +5,6 @@ require "resolv"
5
5
 
6
6
  module HTTPX
7
7
  class Resolver::Multi
8
- include Callbacks
9
- using ArrayExtensions::FilterMap
10
-
11
8
  attr_reader :resolvers, :options
12
9
 
13
10
  def initialize(resolver_type, options)
@@ -28,42 +25,22 @@ module HTTPX
28
25
 
29
26
  def current_selector=(s)
30
27
  @current_selector = s
31
- @resolvers.each { |r| r.__send__(__method__, s) }
28
+ @resolvers.each { |r| r.current_selector = s }
32
29
  end
33
30
 
34
31
  def current_session=(s)
35
32
  @current_session = s
36
- @resolvers.each { |r| r.__send__(__method__, s) }
33
+ @resolvers.each { |r| r.current_session = s }
37
34
  end
38
35
 
39
36
  def log(*args, **kwargs, &blk)
40
- @resolvers.each { |r| r.__send__(__method__, *args, **kwargs, &blk) }
37
+ @resolvers.each { |r| r.log(*args, **kwargs, &blk) }
41
38
  end
42
39
 
43
40
  def closed?
44
41
  @resolvers.all?(&:closed?)
45
42
  end
46
43
 
47
- def empty?
48
- @resolvers.all?(&:empty?)
49
- end
50
-
51
- def inflight?
52
- @resolvers.any(&:inflight?)
53
- end
54
-
55
- def timeout
56
- @resolvers.filter_map(&:timeout).min
57
- end
58
-
59
- def close
60
- @resolvers.each(&:close)
61
- end
62
-
63
- def connections
64
- @resolvers.filter_map { |r| r.resolver_connection if r.respond_to?(:resolver_connection) }
65
- end
66
-
67
44
  def early_resolve(connection)
68
45
  hostname = connection.peer.host
69
46
  addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
@@ -92,9 +69,13 @@ module HTTPX
92
69
 
93
70
  def lazy_resolve(connection)
94
71
  @resolvers.each do |resolver|
95
- resolver << @current_session.try_clone_connection(connection, @current_selector, resolver.family)
72
+ conn_to_resolve = @current_session.try_clone_connection(connection, @current_selector, resolver.family)
73
+ resolver << conn_to_resolve
74
+
96
75
  next if resolver.empty?
97
76
 
77
+ # both the resolver and the connection it's resolving must be pineed to the session
78
+ @current_session.pin(conn_to_resolve, @current_selector)
98
79
  @current_session.select_resolver(resolver, @current_selector)
99
80
  end
100
81
  end