httpx 1.7.6 → 1.7.7

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_7_7.md +17 -0
  3. data/lib/httpx/adapters/faraday.rb +0 -10
  4. data/lib/httpx/adapters/webmock.rb +1 -1
  5. data/lib/httpx/connection/http1.rb +37 -31
  6. data/lib/httpx/connection/http2.rb +13 -13
  7. data/lib/httpx/connection.rb +4 -4
  8. data/lib/httpx/io/ssl.rb +4 -4
  9. data/lib/httpx/loggable.rb +1 -1
  10. data/lib/httpx/parser/http1.rb +0 -1
  11. data/lib/httpx/plugins/auth/digest.rb +6 -0
  12. data/lib/httpx/plugins/circuit_breaker/circuit.rb +1 -0
  13. data/lib/httpx/plugins/cookies/cookie.rb +0 -1
  14. data/lib/httpx/plugins/digest_auth.rb +3 -1
  15. data/lib/httpx/plugins/follow_redirects.rb +13 -1
  16. data/lib/httpx/plugins/h2c.rb +2 -12
  17. data/lib/httpx/plugins/proxy/http.rb +1 -1
  18. data/lib/httpx/plugins/proxy.rb +1 -1
  19. data/lib/httpx/plugins/response_cache.rb +11 -4
  20. data/lib/httpx/plugins/retries.rb +5 -5
  21. data/lib/httpx/plugins/ssrf_filter.rb +1 -1
  22. data/lib/httpx/plugins/tracing.rb +1 -6
  23. data/lib/httpx/plugins/upgrade/h2.rb +1 -11
  24. data/lib/httpx/plugins/upgrade.rb +17 -17
  25. data/lib/httpx/request.rb +15 -2
  26. data/lib/httpx/selector.rb +6 -1
  27. data/lib/httpx/session.rb +27 -23
  28. data/lib/httpx/version.rb +1 -1
  29. data/sig/connection/http1.rbs +3 -1
  30. data/sig/connection/http2.rbs +1 -1
  31. data/sig/loggable.rbs +1 -1
  32. data/sig/plugins/auth/basic.rbs +1 -1
  33. data/sig/plugins/auth/digest.rbs +1 -1
  34. data/sig/plugins/auth/ntlm.rbs +2 -0
  35. data/sig/plugins/follow_redirects.rbs +1 -1
  36. data/sig/plugins/proxy.rbs +1 -0
  37. data/sig/plugins/response_cache.rbs +2 -0
  38. data/sig/plugins/tracing.rbs +1 -1
  39. data/sig/request.rbs +3 -0
  40. data/sig/session.rbs +0 -2
  41. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b34abe93b42862837d9131524d6e9663a93474f70c65ac2a10946eb62c9aeb14
4
- data.tar.gz: '0853b845878005ace796106f79b735d9d75e205b7ab28c03a3786b53233d6a4b'
3
+ metadata.gz: 64a496153f6b67985861f18f1ab86793d356bfb1f49045346e55c4df8a991881
4
+ data.tar.gz: 449bcbe6d7acf74e744f296dbfdf2530fe411b66bfecfba5552e5f6185465ea0
5
5
  SHA512:
6
- metadata.gz: 0be425ba5468990b31fb3fe04b97ee8c727cc1f16690decc2057314c3645e5baa21fae9b54585bc6e628410374dbfd9b8676df3aaed180b321a9b59cbbad2853
7
- data.tar.gz: 3cbe9b0e2952c7d330bffe727f2b478f53152d431a306d48f5f41cb5033e9ba7fa2ddec9a319aed49f051c8e9e6e2d8e489ecccc83a6c622246ef6922c9bbecc
6
+ metadata.gz: '02781b0d9ce574816cb07722aa5f22383c75872b2d782ba9bfc651b9b94b5ffb03860f01cfebdd2de5d38d28e40f7388595b751d549f7ec791e5bf4a9fd3b9e9'
7
+ data.tar.gz: 7f5de973f936e7fa6265918d7d8facf2602367c85cbc90c3ad09b92b98c01f34f9f9513a0faff0498f945d12cb41938b232bf74b96a45eb03ab680fd721ed43f
@@ -0,0 +1,17 @@
1
+ # 1.7.7
2
+
3
+ ## Improvements
4
+
5
+ * ssl cert info will also be redacted when `:debug_redact` option is enabled.
6
+ * fix performance regression of #receive_requests recent loop change (which also improved performance of previous baseline).
7
+ * header/data logs indexed by request (instead of parser).
8
+
9
+ ## Bugfixes
10
+
11
+ * proxy: fixed issue when basic auth to proxy failed with 407 and it'd raise NoMethodError when retrying (marked as unretriable now).
12
+ * selector: prevent removal of wrong selectable when previous closed via `#on_close` during selectables traversal.
13
+ * http1: only reset requests which weren't completed (prevent issuing errors twice).
14
+ * http1: forego clearing buffer on reset (avoid deleting potential next request bytes).
15
+ * response_cache: reset cached response age on successful cache revalidation, as per RFC.
16
+ * rate_limiter: do not apply jitter when delay comes from `retry-after` header.
17
+ * digest: digest header parsing issues results in an error response (instead of an exception raised).
@@ -182,15 +182,6 @@ module Faraday
182
182
  @on_response
183
183
  end
184
184
  end
