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
@@ -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