httpx 1.3.4 → 1.4.1

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_4_0.md +43 -0
  3. data/doc/release_notes/1_4_1.md +19 -0
  4. data/lib/httpx/adapters/datadog.rb +55 -83
  5. data/lib/httpx/adapters/faraday.rb +2 -0
  6. data/lib/httpx/adapters/webmock.rb +18 -6
  7. data/lib/httpx/callbacks.rb +0 -5
  8. data/lib/httpx/chainable.rb +3 -1
  9. data/lib/httpx/connection/http2.rb +12 -8
  10. data/lib/httpx/connection.rb +192 -22
  11. data/lib/httpx/errors.rb +12 -0
  12. data/lib/httpx/loggable.rb +5 -5
  13. data/lib/httpx/options.rb +26 -16
  14. data/lib/httpx/plugins/aws_sigv4.rb +31 -16
  15. data/lib/httpx/plugins/callbacks.rb +12 -2
  16. data/lib/httpx/plugins/circuit_breaker.rb +0 -5
  17. data/lib/httpx/plugins/content_digest.rb +202 -0
  18. data/lib/httpx/plugins/expect.rb +4 -3
  19. data/lib/httpx/plugins/follow_redirects.rb +7 -8
  20. data/lib/httpx/plugins/grpc/grpc_encoding.rb +2 -0
  21. data/lib/httpx/plugins/h2c.rb +23 -20
  22. data/lib/httpx/plugins/internal_telemetry.rb +27 -0
  23. data/lib/httpx/plugins/persistent.rb +16 -0
  24. data/lib/httpx/plugins/proxy/http.rb +17 -19
  25. data/lib/httpx/plugins/proxy.rb +91 -93
  26. data/lib/httpx/plugins/retries.rb +5 -8
  27. data/lib/httpx/plugins/upgrade.rb +5 -10
  28. data/lib/httpx/plugins/webdav.rb +6 -0
  29. data/lib/httpx/plugins/xml.rb +76 -0
  30. data/lib/httpx/pool.rb +73 -244
  31. data/lib/httpx/request/body.rb +25 -26
  32. data/lib/httpx/request.rb +7 -1
  33. data/lib/httpx/resolver/https.rb +15 -20
  34. data/lib/httpx/resolver/multi.rb +34 -16
  35. data/lib/httpx/resolver/native.rb +66 -25
  36. data/lib/httpx/resolver/resolver.rb +59 -15
  37. data/lib/httpx/resolver/system.rb +31 -15
  38. data/lib/httpx/resolver.rb +21 -14
  39. data/lib/httpx/response.rb +5 -3
  40. data/lib/httpx/selector.rb +160 -95
  41. data/lib/httpx/session.rb +273 -140
  42. data/lib/httpx/transcoder/body.rb +15 -31
  43. data/lib/httpx/transcoder/gzip.rb +0 -3
  44. data/lib/httpx/transcoder/json.rb +14 -2
  45. data/lib/httpx/transcoder/multipart/part.rb +1 -1
  46. data/lib/httpx/transcoder/utils/deflater.rb +7 -4
  47. data/lib/httpx/transcoder/utils/inflater.rb +2 -0
  48. data/lib/httpx/transcoder.rb +0 -1
  49. data/lib/httpx/version.rb +1 -1
  50. data/lib/httpx.rb +20 -21
  51. data/sig/callbacks.rbs +0 -1
  52. data/sig/chainable.rbs +4 -0
  53. data/sig/connection/http2.rbs +1 -1
  54. data/sig/connection.rbs +29 -3
  55. data/sig/errors.rbs +6 -0
  56. data/sig/loggable.rbs +2 -0
  57. data/sig/options.rbs +7 -0
  58. data/sig/plugins/aws_sigv4.rbs +8 -2
  59. data/sig/plugins/content_digest.rbs +51 -0
  60. data/sig/plugins/cookies/cookie.rbs +9 -0
  61. data/sig/plugins/grpc/call.rbs +4 -0
  62. data/sig/plugins/persistent.rbs +4 -1
  63. data/sig/plugins/proxy/socks5.rbs +11 -3
  64. data/sig/plugins/proxy.rbs +18 -11
  65. data/sig/plugins/push_promise.rbs +3 -0
  66. data/sig/plugins/rate_limiter.rbs +2 -0
  67. data/sig/plugins/retries.rbs +1 -1
  68. data/sig/plugins/ssrf_filter.rbs +26 -0
  69. data/sig/plugins/webdav.rbs +23 -0
  70. data/sig/plugins/xml.rbs +37 -0
  71. data/sig/pool.rbs +25 -33
  72. data/sig/request/body.rbs +5 -9
  73. data/sig/resolver/multi.rbs +26 -1
  74. data/sig/resolver/native.rbs +2 -2
  75. data/sig/resolver/resolver.rbs +21 -2
  76. data/sig/resolver.rbs +5 -1
  77. data/sig/response/buffer.rbs +1 -1
  78. data/sig/selector.rbs +30 -4
  79. data/sig/session.rbs +47 -18
  80. data/sig/transcoder/body.rbs +2 -4
  81. data/sig/transcoder/chunker.rbs +1 -1
  82. data/sig/transcoder/deflate.rbs +1 -0
  83. data/sig/transcoder/form.rbs +8 -0
  84. data/sig/transcoder/gzip.rbs +4 -1
  85. data/sig/transcoder/utils/body_reader.rbs +3 -3
  86. data/sig/transcoder/utils/deflater.rbs +3 -3
  87. metadata +12 -4
  88. data/lib/httpx/transcoder/xml.rb +0 -52
  89. data/sig/transcoder/xml.rbs +0 -22