185
-
186
- def on_complete(&blk)
187
- if blk
188
- @on_complete = blk
189
- self
190
- else
191
- @on_complete
192
- end
193
- end
194
185
  end
195
186
 
196
187
  include RequestMixin
@@ -224,7 +215,6 @@ module Faraday
224
215
  Array(responses).each_with_index do |response, index|
225
216
  handler = @handlers[index]
226
217
  handler.on_response.call(response)
227
- handler.on_complete.call(handler.env) if handler.on_complete
228
218
  end
229
219
  end
230
220
  end
@@ -132,7 +132,7 @@ module WebMock
132
132
  request.transition(:done)
133
133
  response.finish!
134
134
  request.response = response
135
- request.emit(:response, response)
135
+ request.emit_response(response)
136
136
  request_signature.headers = request.headers.to_h
137
137
 
138
138
  response << mock_response.body.dup unless response.is_a?(HTTPX::ErrorResponse)
@@ -54,9 +54,12 @@ module HTTPX
54
54
  end
55
55
 
56
56
  def reset_requests
57
- requests = @requests
58
- requests.each { |r| r.transition(:idle) }
59
- @pending.unshift(*requests)
57
+ @requests.reverse_each do |request|
58
+ next if request.response
59
+
60
+ request.transition(:idle)
61
+ @pending.unshift(request)
62
+ end
60
63
  @requests.clear
61
64
  end
62
65
 
@@ -116,30 +119,32 @@ module HTTPX
116
119
  end
117
120
 
118
121
  def on_headers(h)
119
- @request = @requests.first
122
+ request = @request = @requests.first
120
123
 
121
- return if @request.response
124
+ return if request.response
122
125
 
123
- log(level: 2) { "headers received" }
124
- headers = @request.options.headers_class.new(h)
125
- response = @request.options.response_class.new(@request,
126
- @parser.status_code,
127
- @parser.http_version.join("."),
128
- headers)
129
- log(color: :yellow) { "-> HEADLINE: #{response.status} HTTP/#{@parser.http_version.join(".")}" }
130
- log(color: :yellow) { response.headers.each.map { |f, v| "-> HEADER: #{f}: #{log_redact_headers(v)}" }.join("\n") }
126
+ request.log(level: 2) { "headers received" }
127
+ headers = request.options.headers_class.new(h)
128
+ response = request.options.response_class.new(request,
129
+ @parser.status_code,
130
+ @parser.http_version.join("."),
131
+ headers)
132
+ request.log(color: :yellow) { "-> HEADLINE: #{response.status} HTTP/#{@parser.http_version.join(".")}" }
133
+ request.log(color: :yellow) { response.headers.each.map { |f, v| "-> HEADER: #{f}: #{log_redact_headers(v)}" }.join("\n") }
131
134
 
132
- @request.response = response
135
+ request.response = response
133
136
  on_complete if response.finished?
134
137
  end
135
138
 
136
139
  def on_trailers(h)
137
- return unless @request
140
+ request = @request
138
141
 
139
- response = @request.response
140
- log(level: 2) { "trailer headers received" }
142
+ return unless request
141
143
 
142
- log(color: :yellow) { h.each.map { |f, v| "-> HEADER: #{f}: #{log_redact_headers(v.join(", "))}" }.join("\n") }
144
+ response = request.response
145
+
146
+ request.log(level: 2) { "trailer headers received" }
147
+ request.log(color: :yellow) { h.each.map { |f, v| "-> HEADER: #{f}: #{log_redact_headers(v.join(", "))}" }.join("\n") }
143
148
  response.merge_headers(h)
144
149
  end
145
150
 
@@ -149,8 +154,9 @@ module HTTPX
149
154
  return unless request
150
155
 
151
156
  begin
152
- log(color: :green) { "-> DATA: #{chunk.bytesize} bytes..." }
153
- log(level: 2, color: :green) { "-> #{log_redact_body(chunk.inspect)}" }
157
+ request.log(color: :green) { "-> DATA: #{chunk.bytesize} bytes..." }
158
+ request.log(level: 2, color: :green) { "-> #{log_redact_body(chunk.inspect)}" }
159
+
154
160
  response = request.response
155
161
 
156
162
  response << chunk
@@ -166,7 +172,7 @@ module HTTPX
166
172
 
167
173
  return unless request
168
174
 
169
- log(level: 2) { "parsing complete" }
175
+ request.log(level: 2) { "parsing complete" }
170
176
  dispatch(request)
171
177
  end
172
178
 
@@ -293,7 +299,7 @@ module HTTPX
293
299
  return if @max_concurrent_requests == 1
294
300
 
295
301
  @requests.each do |r|
296
- r.transition(:idle)
302
+ r.transition(:idle) if r.response.nil?
297
303
 
298
304
  # when we disable pipelining, we still want to try keep-alive.
299
305
  # only when keep-alive with one request fails, do we fallback to
@@ -362,10 +368,10 @@ module HTTPX
362
368
  def join_headers(request)
363
369
  headline = join_headline(request)
364
370
  @buffer << headline << CRLF
365
- log(color: :yellow) { "<- HEADLINE: #{headline.chomp.inspect}" }
371
+ request.log(color: :yellow) { "<- HEADLINE: #{headline.chomp.inspect}" }
366
372
  extra_headers = set_protocol_headers(request)
