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
@@ -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
+
41
+ @username = username
42
+ @password = password
40
43
 
41
- return unless @username && @password
44
+ @ns = 0
42
45
 
43
- scheme ||= case @uri.scheme
44
- when "socks5"
45
- @uri.scheme
46
- when "http", "https"
47
- "basic"
48
- else
49
- return
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,11 +109,34 @@ 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
 
133
+ # adds support for the following options:
134
+ #
135
+ # :proxy :: proxy options defining *:uri*, *:username*, *:password* or
136
+ # *:scheme* (i.e. <tt>{ uri: "http://proxy" }</tt>)
92
137
  module OptionsMethods
93
138
  def option_proxy(value)
94
- value.is_a?(Parameters) ? value : Hash[value]
139
+ value.is_a?(Parameters) ? value : Parameters.new(**Hash[value])
95
140
  end
96
141
 
97
142
  def option_supported_proxy_protocols(value)
@@ -102,88 +147,68 @@ module HTTPX
102
147
  end
103
148
 
104
149
  module InstanceMethods
105
- private
106
-
107
- def find_connection(request, connections, options)
150
+ def find_connection(request_uri, selector, options)
108
151
  return super unless options.respond_to?(:proxy)
109
152
 
110
- uri = URI(request.uri)
111
-
112
- proxy_opts = if (next_proxy = uri.find_proxy)
113
- { uri: next_proxy }
114
- else
115
- proxy = options.proxy
116
-
117
- return super unless proxy
118
-
119
- return super(request, connections, options.merge(proxy: nil)) unless proxy.key?(:uri)
120
-
121
- @_proxy_uris ||= Array(proxy[:uri])
153
+ if (next_proxy = request_uri.find_proxy)
154
+ return super(request_uri, selector, options.merge(proxy: Parameters.new(uri: next_proxy)))
155
+ end
122
156
 
123
- next_proxy = @_proxy_uris.first
124
- raise Error, "Failed to connect to proxy" unless next_proxy
157
+ proxy = options.proxy
125
158
 
126
- next_proxy = URI(next_proxy)
159
+ return super unless proxy
127
160
 
128
- raise Error,
129
- "#{next_proxy.scheme}: unsupported proxy protocol" unless options.supported_proxy_protocols.include?(next_proxy.scheme)
161
+ next_proxy = proxy.uri
130
162
 
131
- if proxy.key?(:no_proxy)
163
+ raise Error, "Failed to connect to proxy" unless next_proxy
132
164
 
133
- no_proxy = proxy[:no_proxy]
134
- no_proxy = no_proxy.join(",") if no_proxy.is_a?(Array)
165
+ raise Error,
166
+ "#{next_proxy.scheme}: unsupported proxy protocol" unless options.supported_proxy_protocols.include?(next_proxy.scheme)
135
167
 
136
- return super(request, connections, options.merge(proxy: nil)) unless URI::Generic.use_proxy?(uri.host, next_proxy.host,
137
- next_proxy.port, no_proxy)
138
- end
168
+ if (no_proxy = proxy.no_proxy)
169
+ no_proxy = no_proxy.join(",") if no_proxy.is_a?(Array)
139
170
 
140
- 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)
141
174
  end
142
175
 
143
- proxy = Parameters.new(**proxy_opts)
144
-
145
- proxy_options = options.merge(proxy: proxy)
146
- connection = pool.find_connection(uri, proxy_options) || init_connection(uri, proxy_options)
147
- unless connections.nil? || connections.include?(connection)
148
- connections << connection
149
- set_connection_callbacks(connection, connections, options)
150
- end
151
- connection
176
+ super(request_uri, selector, options.merge(proxy: proxy))
152
177
  end
153
178
 
154
- def fetch_response(request, connections, options)
179
+ private
180
+
181
+ def fetch_response(request, selector, options)
155
182
  response = super
156
183
 
157
- if response.is_a?(ErrorResponse) && proxy_error?(request, response)
158
- @_proxy_uris.shift
184
+ if response.is_a?(ErrorResponse) && proxy_error?(request, response, options)
185
+ options.proxy.shift
159
186
 
160
187
  # return last error response if no more proxies to try
161
- return response if @_proxy_uris.empty?
188
+ return response if options.proxy.uri.nil?
162
189
 
163
190
  log { "failed connecting to proxy, trying next..." }
164
191
  request.transition(:idle)
165
- send_request(request, connections, options)
192
+ send_request(request, selector, options)
166
193
  return
167
194
  end
168
195
  response
169
196
  end
170
197
 
171
- def proxy_error?(_request, response)
198
+ def proxy_error?(_request, response, options)
199
+ return false unless options.proxy
200
+
172
201
  error = response.error
173
202
  case error
174
203
  when NativeResolveError
