httpx 1.3.4 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_4_0.md +43 -0
  3. data/lib/httpx/adapters/faraday.rb +2 -0
  4. data/lib/httpx/adapters/webmock.rb +11 -5
  5. data/lib/httpx/callbacks.rb +0 -5
  6. data/lib/httpx/chainable.rb +3 -1
  7. data/lib/httpx/connection/http2.rb +11 -7
  8. data/lib/httpx/connection.rb +128 -16
  9. data/lib/httpx/errors.rb +12 -0
  10. data/lib/httpx/loggable.rb +5 -5
  11. data/lib/httpx/options.rb +26 -16
  12. data/lib/httpx/plugins/aws_sigv4.rb +31 -16
  13. data/lib/httpx/plugins/callbacks.rb +12 -2
  14. data/lib/httpx/plugins/circuit_breaker.rb +0 -5
  15. data/lib/httpx/plugins/content_digest.rb +202 -0
  16. data/lib/httpx/plugins/expect.rb +4 -3
  17. data/lib/httpx/plugins/follow_redirects.rb +7 -8
  18. data/lib/httpx/plugins/h2c.rb +23 -20
  19. data/lib/httpx/plugins/internal_telemetry.rb +27 -0
  20. data/lib/httpx/plugins/persistent.rb +16 -0
  21. data/lib/httpx/plugins/proxy/http.rb +17 -19
  22. data/lib/httpx/plugins/proxy.rb +91 -93
  23. data/lib/httpx/plugins/retries.rb +5 -8
  24. data/lib/httpx/plugins/upgrade.rb +5 -10
  25. data/lib/httpx/plugins/webdav.rb +6 -0
  26. data/lib/httpx/plugins/xml.rb +76 -0
  27. data/lib/httpx/pool.rb +73 -244
  28. data/lib/httpx/request/body.rb +16 -12
  29. data/lib/httpx/request.rb +1 -1
  30. data/lib/httpx/resolver/https.rb +12 -19
  31. data/lib/httpx/resolver/multi.rb +34 -16
  32. data/lib/httpx/resolver/native.rb +36 -13
  33. data/lib/httpx/resolver/resolver.rb +49 -11
  34. data/lib/httpx/resolver/system.rb +29 -11
  35. data/lib/httpx/resolver.rb +21 -14
  36. data/lib/httpx/response.rb +5 -3
  37. data/lib/httpx/selector.rb +164 -95
  38. data/lib/httpx/session.rb +296 -139
  39. data/lib/httpx/transcoder/gzip.rb +0 -3
  40. data/lib/httpx/transcoder/json.rb +14 -2
  41. data/lib/httpx/transcoder/utils/deflater.rb +7 -4
  42. data/lib/httpx/transcoder/utils/inflater.rb +2 -0
  43. data/lib/httpx/transcoder.rb +0 -1
  44. data/lib/httpx/version.rb +1 -1
  45. data/lib/httpx.rb +19 -20
  46. data/sig/callbacks.rbs +0 -1
  47. data/sig/chainable.rbs +4 -0
  48. data/sig/connection/http2.rbs +1 -1
  49. data/sig/connection.rbs +14 -3
  50. data/sig/errors.rbs +6 -0
  51. data/sig/loggable.rbs +2 -0
  52. data/sig/options.rbs +7 -0
  53. data/sig/plugins/aws_sigv4.rbs +8 -2
  54. data/sig/plugins/content_digest.rbs +51 -0
  55. data/sig/plugins/cookies/cookie.rbs +9 -0
  56. data/sig/plugins/grpc/call.rbs +4 -0
  57. data/sig/plugins/persistent.rbs +4 -1
  58. data/sig/plugins/proxy/socks5.rbs +11 -3
  59. data/sig/plugins/proxy.rbs +18 -11
  60. data/sig/plugins/push_promise.rbs +3 -0
  61. data/sig/plugins/rate_limiter.rbs +2 -0
  62. data/sig/plugins/retries.rbs +1 -1
  63. data/sig/plugins/ssrf_filter.rbs +26 -0
  64. data/sig/plugins/webdav.rbs +23 -0
  65. data/sig/plugins/xml.rbs +37 -0
  66. data/sig/pool.rbs +25 -33
  67. data/sig/request/body.rbs +5 -1
  68. data/sig/resolver/multi.rbs +26 -1
  69. data/sig/resolver/native.rbs +0 -2
  70. data/sig/resolver/resolver.rbs +21 -2
  71. data/sig/resolver.rbs +5 -1
  72. data/sig/response/buffer.rbs +1 -1
  73. data/sig/selector.rbs +30 -4
  74. data/sig/session.rbs +45 -18
  75. data/sig/transcoder/body.rbs +1 -1
  76. data/sig/transcoder/chunker.rbs +1 -1
  77. data/sig/transcoder/deflate.rbs +1 -0
  78. data/sig/transcoder/form.rbs +8 -0
  79. data/sig/transcoder/gzip.rbs +4 -1
  80. data/sig/transcoder/utils/body_reader.rbs +2 -2
  81. data/sig/transcoder/utils/deflater.rbs +2 -2
  82. metadata +10 -4
  83. data/lib/httpx/transcoder/xml.rb +0 -52
  84. data/sig/transcoder/xml.rbs +0 -22