367
- join_headers2(request.headers.each(extra_headers))
368
- log { "<- " }
373
+ join_headers2(request, request.headers.each(extra_headers))
374
+ request.log { "<- " }
369
375
  @buffer << CRLF
370
376
  end
371
377
 
@@ -373,8 +379,8 @@ module HTTPX
373
379
  return if request.body.empty?
374
380
 
375
381
  while (chunk = request.drain_body)
376
- log(color: :green) { "<- DATA: #{chunk.bytesize} bytes..." }
377
- log(level: 2, color: :green) { "<- #{log_redact_body(chunk.inspect)}" }
382
+ request.log(color: :green) { "<- DATA: #{chunk.bytesize} bytes..." }
383
+ request.log(level: 2, color: :green) { "<- #{log_redact_body(chunk.inspect)}" }
378
384
  @buffer << chunk
379
385
  throw(:buffer_full, request) if @buffer.full?
380
386
  end
@@ -387,15 +393,15 @@ module HTTPX
387
393
  def join_trailers(request)
388
394
  return unless request.trailers? && request.callbacks_for?(:trailers)
389
395
 
390
- join_headers2(request.trailers)
391
- log { "<- " }
396
+ join_headers2(request, request.trailers)
397
+ request.log { "<- " }
392
398
  @buffer << CRLF
393
399
  end
394
400
 
395
- def join_headers2(headers)
401
+ def join_headers2(request, headers)
396
402
  headers.each do |field, value|
397
403
  field = capitalized(field)
398
- log(color: :yellow) { "<- HEADER: #{[field, log_redact_headers(value)].join(": ")}" }
404
+ request.log(color: :yellow) { "<- HEADER: #{[field, log_redact_headers(value)].join(": ")}" }
399
405
  @buffer << "#{field}: #{value}#{CRLF}"
400
406
  end
401
407
  end
@@ -242,11 +242,11 @@ module HTTPX
242
242
  extra_headers = set_protocol_headers(request)
243
243
 
244
244
  if request.headers.key?("host")
245
- log { "forbidden \"host\" header found (#{log_redact_headers(request.headers["host"])}), will use it as authority..." }
245
+ request.log { "forbidden \"host\" header found (#{log_redact_headers(request.headers["host"])}), will use it as authority..." }
246
246
  extra_headers[":authority"] = request.headers["host"]
247
247
  end
248
248
 
249
- log(level: 1, color: :yellow) do
249
+ request.log(level: 1, color: :yellow) do
250
250
  "\n#{request.headers.merge(extra_headers).each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{log_redact_headers(v)}" }.join("\n")}"
251
251
  end
252
252
  stream.headers(request.headers.each(extra_headers), end_stream: request.body.empty?)
@@ -258,7 +258,7 @@ module HTTPX
258
258
  return
259
259
  end
260
260
 
261
- log(level: 1, color: :yellow) do
261
+ request.log(level: 1, color: :yellow) do
262
262
  request.trailers.each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{log_redact_headers(v)}" }.join("\n")
263
263
  end
264
264
  stream.headers(request.trailers.each, end_stream: true)
@@ -286,8 +286,8 @@ module HTTPX
286
286
  end
287
287
 
288
288
  def send_chunk(request, stream, chunk, next_chunk)
289
- log(level: 1, color: :green) { "#{stream.id}: -> DATA: #{chunk.bytesize} bytes..." }
290
- log(level: 2, color: :green) { "#{stream.id}: -> #{log_redact_body(chunk.inspect)}" }
289
+ request.log(level: 1, color: :green) { "#{stream.id}: -> DATA: #{chunk.bytesize} bytes..." }
290
+ request.log(level: 2, color: :green) { "#{stream.id}: -> #{log_redact_body(chunk.inspect)}" }
291
291
  stream.data(chunk, end_stream: end_stream?(request, next_chunk))
292
292
  end
293
293
 
@@ -303,11 +303,11 @@ module HTTPX
303
303
  response = request.response
304
304
 
305
305
  if response.is_a?(Response) && response.version == "2.0"
306
- on_stream_trailers(stream, response, h)
306
+ on_stream_trailers(stream, request, response, h)
307
307
  return
308
308
  end
309
309
 
310
- log(color: :yellow) do
310
+ request.log(color: :yellow) do
311
311
  h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{k == ":status" ? v : log_redact_headers(v)}" }.join("\n")
312
312
  end
313
313
  _, status = h.shift
@@ -319,16 +319,16 @@ module HTTPX
319
319
  handle(request, stream) if request.expects?
320
320
  end
321
321
 
322
- def on_stream_trailers(stream, response, h)
323
- log(color: :yellow) do
322
+ def on_stream_trailers(stream, request, response, h)
323
+ request.log(color: :yellow) do
324
324
  h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{log_redact_headers(v)}" }.join("\n")
325
325
  end
326
326
  response.merge_headers(h)
327
327
  end
328
328
 
329
329
  def on_stream_data(stream, request, data)
330
- log(level: 1, color: :green) { "#{stream.id}: <- DATA: #{data.bytesize} bytes..." }
331
- log(level: 2, color: :green) { "#{stream.id}: <- #{log_redact_body(data.inspect)}" }
330
+ request.log(level: 1, color: :green) { "#{stream.id}: <- DATA: #{data.bytesize} bytes..." }
331
+ request.log(level: 2, color: :green) { "#{stream.id}: <- #{log_redact_body(data.inspect)}" }
332
332
  request.response << data
