httpx 1.3.3 → 1.4.0

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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_3_3.md +1 -1
  3. data/doc/release_notes/1_3_4.md +6 -0
  4. data/doc/release_notes/1_4_0.md +43 -0
  5. data/lib/httpx/adapters/faraday.rb +2 -0
  6. data/lib/httpx/adapters/webmock.rb +11 -5
  7. data/lib/httpx/callbacks.rb +0 -5
  8. data/lib/httpx/chainable.rb +3 -1
  9. data/lib/httpx/connection/http2.rb +11 -7
  10. data/lib/httpx/connection.rb +128 -16
  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/h2c.rb +23 -20
  21. data/lib/httpx/plugins/internal_telemetry.rb +27 -0
  22. data/lib/httpx/plugins/persistent.rb +16 -0
  23. data/lib/httpx/plugins/proxy/http.rb +17 -19
  24. data/lib/httpx/plugins/proxy.rb +91 -93
  25. data/lib/httpx/plugins/retries.rb +5 -8
  26. data/lib/httpx/plugins/upgrade.rb +5 -10
  27. data/lib/httpx/plugins/webdav.rb +6 -0
  28. data/lib/httpx/plugins/xml.rb +76 -0
  29. data/lib/httpx/pool.rb +73 -244
  30. data/lib/httpx/request/body.rb +16 -12
  31. data/lib/httpx/request.rb +1 -1
  32. data/lib/httpx/resolver/https.rb +12 -19
  33. data/lib/httpx/resolver/multi.rb +34 -16
  34. data/lib/httpx/resolver/native.rb +36 -13
  35. data/lib/httpx/resolver/resolver.rb +49 -11
  36. data/lib/httpx/resolver/system.rb +29 -11
  37. data/lib/httpx/resolver.rb +21 -14
  38. data/lib/httpx/response/body.rb +12 -1
  39. data/lib/httpx/response.rb +5 -3
  40. data/lib/httpx/selector.rb +164 -95
  41. data/lib/httpx/session.rb +296 -139
  42. data/lib/httpx/transcoder/gzip.rb +0 -3
  43. data/lib/httpx/transcoder/json.rb +14 -2
  44. data/lib/httpx/transcoder/multipart/encoder.rb +3 -1
  45. data/lib/httpx/transcoder/utils/deflater.rb +7 -4
  46. data/lib/httpx/transcoder/utils/inflater.rb +2 -0
  47. data/lib/httpx/transcoder.rb +0 -1
  48. data/lib/httpx/version.rb +1 -1
  49. data/lib/httpx.rb +19 -20
  50. data/sig/callbacks.rbs +0 -1
  51. data/sig/chainable.rbs +4 -0
  52. data/sig/connection/http2.rbs +1 -1
  53. data/sig/connection.rbs +14 -3
  54. data/sig/errors.rbs +6 -0
  55. data/sig/loggable.rbs +2 -0
  56. data/sig/options.rbs +7 -0
  57. data/sig/plugins/aws_sigv4.rbs +8 -2
  58. data/sig/plugins/content_digest.rbs +51 -0
  59. data/sig/plugins/cookies/cookie.rbs +9 -0
  60. data/sig/plugins/grpc/call.rbs +4 -0
  61. data/sig/plugins/persistent.rbs +4 -1
  62. data/sig/plugins/proxy/socks5.rbs +11 -3
  63. data/sig/plugins/proxy.rbs +18 -11
  64. data/sig/plugins/push_promise.rbs +3 -0
  65. data/sig/plugins/rate_limiter.rbs +2 -0
  66. data/sig/plugins/retries.rbs +1 -1
  67. data/sig/plugins/ssrf_filter.rbs +26 -0
  68. data/sig/plugins/webdav.rbs +23 -0
  69. data/sig/plugins/xml.rbs +37 -0
  70. data/sig/pool.rbs +25 -33
  71. data/sig/request/body.rbs +5 -1
  72. data/sig/resolver/multi.rbs +26 -1
  73. data/sig/resolver/native.rbs +0 -2
  74. data/sig/resolver/resolver.rbs +21 -2
  75. data/sig/resolver.rbs +5 -1
  76. data/sig/response/body.rbs +2 -2
  77. data/sig/response/buffer.rbs +2 -2
  78. data/sig/selector.rbs +30 -4
  79. data/sig/session.rbs +45 -18
  80. data/sig/transcoder/body.rbs +1 -1
  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/multipart.rbs +3 -3
  86. data/sig/transcoder/utils/body_reader.rbs +2 -2
  87. data/sig/transcoder/utils/deflater.rbs +2 -2
  88. metadata +12 -4
  89. data/lib/httpx/transcoder/xml.rb +0 -52
  90. 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
@@ -17,12 +17,23 @@ module HTTPX
17
17
  @headers = response.headers
18
18
  @options = options
19
19
  @window_size = options.window_size
20
- @encoding = response.content_type.charset || Encoding::BINARY
21
20
  @encodings = []
22
21
  @length = 0
23
22
  @buffer = nil
24
23
  @reader = nil
25
24
  @state = :idle
25
+
26
+ # initialize response encoding
27
+ @encoding = if (enc = response.content_type.charset)
28
+ begin
29
+ Encoding.find(enc)
30
+ rescue ArgumentError
31
+ Encoding::BINARY
32
+ end
33
+ else
34
+ Encoding::BINARY
35
+ end
36
+
26
37
  initialize_inflaters
27
38
  end
28
39
 
@@ -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