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/request.rb CHANGED
@@ -11,8 +11,10 @@ module HTTPX
11
11
  include Callbacks
12
12
  using URIExtensions
13
13
 
14
+ ALLOWED_URI_SCHEMES = %w[https http].freeze
15
+
14
16
  # default value used for "user-agent" header, when not overridden.
15
- USER_AGENT = "httpx.rb/#{VERSION}"
17
+ USER_AGENT = "httpx.rb/#{VERSION}".freeze # rubocop:disable Style/RedundantFreeze
16
18
 
17
19
  # the upcased string HTTP verb for this request.
18
20
  attr_reader :verb
@@ -43,16 +45,52 @@ module HTTPX
43
45
 
44
46
  attr_writer :persistent
45
47
 
48
+ attr_reader :active_timeouts
49
+
46
50
  # will be +true+ when request body has been completely flushed.
47
51
  def_delegator :@body, :empty?
48
52
 
49
- # initializes the instance with the given +verb+, an absolute or relative +uri+, and the
50
- # request options.
51
- def initialize(verb, uri, options = {})
53
+ # closes the body
54
+ def_delegator :@body, :close
55
+
56
+ # initializes the instance with the given +verb+ (an upppercase String, ex. 'GEt'),
57
+ # an absolute or relative +uri+ (either as String or URI::HTTP object), the
58
+ # request +options+ (instance of HTTPX::Options) and an optional Hash of +params+.
59
+ #
60
+ # Besides any of the options documented in HTTPX::Options (which would override or merge with what
61
+ # +options+ sets), it accepts also the following:
62
+ #
63
+ # :params :: hash or array of key-values which will be encoded and set in the query string of request uris.
64
+ # :body :: to be encoded in the request body payload. can be a String, an IO object (i.e. a File), or an Enumerable.
65
+ # :form :: hash of array of key-values which will be form-urlencoded- or multipart-encoded in requests body payload.
66
+ # :json :: hash of array of key-values which will be JSON-encoded in requests body payload.
67
+ # :xml :: Nokogiri XML nodes which will be encoded in requests body payload.
68
+ #
69
+ # :body, :form, :json and :xml are all mutually exclusive, i.e. only one of them gets picked up.
70
+ def initialize(verb, uri, options, params = EMPTY_HASH)
52
71
  @verb = verb.to_s.upcase
53
- @options = Options.new(options)
54
72
  @uri = Utils.to_uri(uri)
55
- if @uri.relative?
73
+
74
+ @headers = options.headers.dup
75
+ merge_headers(params.delete(:headers)) if params.key?(:headers)
76
+
77
+ @headers["user-agent"] ||= USER_AGENT
78
+ @headers["accept"] ||= "*/*"
79
+
80
+ # forego compression in the Range request case
81
+ if @headers.key?("range")
82
+ @headers.delete("accept-encoding")
83
+ else
84
+ @headers["accept-encoding"] ||= options.supported_compression_formats
85
+ end
86
+
87
+ @query_params = params.delete(:params) if params.key?(:params)
88
+
89
+ @body = options.request_body_class.new(@headers, options, **params)
90
+
91
+ @options = @body.options
92
+
93
+ if @uri.relative? || @uri.host.nil?
56
94
  origin = @options.origin
57
95
  raise(Error, "invalid URI: #{@uri}") unless origin
58
96
 
@@ -61,28 +99,37 @@ module HTTPX
61
99
  @uri = origin.merge("#{base_path}#{@uri}")
62
100
  end
63
101
 
64
- @headers = @options.headers.dup
65
- @headers["user-agent"] ||= USER_AGENT
66
- @headers["accept"] ||= "*/*"
102
+ raise UnsupportedSchemeError, "#{@uri}: #{@uri.scheme}: unsupported URI scheme" unless ALLOWED_URI_SCHEMES.include?(@uri.scheme)
67
103
 
68
- @body = @options.request_body_class.new(@headers, @options)
69
104
  @state = :idle
70
105
  @response = nil
71
106
  @peer_address = nil