@@ -31,31 +31,53 @@ module HTTPX
31
31
  end
32
32
 
33
33
  class Parameters
34
- attr_reader :uri, :username, :password, :scheme
34
+ attr_reader :uri, :username, :password, :scheme, :no_proxy
35
35
 
36
- def initialize(uri:, scheme: nil, username: nil, password: nil, **extra)
37
- @uri = uri.is_a?(URI::Generic) ? uri : URI(uri)
38
- @username = username || @uri.user
39
- @password = password || @uri.password
36
+ def initialize(uri: nil, scheme: nil, username: nil, password: nil, no_proxy: nil, **extra)
37
+ @no_proxy = Array(no_proxy) if no_proxy
38
+ @uris = Array(uri)
39
+ uri = @uris.first
40
40
 
41
- return unless @username && @password
41
+ @username = username
42
+ @password = password
42
43
 
43
- scheme ||= case @uri.scheme
44
- when "socks5"
45
- @uri.scheme
46
- when "http", "https"
47
- "basic"
48
- else
49
- return
44
+ @ns = 0
45
+
46
+ if uri
47
+ @uri = uri.is_a?(URI::Generic) ? uri : URI(uri)
48
+ @username ||= @uri.user
49
+ @password ||= @uri.password
50
50
  end
51
51
 
52
52
  @scheme = scheme
53
53
 
54
- auth_scheme = scheme.to_s.capitalize
54
+ return unless @uri && @username && @password
55
55
 
56
- require_relative "auth/#{scheme}" unless defined?(Authentication) && Authentication.const_defined?(auth_scheme, false)
56
+ @authenticator = nil
57
+ @scheme ||= infer_default_auth_scheme(@uri)
58
+
59
+ return unless @scheme
57
60
 
58
- @authenticator = Authentication.const_get(auth_scheme).new(@username, @password, **extra)
61
+ @authenticator = load_authenticator(@scheme, @username, @password, **extra)
62
+ end
63
+
64
+ def shift
65
+ # TODO: this operation must be synchronized
66
+ @ns += 1
67
+ @uri = @uris[@ns]
68
+
69
+ return unless @uri
70
+
71
+ @uri = URI(@uri) unless @uri.is_a?(URI::Generic)
72
+
73
+ scheme = infer_default_auth_scheme(@uri)
74
+
75
+ return unless scheme != @scheme
76
+
77
+ @scheme = scheme
78
+ @username = username || @uri.user
79
+ @password = password || @uri.password
80
+ @authenticator = load_authenticator(scheme, @username, @password)
59
81
  end
60
82
 
61
83
  def can_authenticate?(*args)
@@ -87,6 +109,25 @@ module HTTPX
87
109
  super
88
110
  end
89
111
  end
