httpx 1.2.6 → 1.4.4

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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/doc/release_notes/1_3_0.md +18 -0
  4. data/doc/release_notes/1_3_1.md +17 -0
  5. data/doc/release_notes/1_3_2.md +6 -0
  6. data/doc/release_notes/1_3_3.md +5 -0
  7. data/doc/release_notes/1_3_4.md +6 -0
  8. data/doc/release_notes/1_4_0.md +43 -0
  9. data/doc/release_notes/1_4_1.md +19 -0
  10. data/doc/release_notes/1_4_2.md +20 -0
  11. data/doc/release_notes/1_4_3.md +11 -0
  12. data/doc/release_notes/1_4_4.md +14 -0
  13. data/lib/httpx/adapters/datadog.rb +56 -80
  14. data/lib/httpx/adapters/faraday.rb +5 -2
  15. data/lib/httpx/adapters/webmock.rb +24 -8
  16. data/lib/httpx/callbacks.rb +2 -7
  17. data/lib/httpx/chainable.rb +3 -1
  18. data/lib/httpx/connection/http1.rb +11 -7
  19. data/lib/httpx/connection/http2.rb +57 -34
  20. data/lib/httpx/connection.rb +270 -71
  21. data/lib/httpx/errors.rb +15 -4
  22. data/lib/httpx/io/ssl.rb +6 -3
  23. data/lib/httpx/io/tcp.rb +1 -1
  24. data/lib/httpx/io/unix.rb +1 -1
  25. data/lib/httpx/loggable.rb +17 -10
  26. data/lib/httpx/options.rb +30 -23
  27. data/lib/httpx/plugins/aws_sdk_authentication.rb +3 -0
  28. data/lib/httpx/plugins/aws_sigv4.rb +36 -17
  29. data/lib/httpx/plugins/callbacks.rb +13 -2
  30. data/lib/httpx/plugins/circuit_breaker.rb +11 -5
  31. data/lib/httpx/plugins/content_digest.rb +202 -0
  32. data/lib/httpx/plugins/cookies.rb +9 -6
  33. data/lib/httpx/plugins/digest_auth.rb +3 -0
  34. data/lib/httpx/plugins/expect.rb +10 -4
  35. data/lib/httpx/plugins/follow_redirects.rb +68 -33
  36. data/lib/httpx/plugins/grpc/grpc_encoding.rb +2 -0
  37. data/lib/httpx/plugins/grpc.rb +2 -2
  38. data/lib/httpx/plugins/h2c.rb +23 -20
  39. data/lib/httpx/plugins/internal_telemetry.rb +48 -1
  40. data/lib/httpx/plugins/oauth.rb +1 -1
  41. data/lib/httpx/plugins/persistent.rb +16 -0
  42. data/lib/httpx/plugins/proxy/http.rb +19 -16
  43. data/lib/httpx/plugins/proxy/socks4.rb +1 -1
  44. data/lib/httpx/plugins/proxy/socks5.rb +1 -1
  45. data/lib/httpx/plugins/proxy.rb +96 -85
  46. data/lib/httpx/plugins/retries.rb +28 -10
  47. data/lib/httpx/plugins/ssrf_filter.rb +4 -1
  48. data/lib/httpx/plugins/stream.rb +42 -18
  49. data/lib/httpx/plugins/upgrade.rb +5 -10
  50. data/lib/httpx/plugins/webdav.rb +6 -0
  51. data/lib/httpx/plugins/xml.rb +76 -0
  52. data/lib/httpx/pool.rb +73 -244
  53. data/lib/httpx/request/body.rb +50 -55
  54. data/lib/httpx/request.rb +77 -14
  55. data/lib/httpx/resolver/https.rb +17 -20
  56. data/lib/httpx/resolver/multi.rb +34 -16
  57. data/lib/httpx/resolver/native.rb +140 -61
  58. data/lib/httpx/resolver/resolver.rb +64 -19
  59. data/lib/httpx/resolver/system.rb +32 -16
  60. data/lib/httpx/resolver.rb +21 -14
  61. data/lib/httpx/response/body.rb +12 -1
  62. data/lib/httpx/response.rb +16 -9
  63. data/lib/httpx/selector.rb +170 -91
  64. data/lib/httpx/session.rb +282 -139
  65. data/lib/httpx/timers.rb +17 -2
  66. data/lib/httpx/transcoder/body.rb +15 -29
  67. data/lib/httpx/transcoder/form.rb +2 -0
  68. data/lib/httpx/transcoder/gzip.rb +0 -3
  69. data/lib/httpx/transcoder/json.rb +16 -2
  70. data/lib/httpx/transcoder/multipart/encoder.rb +11 -2
  71. data/lib/httpx/transcoder/multipart/part.rb +1 -1
  72. data/lib/httpx/transcoder/utils/deflater.rb +7 -4
  73. data/lib/httpx/transcoder.rb +0 -1
  74. data/lib/httpx/version.rb +1 -1
  75. data/lib/httpx.rb +20 -21
  76. data/sig/callbacks.rbs +2 -3
  77. data/sig/chainable.rbs +6 -2
  78. data/sig/connection/http1.rbs +2 -2
  79. data/sig/connection/http2.rbs +22 -18
  80. data/sig/connection.rbs +40 -9
  81. data/sig/errors.rbs +9 -3
  82. data/sig/httpx.rbs +3 -3
  83. data/sig/io/tcp.rbs +1 -1
  84. data/sig/io/unix.rbs +1 -1
  85. data/sig/loggable.rbs +4 -2
  86. data/sig/options.rbs +8 -13
  87. data/sig/plugins/aws_sigv4.rbs +8 -2
  88. data/sig/plugins/content_digest.rbs +51 -0
  89. data/sig/plugins/cookies/cookie.rbs +9 -0
  90. data/sig/plugins/follow_redirects.rbs +1 -1
  91. data/sig/plugins/grpc/call.rbs +4 -0
  92. data/sig/plugins/persistent.rbs +4 -1
  93. data/sig/plugins/proxy/http.rbs +3 -0
  94. data/sig/plugins/proxy/socks5.rbs +11 -3
  95. data/sig/plugins/proxy.rbs +18 -9
  96. data/sig/plugins/push_promise.rbs +6 -3
  97. data/sig/plugins/rate_limiter.rbs +2 -0
  98. data/sig/plugins/retries.rbs +1 -1
  99. data/sig/plugins/ssrf_filter.rbs +26 -0
  100. data/sig/plugins/stream.rbs +3 -0
  101. data/sig/plugins/webdav.rbs +23 -0
  102. data/sig/plugins/xml.rbs +37 -0
  103. data/sig/pool.rbs +27 -33
  104. data/sig/request/body.rbs +4 -10
  105. data/sig/request.rbs +14 -1
  106. data/sig/resolver/multi.rbs +26 -1
  107. data/sig/resolver/native.rbs +6 -3
  108. data/sig/resolver/resolver.rbs +22 -3
  109. data/sig/resolver.rbs +5 -1
  110. data/sig/response/body.rbs +2 -2
  111. data/sig/response/buffer.rbs +2 -2
  112. data/sig/response.rbs +9 -4
  113. data/sig/selector.rbs +31 -4
  114. data/sig/session.rbs +54 -20
  115. data/sig/timers.rbs +15 -4
  116. data/sig/transcoder/body.rbs +2 -4
  117. data/sig/transcoder/chunker.rbs +1 -1
  118. data/sig/transcoder/deflate.rbs +1 -0
  119. data/sig/transcoder/form.rbs +8 -0
  120. data/sig/transcoder/gzip.rbs +4 -1
  121. data/sig/transcoder/json.rbs +1 -1
  122. data/sig/transcoder/multipart.rbs +6 -4
  123. data/sig/transcoder/utils/body_reader.rbs +3 -3
  124. data/sig/transcoder/utils/deflater.rbs +2 -3
  125. metadata +32 -14
  126. data/lib/httpx/session2.rb +0 -23
  127. data/lib/httpx/transcoder/utils/inflater.rb +0 -19
  128. data/lib/httpx/transcoder/xml.rb +0 -52
  129. data/sig/transcoder/utils/inflater.rbs +0 -12
  130. data/sig/transcoder/xml.rbs +0 -22