@@ -135,7 +135,7 @@ module HTTPX
135
135
  return unless query
136
136
 
137
137
  h, connection = query
138
- host = connection.origin.host
138
+ host = connection.peer.host
139
139
  timeout = (@timeouts[host][0] -= loop_time)
140
140
 
141
141
  return unless timeout <= 0
@@ -164,7 +164,10 @@ module HTTPX
164
164
  @connections.delete(connection)
165
165
  # This loop_time passed to the exception is bogus. Ideally we would pass the total
166
166
  # resolve timeout, including from the previous retries.
167
- raise ResolveTimeoutError.new(loop_time, "Timed out while resolving #{connection.origin.host}")
167
+ ex = ResolveTimeoutError.new(loop_time, "Timed out while resolving #{connection.peer.host}")
168
+ ex.set_backtrace(ex ? ex.backtrace : caller)
169
+ emit_resolve_error(connection, host, ex)
170
+ emit(:close, self)
168
171
  end
169
172
  end
170
173
 
@@ -248,7 +251,10 @@ module HTTPX
248
251
 
249
252
  unless @queries.value?(connection)
250
253
  @connections.delete(connection)
251
- raise NativeResolveError.new(connection, connection.origin.host, "name or service not known")
254
+ ex = NativeResolveError.new(connection, connection.peer.host, "name or service not known")
255
+ ex.set_backtrace(ex ? ex.backtrace : caller)
256
+ emit_resolve_error(connection, connection.peer.host, ex)
257
+ emit(:close, self)
252
258
  end
253
259
 
254
260
  resolve
@@ -265,13 +271,13 @@ module HTTPX
265
271
  hostname, connection = @queries.first
266
272
  reset_hostname(hostname)
267
273
  @connections.delete(connection)
268
- ex = NativeResolveError.new(connection, connection.origin.host, "unknown DNS error (error code #{result})")
274
+ ex = NativeResolveError.new(connection, connection.peer.host, "unknown DNS error (error code #{result})")
269
275
  raise ex
270
276
  when :decode_error
271
277
  hostname, connection = @queries.first
272
278
  reset_hostname(hostname)
273
279
  @connections.delete(connection)
274
- ex = NativeResolveError.new(connection, connection.origin.host, result.message)
280
+ ex = NativeResolveError.new(connection, connection.peer.host, result.message)
275
281
  ex.set_backtrace(result.backtrace)
276
282
  raise ex
277
283
  end
@@ -283,7 +289,7 @@ module HTTPX
283
289
  hostname, connection = @queries.first
284
290
  reset_hostname(hostname)
285
291
  @connections.delete(connection)
286
- raise NativeResolveError.new(connection, connection.origin.host)
292
+ raise NativeResolveError.new(connection, connection.peer.host)
287
293
  else
288
294
  address = addresses.first