112
+
113
+ private
114
+
115
+ def infer_default_auth_scheme(uri)
116
+ case uri.scheme
117
+ when "socks5"
118
+ uri.scheme
119
+ when "http", "https"
120
+ "basic"
121
+ end
122
+ end
123
+
124
+ def load_authenticator(scheme, username, password, **extra)
125
+ auth_scheme = scheme.to_s.capitalize
126
+
127
+ require_relative "auth/#{scheme}" unless defined?(Authentication) && Authentication.const_defined?(auth_scheme, false)
128
+
129
+ Authentication.const_get(auth_scheme).new(username, password, **extra)
130
+ end
90
131
  end
91
132
 
92
133
  # adds support for the following options:
@@ -95,7 +136,7 @@ module HTTPX
95
136
  # *:scheme* (i.e. <tt>{ uri: "http://proxy" }</tt>)
96
137
  module OptionsMethods
97
138
  def option_proxy(value)
98
- value.is_a?(Parameters) ? value : Hash[value]
139
+ value.is_a?(Parameters) ? value : Parameters.new(**Hash[value])
99
140
  end
100
141
 
101
142
  def option_supported_proxy_protocols(value)
@@ -106,97 +147,68 @@ module HTTPX
106
147
  end
107
148
 
108
149
  module InstanceMethods
109
- private
110
-
111
- def find_connection(request, connections, options)
150
+ def find_connection(request_uri, selector, options)
112
151
  return super unless options.respond_to?(:proxy)
113
152
 
114
- uri = request.uri
115
-
116
- proxy_options = proxy_options(uri, options)
117
-
118
- return super(request, connections, proxy_options) unless proxy_options.proxy
119
-
120
- connection = pool.find_connection(uri, proxy_options) || init_connection(uri, proxy_options)
121
- unless connections.nil? || connections.include?(connection)
122
- connections << connection
123
- set_connection_callbacks(connection, connections, options)
153
+ if (next_proxy = request_uri.find_proxy)
154
+ return super(request_uri, selector, options.merge(proxy: Parameters.new(uri: next_proxy)))
124
155
  end
125
- connection
126
- end
127
-
128
- def proxy_options(request_uri, options)
129
- proxy_opts = if (next_proxy = request_uri.find_proxy)
130
- { uri: next_proxy }
131
- else
132
- proxy = options.proxy
133
-
134
- return options unless proxy
135
-
136
- return options.merge(proxy: nil) unless proxy.key?(:uri)
137
156
 
138
- @_proxy_uris ||= Array(proxy[:uri])
157
+ proxy = options.proxy
139
158
 
140
- next_proxy = @_proxy_uris.first
141
- raise Error, "Failed to connect to proxy" unless next_proxy
159
+ return super unless proxy
142
160
 
143
- next_proxy = URI(next_proxy)
161
+ next_proxy = proxy.uri
144
162
 
145
- raise Error,
146
- "#{next_proxy.scheme}: unsupported proxy protocol" unless options.supported_proxy_protocols.include?(next_proxy.scheme)
163
+ raise Error, "Failed to connect to proxy" unless next_proxy
147
164
 
148
- if proxy.key?(:no_proxy)
165
+ raise Error,
166
+ "#{next_proxy.scheme}: unsupported proxy protocol" unless options.supported_proxy_protocols.include?(next_proxy.scheme)
149
167
 
150
- no_proxy = proxy[:no_proxy]
151
- no_proxy = no_proxy.join(",") if no_proxy.is_a?(Array)
168
+ if (no_proxy = proxy.no_proxy)
169
+ no_proxy = no_proxy.join(",") if no_proxy.is_a?(Array)
152
170
 
153
- return options.merge(proxy: nil) unless URI::Generic.use_proxy?(request_uri.host, next_proxy.host,
154
- next_proxy.port, no_proxy)
155
- end
156
-
157
- proxy.merge(uri: next_proxy)
171
+ # TODO: setting proxy to nil leaks the connection object in the pool
172
+ return super(request_uri, selector, options.merge(proxy: nil)) unless URI::Generic.use_proxy?(request_uri.host, next_proxy.host,
173
+ next_proxy.port, no_proxy)
158
174
  end
159
175
 
160
- proxy = Parameters.new(**proxy_opts)
161
-
162
- options.merge(proxy: proxy)
176
+ super(request_uri, selector, options.merge(proxy: proxy))
163
177
  end
164
178
 
165
- def fetch_response(request, connections, options)
166
- response = super
179
+ private
167
180
 
168
- if response.is_a?(ErrorResponse) && proxy_error?(request, response)
169
- return response unless @_proxy_uris
181
+ def fetch_response(request, selector, options)
182
+ response = super
170
183
 