data/lib/httpx/session.rb CHANGED
@@ -9,8 +9,6 @@ module HTTPX
9
9
  include Loggable
10
10
  include Chainable
11
11
 
12
- EMPTY_HASH = {}.freeze
13
-
14
12
  # initializes the session with a set of +options+, which will be shared by all
15
13
  # requests sent from it.
16
14
  #
@@ -19,6 +17,9 @@ module HTTPX
19
17
  @options = self.class.default_options.merge(options)
20
18
  @responses = {}
21
19
  @persistent = @options.persistent
20
+ @pool = @options.pool_class.new(@options.pool_options)
21
+ @wrapped = false
22
+ @closing = false
22
23
  wrap(&blk) if blk
23
24
  end
24
25
 
@@ -28,21 +29,54 @@ module HTTPX
28
29
  # http.get("https://wikipedia.com")
29
30
  # end # wikipedia connection closes here
30
31
  def wrap
31
- prev_persistent = @persistent
32
- @persistent = true
33
- pool.wrap do
34
- begin
35
- yield self
36
- ensure
37
- @persistent = prev_persistent
38
- close unless @persistent
32
+ prev_wrapped = @wrapped
33
+ @wrapped = true
34
+ was_initialized = false
35
+ current_selector = get_current_selector do
36
+ selector = Selector.new
37
+
38
+ set_current_selector(selector)
39
+
40
+ was_initialized = true
41
+
42
+ selector
43
+ end
44
+ begin
45
+ yield self
46
+ ensure
47
+ unless prev_wrapped
48
+ if @persistent
49
+ deactivate(current_selector)
50
+ else
51
+ close(current_selector)
52
+ end
39
53
  end