289
295
  name = address["name"]
@@ -309,9 +315,9 @@ module HTTPX
309
315
  if address.key?("alias") # CNAME
310
316
  hostname_alias = address["alias"]
311
317
  # clean up intermediate queries
312
- @timeouts.delete(name) unless connection.origin.host == name
318
+ @timeouts.delete(name) unless connection.peer.host == name
313
319
 
314
- if catch(:coalesced) { early_resolve(connection, hostname: hostname_alias) }
320
+ if early_resolve(connection, hostname: hostname_alias)
315
321
  @connections.delete(connection)
316
322
  else
317
323
  if @socket_type == :tcp
@@ -326,13 +332,13 @@ module HTTPX
326
332
  end
327
333
  else
328
334
  reset_hostname(name, connection: connection)
329
- @timeouts.delete(connection.origin.host)
335
+ @timeouts.delete(connection.peer.host)
330
336
  @connections.delete(connection)
331
- Resolver.cached_lookup_set(connection.origin.host, @family, addresses) if @resolver_options[:cache]
337
+ Resolver.cached_lookup_set(connection.peer.host, @family, addresses) if @resolver_options[:cache]
332
338
  catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |addr| addr["data"] }) }
333
339
  end
334
340
  end
335
- return emit(:close) if @connections.empty?
341
+ return emit(:close, self) if @connections.empty?
336
342
 
337
343
  resolve
338
344
  end
@@ -345,8 +351,8 @@ module HTTPX
345
351
  hostname ||= @queries.key(connection)
346
352
 
347
353
  if hostname.nil?
348
- hostname = connection.origin.host
349
- log { "resolver: resolve IDN #{connection.origin.non_ascii_hostname} as #{hostname}" } if connection.origin.non_ascii_hostname
354
+ hostname = connection.peer.host
355
+ log { "resolver: resolve IDN #{connection.peer.non_ascii_hostname} as #{hostname}" } if connection.peer.non_ascii_hostname
350
356
 
351
357
  hostname = generate_candidates(hostname).each do |name|
352
358
  @queries[name] = connection
@@ -358,7 +364,10 @@ module HTTPX
358
364
  begin
359
365
  @write_buffer << encode_dns_query(hostname)
360
366
  rescue Resolv::DNS::EncodeError => e
367
+ reset_hostname(hostname, connection: connection)
368
+ @connections.delete(connection)
361
369
  emit_resolve_error(connection, hostname, e)
370
+ emit(:close, self) if @connections.empty?
362
371
  end
363
372
  end
364
373
 
@@ -430,17 +439,31 @@ module HTTPX
430
439
  @read_buffer.clear
431
440
  end
432
441
  @state = nextstate
442
+ rescue Errno::ECONNREFUSED,
443
+ Errno::EADDRNOTAVAIL,
444
+ Errno::EHOSTUNREACH,
445
+ SocketError,
446
+ IOError,
447
+ ConnectTimeoutError => e
448
+ # these errors may happen during TCP handshake
449
+ # treat them as resolve errors.
450
+ handle_error(e)
433
451
  end
434
452
 
435
453
  def handle_error(error)
436
454
  if error.respond_to?(:connection) &&
437
455
  error.respond_to?(:host)
456
+ reset_hostname(error.host, connection: error.connection)
457
+ @connections.delete(error.connection)
438
458
  emit_resolve_error(error.connection, error.host, error)
439
459
  else
440
460
  @queries.each do |host, connection|
461
+ reset_hostname(host, connection: connection)
462
+ @connections.delete(connection)
441
463
  emit_resolve_error(connection, host, error)
442
464
  end
443
465
  end
466
+ emit(:close, self) if @connections.empty?
444
467
  end
445
468
 
446
469
  def reset_hostname(hostname, connection: @queries.delete(hostname), reset_candidates: true)
@@ -26,14 +26,26 @@ module HTTPX
26
26
  end
27
27
  end
28
28
 
29
- attr_reader :family
29
+ attr_reader :family, :options
30
30
 