175
- return false unless @_proxy_uris && !@_proxy_uris.empty?
176
-
177
- proxy_uri = URI(@_proxy_uris.first)
204
+ proxy_uri = URI(options.proxy.uri)
178
205
 
179
- origin = error.connection.origin
206
+ peer = error.connection.peer
180
207
 
181
208
  # failed resolving proxy domain
182
- origin.host == proxy_uri.host && origin.port == proxy_uri.port
209
+ peer.host == proxy_uri.host && peer.port == proxy_uri.port
183
210
  when ResolveError
184
- return false unless @_proxy_uris && !@_proxy_uris.empty?
185
-
186
- proxy_uri = URI(@_proxy_uris.first)
211
+ proxy_uri = URI(options.proxy.uri)
187
212
 
188
213
  error.message.end_with?(proxy_uri.to_s)
189
214
  when *PROXY_ERRORS
@@ -204,25 +229,11 @@ module HTTPX
204
229
 
205
230
  # redefining the connection origin as the proxy's URI,
206
231
  # as this will be used as the tcp peer ip.
207
- proxy_uri = URI(@options.proxy.uri)
208
- @origin.host = proxy_uri.host
209
- @origin.port = proxy_uri.port
232
+ @proxy_uri = URI(@options.proxy.uri)
210
233
  end
211
234
 
212
- def coalescable?(connection)
213
- return super unless @options.proxy
214
-
215
- if @io.protocol == "h2" &&
216
- @origin.scheme == "https" &&
217
- connection.origin.scheme == "https" &&
218
- @io.can_verify_peer?
219
- # in proxied connections, .origin is the proxy ; Given names
220
- # are stored in .origins, this is what is used.
221
- origin = URI(connection.origins.first)
222
- @io.verify_hostname(origin.host)
223
- else
224
- @origin == connection.origin
225
- end
235
+ def peer
236
+ @proxy_uri || super
226
237
  end
227
238
 
228
239
  def connecting?
@@ -248,7 +259,7 @@ module HTTPX
248
259
  @state = :open
249
260
 
250
261
  super
251
- emit(:close)
262
+ # emit(:close)
252
263
  end
253
264
 
254
265
  private
@@ -3,7 +3,12 @@
3
3
  module HTTPX
4
4
  module Plugins
5
5
  #
6
- # This plugin adds support for retrying requests when certain errors happen.
6
+ # This plugin adds support for retrying requests when errors happen.
7
+ #
8
+ # It has a default max number of retries (see *MAX_RETRIES* and the *max_retries* option),
9
+ # after which it will return the last response, error or not. It will **not** raise an exception.
10
+ #
11
+ # It does not retry which are not considered idempotent (see *retry_change_requests* to override).
7
12
  #
8
13
  # https://gitlab.com/os85/httpx/wikis/Retries
9
14
  #
@@ -38,6 +43,14 @@ module HTTPX
38
43
  end
39
44
  end
40
45
 
46
+ # adds support for the following options:
47
+ #
48
+ # :max_retries :: max number of times a request will be retried (defaults to <tt>3</tt>).
49
+ # :retry_change_requests :: whether idempotent requests are retried (defaults to <tt>false</tt>).
50
+ # :retry_after:: seconds after which a request is retried; can also be a callable object (i.e. <tt>->(req, res) { ... } </tt>)
51
+ # :retry_jitter :: number of seconds applied to *:retry_after* (must be a callable, i.e. <tt>->(retry_after) { ... } </tt>).
52
+ # :retry_on :: callable which alternatively defines a different rule for when a response is to be retried
53
+ # (i.e. <tt>->(res) { ... }</tt>).
41
54
  module OptionsMethods
42
55
  def option_retry_after(value)
43
56
  # return early if callable
@@ -76,12 +89,12 @@ module HTTPX
76
89
 
77
90
  module InstanceMethods
78
91
  def max_retries(n)
79
- with(max_retries: n.to_i)
92
+ with(max_retries: n)
80
93
  end
81
94
 
82
95
  private
83
96
 
84
- def fetch_response(request, connections, options)
97
+ def fetch_response(request, selector, options)
85
98
  response = super
86
99
 
87
100
  if response &&
@@ -97,7 +110,7 @@ module HTTPX
97
110
  )
98
111
  __try_partial_retry(request, response)
99
112
  log { "failed to get response, #{request.retries} tries to go..." }
100
- request.retries -= 1
113
+ request.retries -= 1 unless request.ping? # do not exhaust retries on connection liveness probes
101
114
  request.transition(:idle)
102
115
 
103
116
  retry_after = options.retry_after
@@ -111,12 +124,17 @@ module HTTPX
111
124
 
112
125
  retry_start = Utils.now
113
126
  log { "retrying after #{retry_after} secs..." }