171
- @_proxy_uris.shift
184
+ if response.is_a?(ErrorResponse) && proxy_error?(request, response, options)
185
+ options.proxy.shift
172
186
 
173
187
  # return last error response if no more proxies to try
174
- return response if @_proxy_uris.empty?
188
+ return response if options.proxy.uri.nil?
175
189
 
176
190
  log { "failed connecting to proxy, trying next..." }
177
191
  request.transition(:idle)
178
- send_request(request, connections, options)
192
+ send_request(request, selector, options)
179
193
  return
180
194
  end
181
195
  response
182
196
  end
183
197
 
184
- def proxy_error?(_request, response)
198
+ def proxy_error?(_request, response, options)
199
+ return false unless options.proxy
200
+
185
201
  error = response.error
186
202
  case error
187
203
  when NativeResolveError
188
- return false unless @_proxy_uris && !@_proxy_uris.empty?
204
+ proxy_uri = URI(options.proxy.uri)
189
205
 
190
- proxy_uri = URI(@_proxy_uris.first)
191
-
192
- origin = error.connection.origin
206
+ peer = error.connection.peer
193
207
 
194
208
  # failed resolving proxy domain
195
- origin.host == proxy_uri.host && origin.port == proxy_uri.port
209
+ peer.host == proxy_uri.host && peer.port == proxy_uri.port
196
210
  when ResolveError
197
- return false unless @_proxy_uris && !@_proxy_uris.empty?
198
-
199
- proxy_uri = URI(@_proxy_uris.first)
211
+ proxy_uri = URI(options.proxy.uri)
200
212
 
201
213
  error.message.end_with?(proxy_uri.to_s)
202
214
  when *PROXY_ERRORS
@@ -217,25 +229,11 @@ module HTTPX
217
229
 
218
230
  # redefining the connection origin as the proxy's URI,
219
231
  # as this will be used as the tcp peer ip.
220
- proxy_uri = URI(@options.proxy.uri)
221
- @origin.host = proxy_uri.host
222
- @origin.port = proxy_uri.port
232
+ @proxy_uri = URI(@options.proxy.uri)
223
233
  end
224
234
 
225
- def coalescable?(connection)
226
- return super unless @options.proxy
227
-
228
- if @io.protocol == "h2" &&
229
- @origin.scheme == "https" &&
230
- connection.origin.scheme == "https" &&
231
- @io.can_verify_peer?
232
- # in proxied connections, .origin is the proxy ; Given names
233
- # are stored in .origins, this is what is used.
234
- origin = URI(connection.origins.first)
235
- @io.verify_hostname(origin.host)
236
- else
237
- @origin == connection.origin
238
- end
235
+ def peer
236
+ @proxy_uri || super
239
237
  end
240
238
 
241
239
  def connecting?
@@ -261,7 +259,7 @@ module HTTPX
261
259
  @state = :open
262
260
 
263
261
  super
264
- emit(:close)
262
+ # emit(:close)
265
263
  end
266
264
 
267
265
  private
@@ -94,7 +94,7 @@ module HTTPX
94
94
 
95
95
  private
96
96
 
97
- def fetch_response(request, connections, options)
97
+ def fetch_response(request, selector, options)
98
98
  response = super
99
99
 
100
100
  if response &&
@@ -124,20 +124,17 @@ module HTTPX
124
124
 
125
125
  retry_start = Utils.now
126
126
  log { "retrying after #{retry_after} secs..." }
127
-
128
- deactivate_connection(request, connections, options)
129
-
130
- pool.after(retry_after) do
127
+ selector.after(retry_after) do
131
128
  if request.response
132
129
  # request has terminated abruptly meanwhile
133
130
  request.emit(:response, request.response)
134
131
  else