107
+ @ping = false
72
108
  @persistent = @options.persistent
109
+ @active_timeouts = []
110
+ end
111
+
112
+ # whether request has been buffered with a ping
113
+ def ping?
114
+ @ping
115
+ end
116
+
117
+ # marks the request as having been buffered with a ping
118
+ def ping!
119
+ @ping = true
73
120
  end
74
121
 
75
- # the read timeout defied for this requet.
122
+ # the read timeout defined for this request.
76
123
  def read_timeout
77
124
  @options.timeout[:read_timeout]
78
125
  end
79
126
 
80
- # the write timeout defied for this requet.
127
+ # the write timeout defined for this request.
81
128
  def write_timeout
82
129
  @options.timeout[:write_timeout]
83
130
  end
84
131
 
85
- # the request timeout defied for this requet.
132
+ # the request timeout defined for this request.
86
133
  def request_timeout
87
134
  @options.timeout[:request_timeout]
88
135
  end
@@ -91,10 +138,12 @@ module HTTPX
91
138
  @persistent
92
139
  end
93
140
 
141
+ # if the request contains trailer headers
94
142
  def trailers?
95
143
  defined?(@trailers)
96
144
  end
97
145
 
146
+ # returns an instance of HTTPX::Headers containing the trailer headers
98
147
  def trailers
99
148
  @trailers ||= @options.headers_class.new
100
149
  end
@@ -106,6 +155,7 @@ module HTTPX
106
155
  :w
107
156
  end
108
157
 
158
+ # merges +h+ into the instance of HTTPX::Headers of the request.
109
159
  def merge_headers(h)
110
160
  @headers = @headers.merge(h)
111
161
  end
@@ -172,7 +222,7 @@ module HTTPX
172
222
  return @query if defined?(@query)
173
223
 
174
224
  query = []
175
- if (q = @options.params)
225
+ if (q = @query_params)
176
226
  query << Transcoder::Form.encode(q)
177
227
  end
178
228
  query << @uri.query if @uri.query
@@ -210,10 +260,13 @@ module HTTPX
210
260
  case nextstate
211
261
  when :idle
212
262
  @body.rewind
263
+ @ping = false
213
264
  @response = nil
214
265
  @drainer = nil
266
+ @active_timeouts.clear
215
267
  when :headers
216
268
  return unless @state == :idle
269
+
217
270
  when :body
218
271
  return unless @state == :headers ||
219
272
  @state == :expect
@@ -234,6 +287,7 @@ module HTTPX
234
287
  return unless @state == :body
235
288
  when :done
236
289
  return if @state == :expect
290
+
237
291
  end
238
292
  @state = nextstate
239
293
  emit(@state, self)
@@ -244,6 +298,15 @@ module HTTPX
244
298
  def expects?
245
299
  @headers["expect"] == "100-continue" && @informational_status == 100 && !@response
246
300
  end
301
+
302
+ def set_timeout_callback(event, &callback)
303
+ clb = once(event, &callback)
304
+
305
+ # reset timeout callbacks when requests get rerouted to a different connection
306
+ once(:idle) do
307
+ callbacks(event).delete(clb)
308
+ end
309
+ end
247
310
  end
248
311
  end
249
312
 
@@ -27,7 +27,7 @@ module HTTPX
27
27
  use_get: false,
28
28
  }.freeze
29
29
 
30
- def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close, :terminate
30
+ def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close, :terminate, :inflight?
31
31
 
32
32
  def initialize(_, options)
33
33
  super
@@ -43,7 +43,7 @@ module HTTPX
43
43
  end
44
44
 
45
45
  def <<(connection)
46
- return if @uri.origin == connection.origin.to_s
46
+ return if @uri.origin == connection.peer.to_s
47
47
 
48
48
  @uri_addresses ||= HTTPX::Resolver.nolookup_resolve(@uri.host) || @resolver.getaddresses(@uri.host)
49
49
 
@@ -66,28 +66,25 @@ module HTTPX
66
66
  end
67
67
 
68
68
  def resolver_connection