333
333
  end
334
334
 
@@ -337,14 +337,14 @@ module HTTPX
337
337
  stream.close
338
338
  end
339
339
 
340
- def on_stream_half_close(stream, _request)
340
+ def on_stream_half_close(stream, request)
341
341
  unless stream.send_buffer.empty?
342
342
  stream.send_buffer.clear
343
343
  stream.data("", end_stream: true)
344
344
  end
345
345
 
346
346
  # TODO: omit log line if response already here
347
- log(level: 2) { "#{stream.id}: waiting for response..." }
347
+ request.log(level: 2) { "#{stream.id}: waiting for response..." }
348
348
  end
349
349
 
350
350
  def on_stream_close(stream, request, error)
@@ -643,7 +643,7 @@ module HTTPX
643
643
  @no_more_requests_counter = 0
644
644
  @inflight -= 1
645
645
  response.finish!
646
- request.emit(:response, response)
646
+ request.emit_response(response)
647
647
  end
648
648
  parser.on(:altsvc) do |alt_origin, origin, alt_params|
649
649
  build_altsvc_connection(alt_origin, origin, alt_params)
@@ -707,7 +707,7 @@ module HTTPX
707
707
  @inflight -= 1
708
708
  response = ErrorResponse.new(request, error)
709
709
  request.response = response
710
- request.emit(:response, response)
710
+ request.emit_response(response)
711
711
  end
712
712
  end
713
713
 
@@ -938,7 +938,7 @@ module HTTPX
938
938
  @inflight -= 1
939
939
  response = ErrorResponse.new(request, error)
940
940
  request.response = response
941
- request.emit(:response, response)
941
+ request.emit_response(response)
942
942
  end
943
943
 
944
944
  pending = @pending
@@ -955,7 +955,7 @@ module HTTPX
955
955
 
956
956
  resp = ErrorResponse.new(req, error)
957
957
  req.response = resp
958
- req.emit(:response, resp)
958
+ req.emit_response(resp)
959
959
  end
960
960
  end
961
961
 
data/lib/httpx/io/ssl.rb CHANGED
@@ -165,10 +165,10 @@ module HTTPX
165
165
  "SSL connection using #{@io.ssl_version} / #{Array(@io.cipher).first}\n" \
166
166
  "ALPN, server accepted to use #{protocol}\n" \
167
167
  "Server certificate:\n " \
168
- "subject: #{server_cert.subject}\n " \
169
- "start date: #{server_cert.not_before}\n " \
170
- "expire date: #{server_cert.not_after}\n " \
171
- "issuer: #{server_cert.issuer}\n " \
168
+ "subject: #{log_redact(server_cert.subject)}\n " \
169
+ "start date: #{log_redact(server_cert.not_before)}\n " \
170
+ "expire date: #{log_redact(server_cert.not_after)}\n " \
171
+ "issuer: #{log_redact(server_cert.issuer)}\n " \
172
172
  "SSL certificate verify ok."
173
173
  end
174
174
  end
@@ -57,7 +57,7 @@ module HTTPX
57
57
  log_redact(text, @options.debug_redact == :body)
58
58
  end
59
59
 
60
- def log_redact(text, should_redact)
60
+ def log_redact(text, should_redact = nil)
61
61
  should_redact ||= @options.debug_redact == true
62
62
 
63
63
  return text.to_s unless should_redact
@@ -29,7 +29,6 @@ module HTTPX
29
29
  @content_length = nil
30
30
  @_has_trailers = @upgrade = false
31
31
  @buffer = @buffer.to_s
32
- @buffer.clear
33
32
  end
34
33
 
35
34
  def upgrade?
@@ -24,6 +24,11 @@ module HTTPX
24
24
 
25
25
  def authenticate(request, authenticate)
26
26
  "Digest #{generate_header(request.verb, request.path, authenticate)}"
27
+ rescue StandardError => e
28
+ response = ErrorResponse.new(request, e)
29
+ request.response = response
30
+ request.emit_response(response)
31
+ nil
27
32
  end
28
33
 
29
34
  private
@@ -77,6 +82,7 @@ module HTTPX
77
82
 
78
83
  if params["algorithm"] =~ /(.*?)(-sess)?$/
79
84
  alg = Regexp.last_match(1)
85
+ raise_format_error unless alg
80
86
  algorithm = ::Digest.const_get(alg)
81
87
  raise Error, "unknown algorithm \"#{alg}\"" unless algorithm
82
88
 
@@ -17,6 +17,7 @@ module HTTPX
17
17
  @break_in = break_in
18
18
  @circuit_breaker_half_open_drip_rate = circuit_breaker_half_open_drip_rate
19
19
  @attempts = 0
20
+ @opened_at = @attempted_at = @response = nil
20
21
 
21
22
  total_real_attempts = @max_attempts * @circuit_breaker_half_open_drip_rate
22
23
  @drip_factor = (@max_attempts / total_real_attempts).round
@@ -155,7 +155,6 @@ module HTTPX
155
155
  # Tests if it is OK to send this cookie to a given `uri`. A
156
156
  # RuntimeError is raised if the cookie's domain is unknown.
157
157
  def valid_for_uri?(uri)
158
- uri = URI(uri)
159
158
  # RFC 6265 5.4
160
159
 
161
160
  return false if @secure && uri.scheme != "https"