114
- pool.after(retry_after) do
115
- log { "retrying (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
116
- send_request(request, connections, options)
127
+ selector.after(retry_after) do
128
+ if request.response
129
+ # request has terminated abruptly meanwhile
130
+ request.emit(:response, request.response)
131
+ else
132
+ log { "retrying (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
133
+ send_request(request, selector, options)
134
+ end
117
135
  end
118
136
  else
119
- send_request(request, connections, options)
137
+ send_request(request, selector, options)
120
138
  end
121
139
 
122
140
  return
@@ -132,7 +150,7 @@ module HTTPX
132
150
  RETRYABLE_ERRORS.any? { |klass| ex.is_a?(klass) }
133
151
  end
134
152
 
135
- def proxy_error?(request, response)
153
+ def proxy_error?(request, response, _)
136
154
  super && !request.retries.positive?
137
155
  end
138
156
 
@@ -149,7 +167,7 @@ module HTTPX
149
167
  unless response.headers.key?("accept-ranges") &&
150
168
  response.headers["accept-ranges"] == "bytes" && # there's nothing else supported though...
151
169
  (original_body = response.body)
152
- response.close if response.respond_to?(:close)
170
+ response.body.close
153
171
  return
154
172
  end
155
173
 
@@ -87,6 +87,9 @@ module HTTPX
87
87
  end
88
88
  end
89
89
 
90
+ # adds support for the following options:
91
+ #
92
+ # :allowed_schemes :: list of URI schemes allowed (defaults to <tt>["https", "http"]</tt>)
90
93
  module OptionsMethods
91
94
  def option_allowed_schemes(value)
92
95
  Array(value)
@@ -100,7 +103,7 @@ module HTTPX
100
103
 
101
104
  error = ServerSideRequestForgeryError.new("#{request.uri} URI scheme not allowed")
102
105
  error.set_backtrace(caller)
103
- response = ErrorResponse.new(request, error, request.options)
106
+ response = ErrorResponse.new(request, error)
104
107
  request.emit(:response, response)
105
108
  response
106
109
  end
@@ -4,27 +4,39 @@ module HTTPX
4
4
  class StreamResponse
5
5
  def initialize(request, session)
6
6
  @request = request
7
+ @options = @request.options
7
8
  @session = session
8
- @response = nil
9
+ @response_enum = nil
10
+ @buffered_chunks = []
9
11
  end
10
12
 
11
13
  def each(&block)
12
14
  return enum_for(__method__) unless block
13
15
 
16
+ if (response_enum = @response_enum)
17
+ @response_enum = nil
18
+ # streaming already started, let's finish it
19
+
20
+ while (chunk = @buffered_chunks.shift)
21
+ block.call(chunk)
22
+ end
23
+
24
+ # consume enum til the end
25
+ begin
26
+ while (chunk = response_enum.next)
27
+ block.call(chunk)
28
+ end
29
+ rescue StopIteration
30
+ return
31
+ end
32
+ end
33
+
14
34
  @request.stream = self
15
35
 
16
36
  begin
17
37
  @on_chunk = block
18
38
 
19
- if @request.response
20
- # if we've already started collecting the payload, yield it first
21
- # before proceeding.
22
- body = @request.response.body
23
-
24
- body.each do |chunk|
25
- on_chunk(chunk)
26
- end
27
- end
39
+ response = @session.request(@request)
28
40
 
29
41
  response.raise_for_status
30
42
  ensure
@@ -64,27 +76,39 @@ module HTTPX
64
76
  # :nocov:
65
77
 
66
78
  def to_s
67
- response.to_s
79
+ if @request.response
80
+ @request.response.to_s
81
+ else
82
+ @buffered_chunks.join
83
+ end
68
84
  end
69
85
 
70
86
  private
71
87
 
72
88
  def response
73
- return @response if @response
74
-
75
89
  @request.response || begin
76
- @response = @session.request(@request)
90
+ response_enum = each
91
+ while (chunk = response_enum.next)
92
+ @buffered_chunks << chunk
93
+ break if @request.response
94
+ end
95
+ @response_enum = response_enum
96
+ @request.response
77
97
  end
78
98
  end
79
99
 
80
- def respond_to_missing?(meth, *args)
81
- response.respond_to?(meth, *args) || super
100
+ def respond_to_missing?(meth, include_private)
101
+ if (response = @request.response)
102
+ response.respond_to_missing?(meth, include_private)
103
+ else
104
+ @options.response_class.method_defined?(meth) || (include_private && @options.response_class.private_method_defined?(meth))
105
+ end || super
82
106
  end
83
107
 
84
- def method_missing(meth, *args, &block)
108
+ def method_missing(meth, *args, **kwargs, &block)
85
109
  return super unless response.respond_to?(meth)
86
110
 
87
- response.__send__(meth, *args, &block)
111
+ response.__send__(meth, *args, **kwargs, &block)
88
112
  end
89
113
  end
90
114
 
@@ -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