54
+ @wrapped = prev_wrapped
55
+ set_current_selector(nil) if was_initialized
40
56
  end
41
57
  end
42
58
 
43
- # closes all the active connections from the session
44
- def close(*args)
45
- pool.close(*args)
59
+ # closes all the active connections from the session.
60
+ #
61
+ # when called directly without specifying +selector+, all available connections
62
+ # will be picked up from the connection pool and closed. Connections in use
63
+ # by other sessions, or same session in a different thread, will not be reaped.
64
+ def close(selector = Selector.new)
65
+ # throw resolvers away from the pool
66
+ @pool.reset_resolvers
67
+
68
+ # preparing to throw away connections
69
+ while (connection = @pool.pop_connection)
70
+ next if connection.state == :closed
71
+
72
+ select_connection(connection, selector)
73
+ end
74
+ begin
75
+ @closing = true
76
+ selector.terminate
77
+ ensure
78
+ @closing = false
79
+ end
46
80
  end
47
81
 
48
82
  # performs one, or multple requests; it accepts:
@@ -65,10 +99,10 @@ module HTTPX
65
99
  # resp1, resp2 = session.request(["POST", "https://server.org/a", form: { "foo" => "bar" }], ["GET", "https://server.org/b"])
66
100
  # resp1, resp2 = session.request("GET", ["https://server.org/a", "https://server.org/b"], headers: { "x-api-token" => "TOKEN" })
67
101
  #
68
- def request(*args, **options)
102
+ def request(*args, **params)
69
103
  raise ArgumentError, "must perform at least one request" if args.empty?
70
104
 
71
- requests = args.first.is_a?(Request) ? args : build_requests(*args, options)
105
+ requests = args.first.is_a?(Request) ? args : build_requests(*args, params)
72
106
  responses = send_requests(*requests)
73
107
  return responses.first if responses.size == 1
74
108
 
@@ -81,133 +115,150 @@ module HTTPX
81
115
  #
82
116
  # req = session.build_request("GET", "https://server.com")
83
117
  # resp = session.request(req)