31
- attr_writer :pool
31
+ attr_writer :current_selector, :current_session
32
+
33
+ attr_accessor :multi
32
34
 
33
35
  def initialize(family, options)
34
36
  @family = family
35
37
  @record_type = RECORD_TYPES[family]
36
38
  @options = options
39
+
40
+ set_resolver_callbacks
41
+ end
42
+
43
+ def each_connection(&block)
44
+ enum_for(__method__) unless block
45
+
46
+ return unless @connections
47
+
48
+ @connections.each(&block)
37
49
  end
38
50
 
39
51
  def close; end
@@ -48,6 +60,10 @@ module HTTPX
48
60
  true
49
61
  end
50
62
 
63
+ def inflight?
64
+ false
65
+ end
66
+
51
67
  def emit_addresses(connection, family, addresses, early_resolve = false)
52
68
  addresses.map! do |address|
53
69
  address.is_a?(IPAddr) ? address : IPAddr.new(address.to_s)
@@ -56,14 +72,14 @@ module HTTPX
56
72
  # double emission check, but allow early resolution to work
57
73
  return if !early_resolve && connection.addresses && !addresses.intersect?(connection.addresses)
58
74
 
59
- log { "resolver: answer #{FAMILY_TYPES[RECORD_TYPES[family]]} #{connection.origin.host}: #{addresses.inspect}" }
60
- if @pool && # if triggered by early resolve, pool may not be here yet
75
+ log { "resolver: answer #{FAMILY_TYPES[RECORD_TYPES[family]]} #{connection.peer.host}: #{addresses.inspect}" }
76
+ if @current_selector && # if triggered by early resolve, session may not be here yet
61
77
  !connection.io &&
62
78
  connection.options.ip_families.size > 1 &&
63
79
  family == Socket::AF_INET &&
64
- addresses.first.to_s != connection.origin.host.to_s
80
+ addresses.first.to_s != connection.peer.host.to_s
65
81
  log { "resolver: A response, applying resolution delay..." }
66
- @pool.after(0.05) do
82
+ @current_selector.after(0.05) do
67
83
  unless connection.state == :closed ||
68
84
  # double emission check
69
85
  (connection.addresses && addresses.intersect?(connection.addresses))
@@ -92,20 +108,22 @@ module HTTPX
92
108
  end
93
109
  end
94
110
 
95
- def early_resolve(connection, hostname: connection.origin.host)
111
+ def early_resolve(connection, hostname: connection.peer.host)
96
112
  addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
97
113
 
98
- return unless addresses
114
+ return false unless addresses
99
115
 
100
116
  addresses = addresses.select { |addr| addr.family == @family }
101
117
 
102
- return if addresses.empty?
118
+ return false if addresses.empty?
103
119
 
104
120
  emit_addresses(connection, @family, addresses, true)
121
+
122
+ true
105
123
  end
106
124
 
107
- def emit_resolve_error(connection, hostname = connection.origin.host, ex = nil)
108
- emit(:error, connection, resolve_error(hostname, ex))
125
+ def emit_resolve_error(connection, hostname = connection.peer.host, ex = nil)
126
+ emit_connection_error(connection, resolve_error(hostname, ex))
109
127
  end
110
128
 
111
129
  def resolve_error(hostname, ex = nil)
@@ -116,5 +134,25 @@ module HTTPX
116
134
  error.set_backtrace(ex ? ex.backtrace : caller)
117
135
  error
118
136
  end
137
+
138
+ def set_resolver_callbacks
139
+ on(:resolve, &method(:resolve_connection))
140
+ on(:error, &method(:emit_connection_error))
141
+ on(:close, &method(:close_resolver))
142
+ end
143
+
144
+ def resolve_connection(connection)
145
+ @current_session.__send__(:on_resolver_connection, connection, @current_selector)
146
+ end
147
+
148
+ def emit_connection_error(connection, error)
149
+ return connection.emit(:connect_error, error) if connection.connecting? && connection.callbacks_for?(:connect_error)
150
+
151
+ connection.emit(:error, error)
152
+ end
153
+
154
+ def close_resolver(resolver)
155
+ @current_session.__send__(:on_resolver_close, resolver, @current_selector)
156
+ end
119
157
  end