@@ -51,7 +51,9 @@ module HTTPX
51
51
 
52
52
  if probe_response.status == 401 && digest.can_authenticate?(probe_response.headers["www-authenticate"])
53
53
  request.transition(:idle)
54
- request.authorize(digest.authenticate(request, probe_response.headers["www-authenticate"]))
54
+ if (auth_header = digest.authenticate(request, probe_response.headers["www-authenticate"]))
55
+ request.authorize(auth_header)
56
+ end
55
57
  super(request)
56
58
  else
57
59
  probe_response
@@ -157,7 +157,7 @@ module HTTPX
157
157
  response.finish!
158
158
  retry_request.response = response
159
159
  # request has terminated abruptly meanwhile
160
- retry_request.emit(:response, response)
160
+ retry_request.emit_response(response)
161
161
  else
162
162
  log { "redirecting (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
163
163
  send_request(retry_request, selector, options)
@@ -201,6 +201,17 @@ module HTTPX
201
201
  # returns the top-most original HTTPX::Request from the redirect chain
202
202
  attr_accessor :root_request
203
203
 
204
+ def initialize(*)
205
+ super
206
+ @redirect_request = nil
207
+ end
208
+
209
+ def on_response_arrived=(cb)
210
+ @redirect_request.on_response_arrived = cb if @redirect_request
211
+
212
+ super
213
+ end
214
+
204
215
  # returns the follow-up redirect request, or itself
205
216
  def redirect_request
206
217
  @redirect_request || self
@@ -210,6 +221,7 @@ module HTTPX
210
221
  def redirect_request=(req)
211
222
  @redirect_request = req
212
223
  req.root_request = @root_request || self
224
+ req.on_response_arrived = @on_response_arrived
213
225
  @response = nil
214
226
  end
215
227
 
@@ -68,23 +68,13 @@ module HTTPX
68
68
  end
69
69
 
70
70
  def upgrade_to_h2c(request, response)
71
- prev_parser = @parser
72
-
73
- if prev_parser
74
- prev_parser.reset
75
- @inflight -= prev_parser.requests.size
76
- end
71
+ enqueue_pending_requests_from_parser(@parser)
77
72
 
78
73
  @parser = request.options.h2c_class.new(@write_buffer, @options)
79
74
  set_parser_callbacks(@parser)
80
- @inflight += 1
75
+ @inflight += 1 # request is being completed below
81
76
  @parser.upgrade(request, response)
82
77
  @upgrade_protocol = "h2c"
83
-
84
- prev_parser.pending.each do |req|
85
- req.transition(:idle)
86
- send(req)
87
- end
88
78
  end
89
79
 
90
80
  private
@@ -171,7 +171,7 @@ module HTTPX
171
171
  while (req = pending.shift)
172
172
  response.finish!
173
173
  req.response = response
174
- req.emit(:response, response)
174
+ req.emit_response(response)
175
175
  end
176
176
  reset
177
177
  end
@@ -88,7 +88,7 @@ module HTTPX
88
88
  end
89
89
 
90
90
  def can_authenticate?(*args)
91
- return false unless @authenticator
91
+ return false unless @authenticator && @authenticator.respond_to?(:can_authenticate?)
92
92
 
93
93
  @authenticator.can_authenticate?(*args)
94
94
  end
@@ -140,7 +140,7 @@ module HTTPX
140
140
  cached_response = cached_response.dup
141
141
  cached_response.mark_as_cached!
142
142
  request.response = cached_response
143
- request.emit(:response, cached_response)
143
+ request.emit_response(cached_response)
144
144
  return
145
145
  end
146
146
 
@@ -220,11 +220,12 @@ module HTTPX
220
220
  end
221
221
 
222
222
  module ResponseMethods
223
- attr_writer :original_request
223
+ attr_writer :original_request, :revalidated_at
224
224
 
225
225
  def initialize(*)
226
226
  super
227
227
  @cached = false
228
+ @revalidated_at = nil
228
229
  end
229
230
 
230
231
  # a copy of the request this response was originally cached from
@@ -254,6 +255,8 @@ module HTTPX
254
255
  @body = cached_response.body.dup
255
256
 
256
257
  @body.rewind
258
+
259
+ cached_response.revalidated_at = date
257
260
  end
258
261
 
259
262
  # A response is fresh if its age has not yet exceeded its freshness lifetime.
@@ -314,9 +317,13 @@ module HTTPX
314
317
  # returns the value of the "age" header as an Integer (time since epoch).
315
318
  # if no "age" of header exists, it returns the number of seconds since {#date}.
316
319
  def age
317
- return @headers["age"].to_i if @headers.key?("age")
320
+ if (revalidated_at = @revalidated_at)
321
+ (Time.now - revalidated_at).to_i
322
+ else
323
+ return @headers["age"].to_i if @headers.key?("age")
318
324
 
319
- (Time.now - date).to_i
325
+ (Time.now - date).to_i
326
+ end
320
327
  end
321
328
 
322
329
  # returns the value of the "date" header as a Time object
@@ -149,10 +149,6 @@ module HTTPX
149
149
  prepare_to_retry(request, response)
150
150
 
151
151
  if (retry_after = when_to_retry(request, response, options)) && retry_after.positive?
152
- # apply jitter
153
- if (jitter = request.options.retry_jitter)
154
- retry_after = jitter.call(retry_after)
155
- end
156
152
 
157
153
  retry_start = Utils.now
158
154
  log { "retrying after #{retry_after} secs..." }
@@ -160,7 +156,7 @@ module HTTPX
160
156
  if (response = request.response)
161
157
  response.finish!
162
158
  # request has terminated abruptly meanwhile
163
- request.emit(:response, response)
159
+ request.emit_response(response)
164
160
  else
165
161
  log { "retrying (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
166
162
  send_request(request, selector, options)
@@ -205,6 +201,10 @@ module HTTPX
205
201
  def when_to_retry(request, response, options)
206
202
  retry_after = options.retry_after
207
203
  retry_after = retry_after.call(request, response) if retry_after.respond_to?(:call)
204
+ # apply jitter
205
+ if (jitter = request.options.retry_jitter)
206
+ retry_after = jitter.call(retry_after)
207
+ end
208
208
  retry_after
209
209
  end
210
210
 
@@ -107,7 +107,7 @@ module HTTPX
107
107
  error.set_backtrace(caller)
108
108
  response = ErrorResponse.new(request, error)
109
109
  request.response = response
110
- request.emit(:response, response)
110
+ request.emit_response(response)
111
111
  response
112
112
  end
113
113
  allowed_requests = requests.select { |req| responses[requests.index(req)].nil? }
@@ -25,12 +25,7 @@ module HTTPX::Plugins
25
25
  end
26
26
 
27
27
  def merge(tracer)
28
- case tracer
29
- when Wrapper
30
- Wrapper.new(*@tracers, *tracer.tracers)
31
- else
32
- Wrapper.new(*@tracers, tracer)
33
- end
28
+ Wrapper.new(*@tracers, *tracer.tracers)
34
29
  end
35
30
 
36
31
  def freeze
@@ -33,12 +33,7 @@ module HTTPX
33
33
  end
34
34
 
35
35
  def upgrade_to_h2
36
- prev_parser = @parser
37
-
38
- if prev_parser
39
- prev_parser.reset
40
- @inflight -= prev_parser.requests.size
41
- end
36
+ enqueue_pending_requests_from_parser(@parser)
42
37
 
43
38
  @parser = @options.http2_class.new(@write_buffer, @options)
44
39
  set_parser_callbacks(@parser)
@@ -51,11 +46,6 @@ module HTTPX
51
46
  # while the parser is already here.
52
47
  purge_after_closed
53
48
  transition(:idle)
54
-
55
- prev_parser.requests.each do |req|
56
- req.transition(:idle)
57
- send(req)
58
- end
59
49
  end
60
50
  end
61
51
  end
@@ -33,32 +33,32 @@ module HTTPX
33
33
  def fetch_response(request, selector, options)
34
34
  response = super
35
35
 
36
- if response
37
- return response unless response.is_a?(Response)
36
+ return unless response
38
37
 
39
- return response unless response.headers.key?("upgrade")
38
+ return response unless response.is_a?(Response)
40
39
 
41
- upgrade_protocol = response.headers["upgrade"].split(/ *, */).first
40
+ return response unless response.headers.key?("upgrade")
42
41
 
43
- return response unless upgrade_protocol && options.upgrade_handlers.key?(upgrade_protocol)
42
+ upgrade_protocol = response.headers["upgrade"].split(/ *, */).first
44
43
 
45
- protocol_handler = options.upgrade_handlers[upgrade_protocol]
44
+ return response unless upgrade_protocol && options.upgrade_handlers.key?(upgrade_protocol)
46
45
 
47
- return response unless protocol_handler
46
+ protocol_handler = options.upgrade_handlers[upgrade_protocol]
48
47
 
49
- log { "upgrading to #{upgrade_protocol}..." }
50
- connection = find_connection(request.uri, selector, options)
48
+ return response unless protocol_handler
51
49
 
52
- # do not upgrade already upgraded connections
53
- return if connection.upgrade_protocol == upgrade_protocol
50
+ log { "upgrading to #{upgrade_protocol}..." }
51
+ connection = find_connection(request.uri, selector, options)
54
52
 
55
- protocol_handler.call(connection, request, response)
53
+ # do not upgrade already upgraded connections
54
+ return if connection.upgrade_protocol == upgrade_protocol
56
55
 
57
- # keep in the loop if the server is switching, unless
58
- # the connection has been hijacked, in which case you want
59
- # to terminante immediately
60
- return if response.status == 101 && !connection.hijacked
61
- end
56
+ protocol_handler.call(connection, request, response)
57
+
58
+ # keep in the loop if the server is switching, unless
59
+ # the connection has been hijacked, in which case you want
60
+ # to terminante immediately
61
+ return fetch_response(request, selector, options) if response.status == 101 && !connection.hijacked
62
62
 
63
63
  response
64
64
  end
data/lib/httpx/request.rb CHANGED
@@ -45,6 +45,9 @@ module HTTPX
45
45
  # the connection the request is currently being sent to (none if before or after transaction)
46
46
  attr_writer :connection
47
47
 
48
+ # callback triggered when a response (which may not be the final response) was assigned to the request.
49
+ attr_writer :on_response_arrived
50
+
48
51
  attr_writer :persistent
49
52
 
50
53
  attr_reader :active_timeouts
@@ -94,7 +97,9 @@ module HTTPX
94
97
  raise UnsupportedSchemeError, "#{@uri}: #{@uri.scheme}: unsupported URI scheme" unless ALLOWED_URI_SCHEMES.include?(@uri.scheme)
95
98
 
96
99
  @state = :idle
97
- @connection = @response = @drainer = @peer_address = @informational_status = nil
100
+ @connection = @response =
101
+ @drainer = @peer_address =
102
+ @informational_status = @on_response_arrived = nil
98
103
  @ping = false
99
104
  @persistent = @options.persistent
100
105
  @active_timeouts = []
@@ -330,9 +335,17 @@ module HTTPX
330
335
  else
331
336
  response = ErrorResponse.new(self, error)
332
337
  self.response = response
333
- emit(:response, response)
338
+ emit_response(response)
334
339
  end
335
340
  end
341
+
342
+ def emit_response(response)
343
+ emit(:response, response)
344
+
345
+ return unless @on_response_arrived
346
+
347
+ @on_response_arrived.call
348
+ end
336
349
  end
337
350
  end
338
351
 
@@ -147,7 +147,12 @@ module HTTPX
147
147
 
148
148
  is_closed = io.state == :closed
149
149
 
150
- next(is_closed) if is_closed
150
+ if is_closed
151
+ # the process by which io was closed may have already triggered the on_close callback,
152
+ # which already deregistered the io. this check prevents it from deleting the wrong io,
153
+ # because of https://bugs.ruby-lang.org/issues/22021 .
154
+ next(@selectables.include?(io))
155
+ end
151
156
 
152
157
  if interests
153
158
  io.log(level: 2) do
data/lib/httpx/session.rb CHANGED
@@ -192,11 +192,8 @@ module HTTPX
192
192
  when :idle
193
193
  do_init_connection(connection, selector)
194
194
  when :open
195
- if options.io
196
- select_connection(connection, selector)
197
- else
198
- pin(connection, selector)
199
- end
195
+ # external io
196
+ select_connection(connection, selector)
200
197
  when :closing, :closed
201
198
  connection.idling
202
199
  if connection.addresses?
@@ -262,7 +259,7 @@ module HTTPX
262
259
 
263
260
  response = ErrorResponse.new(request, error)
264
261
  request.response = response
265
- request.emit(:response, response)
262
+ request.emit_response(response)
266
263
  end
267
264
 
268
265
  # returns a set of HTTPX::Request objects built from the given +args+ and +options+.
@@ -303,7 +300,6 @@ module HTTPX
303
300
  def send_requests(*requests)
304
301
  selector = get_current_selector { Selector.new }
305
302
  begin
306
- _send_requests(requests, selector)
307
303
  receive_requests(requests, selector)
308
304
  ensure
309
305
  unless @wrapped
@@ -316,42 +312,50 @@ module HTTPX
316
312
  end
317
313
  end
318
314
 
319
- # sends an array of HTTPX::Request objects
320
- def _send_requests(requests, selector)
321
- requests.each do |request|
322
- send_request(request, selector)
323
- end
324
- end
325
-
326
315
  # returns the array of HTTPX::Response objects corresponding to the array of HTTPX::Request +requests+.
327
316
  def receive_requests(requests, selector)
328
- waiting = 0
329
- responses = requests.map do |request|
317
+ pending_idxs = [] #: Array[Integer]
318
+ pending = 0
319
+
320
+ waiting = false
321
+
322
+ responses = requests.each_with_index.map do |request, idx|
323
+ send_request(request, selector)
324
+
330
325
  fetch_response(request, selector, request.options).tap do |response|
331
- waiting += 1 if response.nil?
326
+ if response.nil?
327
+ pending += 1
328
+ request.on_response_arrived = lambda do
329
+ pending_idxs << idx if waiting
330
+ end
331
+ end
332
332
  end
333
333
  end
334
334
 
335
- until waiting.zero? || selector.empty?
335
+ until pending.zero? || selector.empty?
336
336
  # loop on selector until at least one response has been received.
337
+ waiting = true
337
338
  catch(:coalesced) { selector.next_tick }
339
+ waiting = false
338
340
 
339
- responses.each_with_index do |response, idx|
340
- next unless response.nil?
341
-
341
+ while (idx = pending_idxs.shift)
342
342
  request = requests[idx]
343
343
 
344
344
  response = fetch_response(request, selector, request.options)
345
345
 
346
+ # stop on first pending response. this avoids traversing pending idxs all the way
347
+ # (which is more expensive in the beginning, when the array is larger and N) while
348
+ # making the next loop cheaper (because we're dropping).
346
349
  next unless response
347
350
 
348
351
  request.complete!(response)
349
352
  responses[idx] = response
350
- waiting -= 1
353
+ request.on_response_arrived = nil
354
+ pending -= 1
351
355
  end
352
356
  end
353
357
 
354
- raise Error, "something went wrong, responses not found and requests not resent" unless waiting.zero?
358
+ raise Error, "something went wrong, responses not found and requests not resent" unless pending.zero?
355
359
 
356
360
  responses
357
361
  end
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.7.6"
4
+ VERSION = "1.7.7"
5
5
  end
@@ -74,11 +74,13 @@ module HTTPX
74
74
 
75
75
  def handle: (Request request) -> void
76
76
 
77
+ def join_headline: (Request request) -> String
78
+
77
79
  def join_headers: (Request request) -> void
78
80
 
79
81
  def join_trailers: (Request request) -> void
80
82
 
81
- def join_headers2: (_Each[[String, String]] headers) -> void
83
+ def join_headers2: (Request request, _Each[[String, String]] headers) -> void
82
84
 
83
85
  def join_body: (Request request) -> void
84
86
 
@@ -76,7 +76,7 @@ module HTTPX
76
76
 
77
77
  def on_stream_headers: (::HTTP2::Stream stream, Request request, Array[[String, String]] headers) -> void
78
78
 
79
- def on_stream_trailers: (::HTTP2::Stream stream, Response response, Array[[String, String]] headers) -> void
79
+ def on_stream_trailers: (::HTTP2::Stream stream, Request request, Response response, Array[[String, String]] headers) -> void
80
80
 
81
81
  def on_stream_data: (::HTTP2::Stream stream, Request request, String data) -> void
82
82
 
data/sig/loggable.rbs CHANGED
@@ -16,6 +16,6 @@ module HTTPX
16
16
 
17
17
  def log_redact_body: (_ToS text) -> String
18
18
 
19
- def log_redact: (_ToS text, bool should_redact) -> String
19
+ def log_redact: (_ToS text, ?bool? should_redact) -> String
20
20
  end
21
21
  end
@@ -9,7 +9,7 @@ module HTTPX
9
9
 
10
10
  private
11
11
 
12
- def initialize: (string user, string password, *untyped) -> void
12
+ def initialize: (string user, string password, **untyped) -> void
13
13
 
14
14
  end
15
15
  end
@@ -12,7 +12,7 @@ module HTTPX
12
12
 
13
13
  def can_authenticate?: (String? authenticate) -> boolish
14
14
 
15
- def authenticate: (Request request, String authenticate) -> String
15
+ def authenticate: (Request request, String authenticate) -> String?
16
16
 
17
17
  private
18
18
 
@@ -10,6 +10,8 @@ module HTTPX
10
10
 
11
11
  def authenticate: (Request request, String authenticate) -> String
12
12
 
13
+ def negotiate: () -> void
14
+
13
15
  private
14
16
 
15
17
  def initialize: (string user, string password, ?domain: String?) -> void
@@ -30,7 +30,7 @@ module HTTPX
30
30
  end
31
31
 
32
32
  module RequestMethods
33
- attr_accessor root_request: instance?
33
+ attr_accessor root_request: (Request & RequestMethods)?
34
34
 
35
35
  @redirect_request: redirect_request
36
36
 
@@ -24,6 +24,7 @@ module HTTPX
24
24
  attr_reader scheme: String?
25
25
  attr_reader no_proxy: Array[String]?
26
26
 
27
+ @ns: Integer
27
28
  @uris: Array[URI::Generic | String]
28
29
  @authenticator: _Authenticator
29
30
 
@@ -51,6 +51,8 @@ module HTTPX
51
51
  module ResponseMethods
52
52
  attr_writer original_request: cacheRequest
53
53
 
54
+ attr_writer revalidated_at: Time
55
+
54
56
  @cached: bool
55
57
  @cache_control: Array[String]?
56
58
  @vary: Array[String]?
@@ -18,7 +18,7 @@ module HTTPX
18
18
 
19
19
  def initialize: (*_Tracer tracers) -> void
20
20
 
21
- def merge: (instance | _Tracer) -> void
21
+ def merge: (instance) -> void
22
22
  end
23
23
 
24
24
  interface _RetriesOptions
data/sig/request.rbs CHANGED
@@ -19,6 +19,7 @@ module HTTPX
19
19
  attr_reader active_timeouts: Array[Symbol]
20
20
 
21
21
  attr_writer connection: Connection?
22
+ attr_writer on_response_arrived: (^() -> void)?
22
23
  attr_accessor peer_address: (String | IPAddr)?
23
24
 
24
25
  attr_writer persistent: bool
@@ -84,6 +85,8 @@ module HTTPX
84
85
 
85
86
  def handle_error: (StandardError error) -> void
86
87
 
88
+ def emit_response: (response response) -> void
89
+
87
90
  private
88
91
 
89
92
  def initialize_body: (Options options) -> Transcoder::_Encoder?
data/sig/session.rbs CHANGED
@@ -62,8 +62,6 @@ module HTTPX
62
62
 
63
63
  def send_requests: (*Request) -> Array[response]
64
64
 
65
- def _send_requests: (Array[Request] requests, Selector selector) -> void
66
-
67
65
  def receive_requests: (Array[Request] requests, Selector selector) -> Array[response]
68
66
 
69
67
  def early_resolve: (resolver resolver, Connection connection) -> bool
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.7.6
4
+ version: 1.7.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
@@ -166,6 +166,7 @@ extra_rdoc_files:
166
166
  - doc/release_notes/1_7_4.md
167
167
  - doc/release_notes/1_7_5.md
168
168
  - doc/release_notes/1_7_6.md
169
+ - doc/release_notes/1_7_7.md
169
170
  files:
170
171
  - LICENSE.txt
171
172
  - README.md
@@ -304,6 +305,7 @@ files:
304
305
  - doc/release_notes/1_7_4.md
305
306
  - doc/release_notes/1_7_5.md
306
307
  - doc/release_notes/1_7_6.md
308
+ - doc/release_notes/1_7_7.md
307
309
  - lib/httpx.rb
308
310
  - lib/httpx/adapters/datadog.rb
309
311
  - lib/httpx/adapters/faraday.rb