84
- def build_request(verb, uri, options = EMPTY_HASH)
85
- rklass = @options.request_class
86
- options = @options.merge(options) unless options.is_a?(Options)
87
- request = rklass.new(verb, uri, options)
118
+ def build_request(verb, uri, params = EMPTY_HASH, options = @options)
119
+ rklass = options.request_class
120
+ request = rklass.new(verb, uri, options, params)
88
121
  request.persistent = @persistent
89
122
  set_request_callbacks(request)
90
123
  request
91
124
  end
92
125
 
93
- private
94
-
95
- # returns the HTTPX::Pool object which manages the networking required to
96
- # perform requests.
97
- def pool
98
- Thread.current[:httpx_connection_pool] ||= Pool.new
126
+ def select_connection(connection, selector)
127
+ pin_connection(connection, selector)
128
+ selector.register(connection)
99
129
  end
100
130
 
101
- # callback executed when a response for a given request has been received.
102
- def on_response(request, response)
103
- @responses[request] = response
131
+ def pin_connection(connection, selector)
132
+ connection.current_session = self
133
+ connection.current_selector = selector
104
134
  end
105
135
 
106
- # callback executed when an HTTP/2 promise frame has been received.
107
- def on_promise(_, stream)
108
- log(level: 2) { "#{stream.id}: refusing stream!" }
109
- stream.refuse
136
+ alias_method :select_resolver, :select_connection
137
+
138
+ def deselect_connection(connection, selector, cloned = false)
139
+ selector.deregister(connection)
140
+
141
+ # when connections coalesce
142
+ return if connection.state == :idle
143
+
144
+ return if cloned
145
+
146
+ return if @closing && connection.state == :closed
147
+
148
+ @pool.checkin_connection(connection)
110
149
  end
111
150
 
112
- # returns the corresponding HTTP::Response to the given +request+ if it has been received.
113
- def fetch_response(request, _, _)
114
- @responses.delete(request)
151
+ def deselect_resolver(resolver, selector)
152
+ selector.deregister(resolver)
153
+
154
+ return if @closing && resolver.closed?
155
+
156
+ @pool.checkin_resolver(resolver)
115
157
  end
116
158
 
117
- # returns the HTTPX::Connection through which the +request+ should be sent through.
118
- def find_connection(request, connections, options)
119
- uri = request.uri
159
+ def try_clone_connection(connection, selector, family)
160
+ connection.family ||= family
120
161
 
121
- connection = pool.find_connection(uri, options) || init_connection(uri, options)
122
- unless connections.nil? || connections.include?(connection)
123
- connections << connection
124
- set_connection_callbacks(connection, connections, options)
125
- end
126
- connection
162
+ return connection if connection.family == family
163
+
164
+ new_connection = connection.class.new(connection.origin, connection.options)
165
+
166
+ new_connection.family = family
167
+
168
+ connection.sibling = new_connection
169
+
170
+ do_init_connection(new_connection, selector)
171
+ new_connection
127
172
  end
128
173
 
129
- def send_request(request, connections, options = request.options)
130
- error = catch(:resolve_error) do
131
- connection = find_connection(request, connections, options)
132
- connection.send(request)
174
+ # returns the HTTPX::Connection through which the +request+ should be sent through.
175
+ def find_connection(request_uri, selector, options)
176
+ if (connection = selector.find_connection(request_uri, options))
177
+ return connection
133
178
  end
134
- return unless error.is_a?(Error)
135
179
 
136
- request.emit(:response, ErrorResponse.new(request, error, options))
137
- end
180
+ connection = @pool.checkout_connection(request_uri, options)
138
181
 
139
- # sets the callbacks on the +connection+ required to process certain specific
140
- # connection lifecycle events which deal with request rerouting.
141
- def set_connection_callbacks(connection, connections, options, cloned: false)
142
- connection.only(:misdirected) do |misdirected_request|
143
- other_connection = connection.create_idle(ssl: { alpn_protocols: %w[http/1.1] })
144
- other_connection.merge(connection)
145
- catch(:coalesced) do
146
- pool.init_connection(other_connection, options)
182
+ case connection.state
183
+ when :idle
184
+ do_init_connection(connection, selector)
185
+ when :open
186
+ if options.io
187
+ select_connection(connection, selector)
188
+ else
189
+ pin_connection(connection, selector)
147
190
  end