120
158
  end
@@ -47,8 +47,12 @@ module HTTPX
47
47
  yield self
48
48
  end
49
49
 
50
- def connections
51
- EMPTY
50
+ def multi
51
+ self
52
+ end
53
+
54
+ def empty?
55
+ true
52
56
  end
53
57
 
54
58
  def close
@@ -84,7 +88,7 @@ module HTTPX
84
88
 
85
89
  return unless connection
86
90
 
87
- @timeouts[connection.origin.host].first
91
+ @timeouts[connection.peer.host].first
88
92
  end
89
93
 
90
94
  def <<(connection)
@@ -92,6 +96,11 @@ module HTTPX
92
96
  resolve
93
97
  end
94
98
 
99
+ def early_resolve(connection, **)
100
+ self << connection
101
+ true
102
+ end
103
+
95
104
  def handle_socket_timeout(interval)
96
105
  error = HTTPX::ResolveTimeoutError.new(interval, "timed out while waiting on select")
97
106
  error.set_backtrace(caller)
@@ -120,23 +129,26 @@ module HTTPX
120
129
  def consume
121
130
  return if @connections.empty?
122
131
 
123
- while @pipe_read.ready? && (event = @pipe_read.getbyte)
132
+ if @pipe_read.wait_readable
133
+ event = @pipe_read.getbyte
134
+
124
135
  case event
125
136
  when DONE
126
137
  *pair, addrs = @pipe_mutex.synchronize { @ips.pop }
127
138
  @queries.delete(pair)
139
+ _, connection = pair
140
+ @connections.delete(connection)
128
141
 
129
142
  family, connection = pair
130
143
  catch(:coalesced) { emit_addresses(connection, family, addrs) }
131
144
  when ERROR
132
145
  *pair, error = @pipe_mutex.synchronize { @ips.pop }
133
146
  @queries.delete(pair)
147
+ @connections.delete(connection)
134
148
 
135
- family, connection = pair
136
- emit_resolve_error(connection, connection.origin.host, error)
149
+ _, connection = pair
150
+ emit_resolve_error(connection, connection.peer.host, error)
137
151
  end
138
-
139
- @connections.delete(connection) if @queries.empty?
140
152
  end
141
153
 
142
154
  return emit(:close, self) if @connections.empty?
@@ -148,9 +160,9 @@ module HTTPX
148
160
  raise Error, "no URI to resolve" unless connection
149
161
  return unless @queries.empty?
150
162
 
151
- hostname = connection.origin.host
163
+ hostname = connection.peer.host
152
164
  scheme = connection.origin.scheme
153
- log { "resolver: resolve IDN #{connection.origin.non_ascii_hostname} as #{hostname}" } if connection.origin.non_ascii_hostname
165
+ log { "resolver: resolve IDN #{connection.peer.non_ascii_hostname} as #{hostname}" } if connection.peer.non_ascii_hostname
154
166
 
155
167
  transition(:open)
156
168
 
@@ -164,7 +176,7 @@ module HTTPX
164
176
  def async_resolve(connection, hostname, scheme)
165
177
  families = connection.options.ip_families
166
178
  log { "resolver: query for #{hostname}" }
167
- timeouts = @timeouts[connection.origin.host]
179
+ timeouts = @timeouts[connection.peer.host]
168
180
  resolve_timeout = timeouts.first
169
181
 
170
182
  Thread.start do
@@ -210,5 +222,11 @@ module HTTPX
210
222
  def __addrinfo_resolve(host, scheme)
211
223
  Addrinfo.getaddrinfo(host, scheme, Socket::AF_UNSPEC, Socket::SOCK_STREAM)
212
224
  end
225
+
226
+ def emit_connection_error(_, error)
227
+ throw(:resolve_error, error)
228
+ end
229
+
230
+ def close_resolver(resolver); end
213
231
  end
214
232
  end
@@ -53,8 +53,8 @@ module HTTPX
53
53
 