69
- @resolver_connection ||= @pool.find_connection(@uri, @options) || begin
70
- @building_connection = true
71
- connection = @options.connection_class.new(@uri, @options.merge(ssl: { alpn_protocols: %w[h2] }))
72
- @pool.init_connection(connection, @options)
73
- # only explicity emit addresses if connection didn't pre-resolve, i.e. it's not an IP.
74
- emit_addresses(connection, @family, @uri_addresses) unless connection.addresses
75
- @building_connection = false
76
- connection
69
+ # TODO: leaks connection object into the pool
70
+ @resolver_connection ||= @current_session.find_connection(@uri, @current_selector,
71
+ @options.merge(ssl: { alpn_protocols: %w[h2] })).tap do |conn|
72
+ emit_addresses(conn, @family, @uri_addresses) unless conn.addresses
77
73
  end
78
74
  end
79
75
 
80
76
  private
81
77
 
82
78
  def resolve(connection = @connections.first, hostname = nil)
83
- return if @building_connection
84
79
  return unless connection
85
80
 
86
81
  hostname ||= @queries.key(connection)
87
82
 
88
83
  if hostname.nil?
89
- hostname = connection.origin.host
90
- log { "resolver: resolve IDN #{connection.origin.non_ascii_hostname} as #{hostname}" } if connection.origin.non_ascii_hostname
84
+ hostname = connection.peer.host
85
+ log do
86
+ "resolver #{FAMILY_TYPES[@record_type]}: resolve IDN #{connection.peer.non_ascii_hostname} as #{hostname}"
87
+ end if connection.peer.non_ascii_hostname
91
88
 
92
89
  hostname = @resolver.generate_candidates(hostname).each do |name|
93
90
  @queries[name.to_s] = connection
@@ -95,7 +92,7 @@ module HTTPX
95
92
  else
96
93
  @queries[hostname] = connection
97
94
  end
98
- log { "resolver: query #{FAMILY_TYPES[RECORD_TYPES[@family]]} for #{hostname}" }
95
+ log { "resolver #{FAMILY_TYPES[@record_type]}: query for #{hostname}" }
99
96
 
100
97
  begin
101
98
  request = build_request(hostname)
@@ -106,7 +103,7 @@ module HTTPX
106
103
  @connections << connection
107
104
  rescue ResolveError, Resolv::DNS::EncodeError => e
108
105
  reset_hostname(hostname)
109
- emit_resolve_error(connection, connection.origin.host, e)
106
+ emit_resolve_error(connection, connection.peer.host, e)
110
107
  end
111
108
  end
112
109
 
@@ -115,7 +112,7 @@ module HTTPX
115
112
  rescue StandardError => e
116
113
  hostname = @requests.delete(request)
117
114
  connection = reset_hostname(hostname)
118
- emit_resolve_error(connection, connection.origin.host, e)
115
+ emit_resolve_error(connection, connection.peer.host, e)
119
116
  else
120
117
  # @type var response: HTTPX::Response
121
118
  parse(request, response)
@@ -154,7 +151,7 @@ module HTTPX
154
151
  when :decode_error
155
152
  host = @requests.delete(request)
156
153
  connection = reset_hostname(host)
157
- emit_resolve_error(connection, connection.origin.host, result)
154
+ emit_resolve_error(connection, connection.peer.host, result)
158
155
  end
159
156
  end
160
157
 
@@ -174,7 +171,7 @@ module HTTPX
174
171
  alias_address = answers[address["alias"]]
175
172
  if alias_address.nil?
176
173
  reset_hostname(address["name"])
177
- if catch(:coalesced) { early_resolve(connection, hostname: address["alias"]) }
174
+ if early_resolve(connection, hostname: address["alias"])
178
175
  @connections.delete(connection)
179
176
  else
180
177
  resolve(connection, address["alias"])
@@ -199,7 +196,7 @@ module HTTPX
199
196
  @queries.delete_if { |_, conn| connection == conn }
200
197
 
201
198
  Resolver.cached_lookup_set(hostname, @family, addresses) if @resolver_options[:cache]