148
- set_connection_callbacks(other_connection, connections, options)
149
- connections << other_connection
150
- misdirected_request.transition(:idle)
151
- other_connection.send(misdirected_request)
152
- end
153
- connection.only(:altsvc) do |alt_origin, origin, alt_params|
154
- other_connection = build_altsvc_connection(connection, connections, alt_origin, origin, alt_params, options)
155
- connections << other_connection if other_connection
191
+ when :closing, :closed
192
+ connection.idling
193
+ select_connection(connection, selector)
194
+ else
195
+ pin_connection(connection, selector)
156
196
  end
157
- connection.only(:cloned) do |cloned_conn|
158
- set_connection_callbacks(cloned_conn, connections, options, cloned: true)
159
- connections << cloned_conn
160
- end unless cloned
161
- end
162
-
163
- # returns an HTTPX::Connection for the negotiated Alternative Service (or none).
164
- def build_altsvc_connection(existing_connection, connections, alt_origin, origin, alt_params, options)
165
- # do not allow security downgrades on altsvc negotiation
166
- return if existing_connection.origin.scheme == "https" && alt_origin.scheme != "https"
167
197
 
168
- altsvc = AltSvc.cached_altsvc_set(origin, alt_params.merge("origin" => alt_origin))
198
+ connection
199
+ end
169
200
 
170
- # altsvc already exists, somehow it wasn't advertised, probably noop
171
- return unless altsvc
201
+ private
172
202
 
173
- alt_options = options.merge(ssl: options.ssl.merge(hostname: URI(origin).host))
203
+ def deactivate(selector)
204
+ selector.each_connection do |connection|
205
+ connection.deactivate
206
+ deselect_connection(connection, selector) if connection.state == :inactive
207
+ end
208
+ end
174
209
 
175
- connection = pool.find_connection(alt_origin, alt_options) || init_connection(alt_origin, alt_options)
210
+ # callback executed when a response for a given request has been received.
211
+ def on_response(request, response)
212
+ @responses[request] = response
213
+ end
176
214
 
177
- # advertised altsvc is the same origin being used, ignore
178
- return if connection == existing_connection
215
+ # callback executed when an HTTP/2 promise frame has been received.
216
+ def on_promise(_, stream)
217
+ log(level: 2) { "#{stream.id}: refusing stream!" }
218
+ stream.refuse
219
+ end
179
220
 
180
- connection.extend(AltSvc::ConnectionMixin) unless connection.is_a?(AltSvc::ConnectionMixin)
221
+ # returns the corresponding HTTP::Response to the given +request+ if it has been received.
222
+ def fetch_response(request, _selector, _options)
223
+ @responses.delete(request)
224
+ end
181
225
 
182
- set_connection_callbacks(connection, connections, alt_options)
226
+ # sends the +request+ to the corresponding HTTPX::Connection
227
+ def send_request(request, selector, options = request.options)
228
+ error = begin
229
+ catch(:resolve_error) do
230
+ connection = find_connection(request.uri, selector, options)
231
+ connection.send(request)
232
+ end
233
+ rescue StandardError => e
234
+ e
235
+ end
236
+ return unless error && error.is_a?(Exception)
183
237
 
184
- log(level: 1) { "#{origin} alt-svc: #{alt_origin}" }
238
+ raise error unless error.is_a?(Error)
185
239
 
186
- connection.merge(existing_connection)
187
- existing_connection.terminate
188
- connection
189
- rescue UnsupportedSchemeError
190
- altsvc["noop"] = true
191
- nil
240
+ request.emit(:response, ErrorResponse.new(request, error))
192
241
  end
193
242
 
194
243
  # returns a set of HTTPX::Request objects built from the given +args+ and +options+.
195
- def build_requests(*args, options)
196
- request_options = @options.merge(options)
197
-
244
+ def build_requests(*args, params)
198
245
  requests = if args.size == 1