135
132
  log { "retrying (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
136
- send_request(request, connections, options)
133
+ send_request(request, selector, options)
137
134
  end
138
135
  end
139
136
  else
140
- send_request(request, connections, options)
137
+ send_request(request, selector, options)
141
138
  end
142
139
 
143
140
  return
@@ -153,7 +150,7 @@ module HTTPX
153
150
  RETRYABLE_ERRORS.any? { |klass| ex.is_a?(klass) }
154
151
  end
155
152
 
156
- def proxy_error?(request, response)
153
+ def proxy_error?(request, response, _)
157
154
  super && !request.retries.positive?
158
155
  end
159
156
 
@@ -28,7 +28,7 @@ module HTTPX
28
28
  end
29
29
 
30
30
  module InstanceMethods
31
- def fetch_response(request, connections, options)
31
+ def fetch_response(request, selector, options)
32
32
  response = super
33
33
 
34
34
  if response
@@ -45,7 +45,7 @@ module HTTPX
45
45
  return response unless protocol_handler
46
46
 
47
47
  log { "upgrading to #{upgrade_protocol}..." }
48
- connection = find_connection(request, connections, options)
48
+ connection = find_connection(request.uri, selector, options)
49
49
 
50
50
  # do not upgrade already upgraded connections
51
51
  return if connection.upgrade_protocol == upgrade_protocol
@@ -60,14 +60,6 @@ module HTTPX
60
60
 
61
61
  response
62
62
  end
63
-
64
- def close(*args)
65
- return super if args.empty?
66
-
67
- connections, = args
68
-
69
- pool.close(connections.reject(&:hijacked))
70
- end
71
63
  end
72
64
 
73
65
  module ConnectionMethods
@@ -75,6 +67,9 @@ module HTTPX
75
67
 
76
68
  def hijack_io
77
69
  @hijacked = true
70
+
71
+ # connection is taken away from selector and not given back to the pool.
72
+ @current_session.deselect_connection(self, @current_selector, true)
78
73
  end
79
74
  end
80
75
  end
@@ -8,6 +8,10 @@ module HTTPX
8
8
  # https://gitlab.com/os85/httpx/wikis/WebDav
9
9
  #
10
10
  module WebDav
11
+ def self.configure(klass)
12
+ klass.plugin(:xml)
13
+ end
14
+
11
15
  module InstanceMethods
12
16
  def copy(src, dest)
13
17
  request("COPY", src, headers: { "destination" => @options.origin.merge(dest) })
@@ -43,6 +47,8 @@ module HTTPX
43
47
  ensure
44
48
  unlock(path, lock_token)
45
49
  end
50
+
51
+ response
46
52
  end
47
53
 
48
54
  def unlock(path, lock_token)
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # This plugin supports request XML encoding/response decoding using the nokogiri gem.
7
+ #
8
+ # https://gitlab.com/os85/httpx/wikis/XML
9
+ #
10
+ module XML
11
+ MIME_TYPES = %r{\b(application|text)/(.+\+)?xml\b}.freeze
12
+ module Transcoder
13
+ module_function
14
+
15
+ class Encoder
16
+ def initialize(xml)
17
+ @raw = xml
18
+ end
19
+
20
+ def content_type
21
+ charset = @raw.respond_to?(:encoding) && @raw.encoding ? @raw.encoding.to_s.downcase : "utf-8"
22
+ "application/xml; charset=#{charset}"
23
+ end
24
+
25
+ def bytesize
26
+ @raw.to_s.bytesize
27
+ end
28
+
29
+ def to_s
30
+ @raw.to_s
31
+ end
32
+ end
33
+
34
+ def encode(xml)
35
+ Encoder.new(xml)
36
+ end
37
+
38
+ def decode(response)
39
+ content_type = response.content_type.mime_type
40
+
41
+ raise HTTPX::Error, "invalid form mime type (#{content_type})" unless MIME_TYPES.match?(content_type)
42
+
43
+ Nokogiri::XML.method(:parse)
44
+ end
45
+ end
46
+
47
+ class << self
48
+ def load_dependencies(*)
49
+ require "nokogiri"
50
+ end
51
+ end
52
+
53
+ module ResponseMethods
54
+ # decodes the response payload into a Nokogiri::XML::Node object **if** the payload is valid
55
+ # "application/xml" (requires the "nokogiri" gem).
56
+ def xml
57
+ decode(Transcoder)
58
+ end
59
+ end
60
+
61
+ module RequestBodyClassMethods
62
+ # ..., xml: Nokogiri::XML::Node #=> xml encoder
63
+ def initialize_body(params)
64
+ if (xml = params.delete(:xml))
65
+ # @type var xml: Nokogiri::XML::Node | String
66
+ return Transcoder.encode(xml)
67
+ end
68
+
69
+ super
70
+ end
71
+ end
72
+ end
73
+
74
+ register_plugin(:xml, XML)
75
+ end
76
+ end