54
54
  def cached_lookup(hostname)
55
55
  now = Utils.now
56
- @lookup_mutex.synchronize do
57
- lookup(hostname, now)
56
+ lookup_synchronize do |lookups|
57
+ lookup(hostname, lookups, now)
58
58
  end
59
59
  end
60
60
 
@@ -63,37 +63,37 @@ module HTTPX
63
63
  entries.each do |entry|
64
64
  entry["TTL"] += now
65
65
  end
66
- @lookup_mutex.synchronize do
66
+ lookup_synchronize do |lookups|
67
67
  case family
68
68
  when Socket::AF_INET6
69
- @lookups[hostname].concat(entries)
69
+ lookups[hostname].concat(entries)
70
70
  when Socket::AF_INET
71
- @lookups[hostname].unshift(*entries)
71
+ lookups[hostname].unshift(*entries)
72
72
  end
73
73
  entries.each do |entry|
74
74
  next unless entry["name"] != hostname
75
75
 
76
76
  case family
77
77
  when Socket::AF_INET6
78
- @lookups[entry["name"]] << entry
78
+ lookups[entry["name"]] << entry
79
79
  when Socket::AF_INET
80
- @lookups[entry["name"]].unshift(entry)
80
+ lookups[entry["name"]].unshift(entry)
81
81
  end
82
82
  end
83
83
  end
84
84
  end
85
85
 
86
86
  # do not use directly!
87
- def lookup(hostname, ttl)
88
- return unless @lookups.key?(hostname)
87
+ def lookup(hostname, lookups, ttl)
88
+ return unless lookups.key?(hostname)
89
89
 
90
- entries = @lookups[hostname] = @lookups[hostname].select do |address|
90
+ entries = lookups[hostname] = lookups[hostname].select do |address|
91
91
  address["TTL"] > ttl
92
92
  end
93
93
 
94
94
  ips = entries.flat_map do |address|
95
95
  if address.key?("alias")
96
- lookup(address["alias"], ttl)
96
+ lookup(address["alias"], lookups, ttl)
97
97
  else
98
98
  IPAddr.new(address["data"])
99
99
  end
@@ -103,12 +103,11 @@ module HTTPX
103
103
  end
104
104
 
105
105
  def generate_id
106
- @identifier_mutex.synchronize { @identifier = (@identifier + 1) & 0xFFFF }
106
+ id_synchronize { @identifier = (@identifier + 1) & 0xFFFF }
107
107
  end
108
108
 
109
109
  def encode_dns_query(hostname, type: Resolv::DNS::Resource::IN::A, message_id: generate_id)
110
- Resolv::DNS::Message.new.tap do |query|
111
- query.id = message_id
110
+ Resolv::DNS::Message.new(message_id).tap do |query|
112
111
  query.rd = 1
113
112
  query.add_question(hostname, type)
114
113
  end.encode
@@ -150,5 +149,13 @@ module HTTPX
150
149
 
151
150
  [:ok, addresses]
152
151
  end
152
+
153
+ def lookup_synchronize
154
+ @lookup_mutex.synchronize { yield(@lookups) }
155
+ end
156
+
157
+ def id_synchronize(&block)
158
+ @identifier_mutex.synchronize(&block)
159
+ end
153
160
  end
154
161
  end
@@ -166,10 +166,12 @@ module HTTPX
166
166
  decode(Transcoder::Form)
167
167
  end
168
168
 
169
- # decodes the response payload into a Nokogiri::XML::Node object **if** the payload is valid
170
- # "application/xml" (requires the "nokogiri" gem).
171
169
  def xml
172
- decode(Transcoder::Xml)
170
+ # TODO: remove at next major version.
171
+ warn "DEPRECATION WARNING: calling `.#{__method__}` on plain HTTPX responses is deprecated. " \
172
+ "Use HTTPX.plugin(:xml) sessions and call `.#{__method__}` in its responses instead."
173
+ require "httpx/plugins/xml"
174
+ decode(Plugins::XML::Transcoder)
173
175
  end
174
176
 
175
177
  private