199
246
  reqs = args.first
200
- reqs.map do |verb, uri, opts = EMPTY_HASH|
201
- build_request(verb, uri, request_options.merge(opts))
247
+ reqs.map do |verb, uri, ps = EMPTY_HASH|
248
+ request_params = params
249
+ request_params = request_params.merge(ps) unless ps.empty?
250
+ build_request(verb, uri, request_params)
202
251
  end
203
252
  else
204
253
  verb, uris = args
205
254
  if uris.respond_to?(:each)
206
- uris.enum_for(:each).map do |uri, opts = EMPTY_HASH|
207
- build_request(verb, uri, request_options.merge(opts))
255
+ uris.enum_for(:each).map do |uri, ps = EMPTY_HASH|
256
+ request_params = params
257
+ request_params = request_params.merge(ps) unless ps.empty?
258
+ build_request(verb, uri, request_params)
208
259
  end
209
260
  else
210
- [build_request(verb, uris, request_options)]
261
+ [build_request(verb, uris, params)]
211
262
  end
212
263
  end
213
264
  raise ArgumentError, "wrong number of URIs (given 0, expect 1..+1)" if requests.empty?
@@ -220,67 +271,159 @@ module HTTPX
220
271
  request.on(:promise, &method(:on_promise))
221
272
  end
222
273
 
223
- def init_connection(uri, options)
224
- connection = options.connection_class.new(uri, options)
225
- catch(:coalesced) do
226
- pool.init_connection(connection, options)
227
- connection
228
- end
274
+ def do_init_connection(connection, selector)
275
+ resolve_connection(connection, selector) unless connection.family
229
276
  end
230
277
 
231
278
  # sends an array of HTTPX::Request +requests+, returns the respective array of HTTPX::Response objects.
232
279
  def send_requests(*requests)
233
- connections = _send_requests(requests)
234
- receive_requests(requests, connections)
280
+ selector = get_current_selector { Selector.new }
281
+ begin
282
+ _send_requests(requests, selector)
283
+ receive_requests(requests, selector)
284
+ ensure
285
+ unless @wrapped
286
+ if @persistent
287
+ deactivate(selector)
288
+ else
289
+ close(selector)
290
+ end
291
+ end
292
+ end
235
293
  end
236
294
 
237
295
  # sends an array of HTTPX::Request objects
238
- def _send_requests(requests)
239
- connections = []
240
-
296
+ def _send_requests(requests, selector)
241
297
  requests.each do |request|
242
- send_request(request, connections)
298
+ send_request(request, selector)
243
299
  end
244
-
245
- connections
246
300
  end
247
301
 
248
302
  # returns the array of HTTPX::Response objects corresponding to the array of HTTPX::Request +requests+.
249
- def receive_requests(requests, connections)
303
+ def receive_requests(requests, selector)
250
304
  # @type var responses: Array[response]
251
305
  responses = []
252
306
 
253
- begin
254
- # guarantee ordered responses
255
- loop do
256
- request = requests.first
307
+ # guarantee ordered responses
308
+ loop do
309
+ request = requests.first
310
+
311
+ return responses unless request
257
312
 
258
- return responses unless request
313
+ catch(:coalesced) { selector.next_tick } until (response = fetch_response(request, selector, request.options))
314
+ request.emit(:complete, response)
259
315
 
260
- catch(:coalesced) { pool.next_tick } until (response = fetch_response(request, connections, request.options))
316
+ responses << response
317
+ requests.shift
261
318
 
319
+ break if requests.empty?
320
+
321
+ next unless selector.empty?
322
+
323
+ # in some cases, the pool of connections might have been drained because there was some
324
+ # handshake error, and the error responses have already been emitted, but there was no
325
+ # opportunity to traverse the requests, hence we're returning only a fraction of the errors
326
+ # we were supposed to. This effectively fetches the existing responses and return them.
327
+ while (request = requests.shift)
328
+ response = fetch_response(request, selector, request.options)
329
+ request.emit(:complete, response) if response
262
330
  responses << response
263
- requests.shift
331
+ end
332
+ break
333
+ end
334
+ responses
335
+ end
264
336
 