202
- emit_addresses(connection, @family, addresses.map { |addr| addr["data"] })
199
+ catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |addr| addr["data"] }) }
203
200
  end
204
201
  end
205
202
  return if @connections.empty?
@@ -219,7 +216,7 @@ module HTTPX
219
216
  uri.query = URI.encode_www_form(params)
220
217
  request = rklass.new("GET", uri, @options)
221
218
  else
222
- request = rklass.new("POST", uri, @options.merge(body: [payload]))
219
+ request = rklass.new("POST", uri, @options, body: [payload])
223
220
  request.headers["content-type"] = "application/dns-message"
224
221
  end
225
222
  request.headers["accept"] = "application/dns-message"
@@ -8,27 +8,45 @@ module HTTPX
8
8
  include Callbacks
9
9
  using ArrayExtensions::FilterMap
10
10
 
11
- attr_reader :resolvers
11
+ attr_reader :resolvers, :options
12
12
 
13
13
  def initialize(resolver_type, options)
14
+ @current_selector = nil
15
+ @current_session = nil
14
16
  @options = options
15
17
  @resolver_options = @options.resolver_options
16
18
 
17
19
  @resolvers = options.ip_families.map do |ip_family|
18
20
  resolver = resolver_type.new(ip_family, options)
19
- resolver.on(:resolve, &method(:on_resolver_connection))
20
- resolver.on(:error, &method(:on_resolver_error))
21
- resolver.on(:close) { on_resolver_close(resolver) }
21
+ resolver.multi = self
22
22
  resolver
23
23
  end
24
24
 
25
25
  @errors = Hash.new { |hs, k| hs[k] = [] }
26
26
  end
27
27
 
28
+ def current_selector=(s)
29
+ @current_selector = s
30
+ @resolvers.each { |r| r.__send__(__method__, s) }
31
+ end
32
+
33
+ def current_session=(s)
34
+ @current_session = s
35
+ @resolvers.each { |r| r.__send__(__method__, s) }
36
+ end
37
+
28
38
  def closed?
29
39
  @resolvers.all?(&:closed?)
30
40
  end
31
41
 
42
+ def empty?
43
+ @resolvers.all?(&:empty?)
44
+ end
45
+
46
+ def inflight?
47
+ @resolvers.any(&:inflight?)
48
+ end
49
+
32
50
  def timeout
33
51
  @resolvers.filter_map(&:timeout).min
34
52
  end
@@ -42,10 +60,11 @@ module HTTPX
42
60
  end
43
61
 
44
62
  def early_resolve(connection)
45
- hostname = connection.origin.host
63
+ hostname = connection.peer.host
46
64
  addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
47
- return unless addresses
65
+ return false unless addresses
48
66
 
67
+ resolved = false
49
68
  addresses.group_by(&:family).sort { |(f1, _), (f2, _)| f2 <=> f1 }.each do |family, addrs|
50
69
  # try to match the resolver by family. However, there are cases where that's not possible, as when
51
70
  # the system does not have IPv6 connectivity, but it does support IPv6 via loopback/link-local.
@@ -55,21 +74,20 @@ module HTTPX
55
74
 
56
75
  # it does not matter which resolver it is, as early-resolve code is shared.
57
76
  resolver.emit_addresses(connection, family, addrs, true)
58
- end
59
- end
60
77
 
61
- private
78
+ resolved = true
79
+ end
62
80
 
63
- def on_resolver_connection(connection)
64
- emit(:resolve, connection)
81
+ resolved
65
82
  end
66
83
 
67
- def on_resolver_error(connection, error)
68
- emit(:error, connection, error)
69
- end
84
+ def lazy_resolve(connection)
85
+ @resolvers.each do |resolver|
86
+ resolver << @current_session.try_clone_connection(connection, @current_selector, resolver.family)
87
+ next if resolver.empty?
70
88
 
71
- def on_resolver_close(resolver)
72
- emit(:close, resolver)
89
+ @current_session.select_resolver(resolver, @current_selector)
90
+ end
73
91
  end
74
92
  end
75
93
  end