265
- break if requests.empty?
337
+ def resolve_connection(connection, selector)
338
+ if connection.addresses || connection.open?
339
+ #
340
+ # there are two cases in which we want to activate initialization of
341
+ # connection immediately:
342
+ #
343
+ # 1. when the connection already has addresses, i.e. it doesn't need to
344
+ # resolve a name (not the same as name being an IP, yet)
345
+ # 2. when the connection is initialized with an external already open IO.
346
+ #
347
+ on_resolver_connection(connection, selector)
348
+ return
349
+ end
266
350
 
267
- next unless pool.empty?
351
+ resolver = find_resolver_for(connection, selector)
268
352
 
269
- # in some cases, the pool of connections might have been drained because there was some
270
- # handshake error, and the error responses have already been emitted, but there was no
271
- # opportunity to traverse the requests, hence we're returning only a fraction of the errors
272
- # we were supposed to. This effectively fetches the existing responses and return them.
273
- while (request = requests.shift)
274
- responses << fetch_response(request, connections, request.options)
275
- end
276
- break
353
+ resolver.early_resolve(connection) || resolver.lazy_resolve(connection)
354
+ end
355
+
356
+ def on_resolver_connection(connection, selector)
357
+ from_pool = false
358
+ found_connection = selector.find_mergeable_connection(connection) || begin
359
+ from_pool = true
360
+ @pool.checkout_mergeable_connection(connection)
361
+ end
362
+
363
+ return select_connection(connection, selector) unless found_connection
364
+
365
+ if found_connection.open?
366
+ coalesce_connections(found_connection, connection, selector, from_pool)
367
+ else
368
+ found_connection.once(:open) do
369
+ next unless found_connection.current_session == self
370
+
371
+ coalesce_connections(found_connection, connection, selector, from_pool)
277
372
  end
278
- responses
279
- ensure
280
- if @persistent
281
- pool.deactivate(connections)
282
- else
283
- close(connections)
373
+ end
374
+ end
375
+
376
+ def on_resolver_close(resolver, selector)
377
+ return if resolver.closed?
378
+
379
+ deselect_resolver(resolver, selector)
380
+ resolver.close unless resolver.closed?
381
+ end
382
+
383
+ def find_resolver_for(connection, selector)
384
+ resolver = selector.find_resolver(connection.options)
385
+
386
+ unless resolver
387
+ resolver = @pool.checkout_resolver(connection.options)
388
+ resolver.current_session = self
389
+ resolver.current_selector = selector
390
+ end
391
+
392
+ resolver
393
+ end
394
+
395
+ # coalesces +conn2+ into +conn1+. if +conn1+ was loaded from the connection pool
396
+ # (it is known via +from_pool+), then it adds its to the +selector+.
397
+ def coalesce_connections(conn1, conn2, selector, from_pool)
398
+ unless conn1.coalescable?(conn2)
399
+ select_connection(conn2, selector)
400
+ @pool.checkin_connection(conn1) if from_pool
401
+ return false
402
+ end
403
+
404
+ conn2.coalesced_connection = conn1
405
+ select_connection(conn1, selector) if from_pool
406
+ deselect_connection(conn2, selector)
407
+ true
408
+ end
409
+
410
+ def get_current_selector
411
+ selector_store[self] || (yield if block_given?)
412
+ end
413
+
414
+ def set_current_selector(selector)
415
+ if selector
416
+ selector_store[self] = selector
417
+ else
418
+ selector_store.delete(self)
419
+ end
420
+ end
421
+
422
+ def selector_store
423
+ th_current = Thread.current
424
+ th_current.thread_variable_get(:httpx_persistent_selector_store) || begin
425
+ {}.compare_by_identity.tap do |store|
426
+ th_current.thread_variable_set(:httpx_persistent_selector_store, store)
284
427
  end
285
428
  end
286
429
  end
data/lib/httpx/timers.rb CHANGED
@@ -26,7 +26,7 @@ module HTTPX
26
26
 
27
27
  @next_interval_at = nil
28
28
 
29
- interval
29
+ Timer.new(interval, callback)
30
30
  end
31
31
 
32
32
  def wait_interval
@@ -43,11 +43,22 @@ module HTTPX
43
43
 
44
44
  elapsed_time = Utils.elapsed_time(@next_interval_at)
45
45
 
46
- @intervals.delete_if { |interval| interval.elapse(elapsed_time) <= 0 }
46
+ @intervals = @intervals.drop_while { |interval| interval.elapse(elapsed_time) <= 0 }
47
47
 
48
48
  @next_interval_at = nil if @intervals.empty?
49
49
  end
50
50
 
51
+ class Timer
52
+ def initialize(interval, callback)
53
+ @interval = interval
54
+ @callback = callback
55
+ end
56
+
57
+ def cancel
58
+ @interval.delete(@callback)
59
+ end
60
+ end
61
+
51
62
  class Interval
52
63
  include Comparable
53
64
 
@@ -63,6 +74,10 @@ module HTTPX
63
74
  @on_empty = blk
64
75
  end
65
76
 
77
+ def cancel
78
+ @on_empty.call
79
+ end
80
+
66
81
  def <=>(other)
67
82
  @interval <=> other.interval
68
83
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
3
+ require "delegate"
4
4
 
5
5
  module HTTPX::Transcoder
6
6
  module Body
@@ -8,46 +8,32 @@ module HTTPX::Transcoder
8
8
 
9
9
  module_function
10
10
 
11
- class Encoder
12
- extend Forwardable
13
-
14
- def_delegator :@raw, :to_s
15
-
11
+ class Encoder < SimpleDelegator
16
12
  def initialize(body)
17
- @raw = body
13
+ body = body.open(File::RDONLY, encoding: Encoding::BINARY) if Object.const_defined?(:Pathname) && body.is_a?(Pathname)
14
+ @body = body
15
+ super(body)
18
16
  end
19
17
 
20
18
  def bytesize
21
- if @raw.respond_to?(:bytesize)
22
- @raw.bytesize
23
- elsif @raw.respond_to?(:to_ary)
24
- @raw.sum(&:bytesize)
25
- elsif @raw.respond_to?(:size)
26
- @raw.size || Float::INFINITY
27
- elsif @raw.respond_to?(:length)
28
- @raw.length || Float::INFINITY
29
- elsif @raw.respond_to?(:each)
19
+ if @body.respond_to?(:bytesize)
20
+ @body.bytesize
21
+ elsif @body.respond_to?(:to_ary)
22
+ @body.sum(&:bytesize)
23
+ elsif @body.respond_to?(:size)
24
+ @body.size || Float::INFINITY
25
+ elsif @body.respond_to?(:length)
26
+ @body.length || Float::INFINITY
27
+ elsif @body.respond_to?(:each)
30
28
  Float::INFINITY
31
29
  else
32
- raise Error, "cannot determine size of body: #{@raw.inspect}"
30
+ raise Error, "cannot determine size of body: #{@body.inspect}"
33
31
  end
34
32
  end
35
33
 
36
34
  def content_type
37
35
  "application/octet-stream"
38
36
  end
39
-
40
- private
41
-
42
- def respond_to_missing?(meth, *args)
43
- @raw.respond_to?(meth, *args) || super
44
- end
45
-
46
- def method_missing(meth, *args, &block)
47
- return super unless @raw.respond_to?(meth)
48
-
49
- @raw.__send__(meth, *args, &block)
50
- end
51
37
  end
52
38
 
53
39
  def encode(body)
@@ -20,6 +20,8 @@ module HTTPX
20
20
 
21
21
  def_delegator :@raw, :bytesize
22
22
 
23
+ def_delegator :@raw, :==
24
+
23
25
  def initialize(form)
24
26
  @raw = form.each_with_object("".b) do |(key, val), buf|
25
27
  HTTPX::Transcoder.normalize_keys(key, val) do |k, v|
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
4
- require "uri"
5
- require "stringio"
6
3
  require "zlib"
7
4
 
8
5
  module HTTPX