httpx 0.20.5 → 0.21.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +44 -26
  3. data/doc/release_notes/0_13_0.md +1 -1
  4. data/doc/release_notes/0_21_0.md +96 -0
  5. data/doc/release_notes/0_21_1.md +12 -0
  6. data/lib/httpx/connection/http1.rb +2 -1
  7. data/lib/httpx/connection.rb +47 -3
  8. data/lib/httpx/errors.rb +18 -0
  9. data/lib/httpx/extensions.rb +8 -4
  10. data/lib/httpx/io/unix.rb +1 -1
  11. data/lib/httpx/options.rb +7 -3
  12. data/lib/httpx/plugins/circuit_breaker/circuit.rb +76 -0
  13. data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +44 -0
  14. data/lib/httpx/plugins/circuit_breaker.rb +115 -0
  15. data/lib/httpx/plugins/cookies.rb +1 -1
  16. data/lib/httpx/plugins/expect.rb +1 -1
  17. data/lib/httpx/plugins/multipart/decoder.rb +1 -1
  18. data/lib/httpx/plugins/proxy.rb +7 -1
  19. data/lib/httpx/plugins/retries.rb +1 -1
  20. data/lib/httpx/plugins/webdav.rb +78 -0
  21. data/lib/httpx/request.rb +15 -25
  22. data/lib/httpx/resolver/https.rb +2 -7
  23. data/lib/httpx/resolver/native.rb +19 -8
  24. data/lib/httpx/response.rb +27 -9
  25. data/lib/httpx/timers.rb +3 -0
  26. data/lib/httpx/transcoder/form.rb +1 -1
  27. data/lib/httpx/transcoder/json.rb +19 -3
  28. data/lib/httpx/transcoder/xml.rb +57 -0
  29. data/lib/httpx/transcoder.rb +1 -0
  30. data/lib/httpx/version.rb +1 -1
  31. data/sig/buffer.rbs +1 -1
  32. data/sig/chainable.rbs +1 -0
  33. data/sig/connection.rbs +12 -4
  34. data/sig/errors.rbs +13 -0
  35. data/sig/io.rbs +6 -0
  36. data/sig/options.rbs +4 -1
  37. data/sig/plugins/circuit_breaker.rbs +61 -0
  38. data/sig/plugins/compression/brotli.rbs +1 -1
  39. data/sig/plugins/compression/deflate.rbs +1 -1
  40. data/sig/plugins/compression/gzip.rbs +3 -3
  41. data/sig/plugins/compression.rbs +1 -1
  42. data/sig/plugins/multipart.rbs +1 -1
  43. data/sig/plugins/proxy/socks5.rbs +3 -2
  44. data/sig/plugins/proxy.rbs +1 -1
  45. data/sig/registry.rbs +5 -4
  46. data/sig/request.rbs +7 -1
  47. data/sig/resolver/native.rbs +5 -2
  48. data/sig/response.rbs +3 -1
  49. data/sig/timers.rbs +1 -1
  50. data/sig/transcoder/json.rbs +4 -1
  51. data/sig/transcoder/xml.rbs +21 -0
  52. data/sig/transcoder.rbs +2 -2
  53. data/sig/utils.rbs +2 -2
  54. metadata +15 -3
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # This plugin implements a circuit breaker around connection errors.
7
+ #
8
+ # https://gitlab.com/honeyryderchuck/httpx/wikis/Circuit-Breaker
9
+ #
10
+ module CircuitBreaker
11
+ using URIExtensions
12
+
13
+ def self.load_dependencies(*)
14
+ require_relative "circuit_breaker/circuit"
15
+ require_relative "circuit_breaker/circuit_store"
16
+ end
17
+
18
+ def self.extra_options(options)
19
+ options.merge(circuit_breaker_max_attempts: 3, circuit_breaker_reset_attempts_in: 60, circuit_breaker_break_in: 60,
20
+ circuit_breaker_half_open_drip_rate: 1)
21
+ end
22
+
23
+ module InstanceMethods
24
+ def initialize(*)
25
+ super
26
+ @circuit_store = CircuitStore.new(@options)
27
+ end
28
+
29
+ def initialize_dup(orig)
30
+ super
31
+ @circuit_store = orig.instance_variable_get(:@circuit_store).dup
32
+ end
33
+
34
+ def send_requests(*requests)
35
+ # @type var short_circuit_responses: Array[response]
36
+ short_circuit_responses = []
37
+
38
+ # run all requests through the circuit breaker, see if the circuit is
39
+ # open for any of them.
40
+ real_requests = requests.each_with_object([]) do |req, real_reqs|
41
+ short_circuit_response = @circuit_store.try_respond(req)
42
+ if short_circuit_response.nil?
43
+ real_reqs << req
44
+ next
45
+ end
46
+ short_circuit_responses[requests.index(req)] = short_circuit_response
47
+ end
48
+
49
+ # run requests for the remainder
50
+ unless real_requests.empty?
51
+ responses = super(*real_requests)
52
+
53
+ real_requests.each_with_index do |request, idx|
54
+ short_circuit_responses[requests.index(request)] = responses[idx]
55
+ end
56
+ end
57
+
58
+ short_circuit_responses
59
+ end
60
+
61
+ def on_response(request, response)
62
+ if response.is_a?(ErrorResponse)
63
+ case response.error
64
+ when RequestTimeoutError
65
+ @circuit_store.try_open(request.uri, response)
66
+ else
67
+ @circuit_store.try_open(request.origin, response)
68
+ end
69
+ elsif (break_on = request.options.circuit_breaker_break_on) && break_on.call(response)
70
+ @circuit_store.try_open(request.uri, response)
71
+ end
72
+
73
+ super
74
+ end
75
+ end
76
+
77
+ module OptionsMethods
78
+ def option_circuit_breaker_max_attempts(value)
79
+ attempts = Integer(value)
80
+ raise TypeError, ":circuit_breaker_max_attempts must be positive" unless attempts.positive?
81
+
82
+ attempts
83
+ end
84
+
85
+ def option_circuit_breaker_reset_attempts_in(value)
86
+ timeout = Float(value)
87
+ raise TypeError, ":circuit_breaker_reset_attempts_in must be positive" unless timeout.positive?
88
+
89
+ timeout
90
+ end
91
+
92
+ def option_circuit_breaker_break_in(value)
93
+ timeout = Float(value)
94
+ raise TypeError, ":circuit_breaker_break_in must be positive" unless timeout.positive?
95
+
96
+ timeout
97
+ end
98
+
99
+ def option_circuit_breaker_half_open_drip_rate(value)
100
+ ratio = Float(value)
101
+ raise TypeError, ":circuit_breaker_half_open_drip_rate must be a number between 0 and 1" unless (0..1).cover?(ratio)
102
+
103
+ ratio
104
+ end
105
+
106
+ def option_circuit_breaker_break_on(value)
107
+ raise TypeError, ":circuit_breaker_break_on must be called with the response" unless value.respond_to?(:call)
108
+
109
+ value
110
+ end
111
+ end
112
+ end
113
+ register_plugin :circuit_breaker, CircuitBreaker
114
+ end
115
+ end
@@ -42,7 +42,7 @@ module HTTPX
42
42
 
43
43
  private
44
44
 
45
- def on_response(reuest, response)
45
+ def on_response(_request, response)
46
46
  if response && response.respond_to?(:headers) && (set_cookie = response.headers["set-cookie"])
47
47
 
48
48
  log { "cookies: set-cookie is over #{Cookie::MAX_LENGTH}" } if set_cookie.bytesize > Cookie::MAX_LENGTH
@@ -22,7 +22,7 @@ module HTTPX
22
22
 
23
23
  module OptionsMethods
24
24
  def option_expect_timeout(value)
25
- seconds = Integer(value)
25
+ seconds = Float(value)
26
26
  raise TypeError, ":expect_timeout must be positive" unless seconds.positive?
27
27
 
28
28
  seconds
@@ -61,7 +61,7 @@ module HTTPX::Plugins
61
61
  @state = :idle
62
62
  end
63
63
 
64
- def call(response, _)
64
+ def call(response, *)
65
65
  response.body.each do |chunk|
66
66
  @buffer << chunk
67
67
 
@@ -105,6 +105,10 @@ module HTTPX
105
105
  end
106
106
  return if @_proxy_uris.empty?
107
107
 
108
+ proxy = options.proxy
109
+
110
+ return { uri: uri.host } if proxy && proxy.key?(:no_proxy) && !Array(proxy[:no_proxy]).grep(uri.host).empty?
111
+
108
112
  proxy_opts = { uri: @_proxy_uris.first }
109
113
  proxy_opts = options.proxy.merge(proxy_opts) if options.proxy
110
114
  proxy_opts
@@ -117,7 +121,9 @@ module HTTPX
117
121
  next_proxy = proxy_uris(uri, options)
118
122
  raise Error, "Failed to connect to proxy" unless next_proxy
119
123
 
120
- proxy_options = options.merge(proxy: Parameters.new(**next_proxy))
124
+ proxy = Parameters.new(**next_proxy) unless next_proxy[:uri] == uri.host
125
+
126
+ proxy_options = options.merge(proxy: proxy)
121
127
  connection = pool.find_connection(uri, proxy_options) || build_connection(uri, proxy_options)
122
128
  unless connections.nil? || connections.include?(connection)
123
129
  connections << connection
@@ -67,7 +67,7 @@ module HTTPX
67
67
  end
68
68
 
69
69
  def option_retry_on(value)
70
- raise ":retry_on must be called with the response" unless value.respond_to?(:call)
70
+ raise TypeError, ":retry_on must be called with the response" unless value.respond_to?(:call)
71
71
 
72
72
  value
73
73
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # This plugin implements convenience methods for performing WEBDAV requests.
7
+ #
8
+ # https://gitlab.com/honeyryderchuck/httpx/wikis/WEBDAV
9
+ #
10
+ module WebDav
11
+ module InstanceMethods
12
+ def copy(src, dest)
13
+ request(:copy, src, headers: { "destination" => @options.origin.merge(dest) })
14
+ end
15
+
16
+ def move(src, dest)
17
+ request(:move, src, headers: { "destination" => @options.origin.merge(dest) })
18
+ end
19
+
20
+ def lock(path, timeout: nil, &blk)
21
+ headers = {}
22
+ headers["timeout"] = if timeout && timeout.positive?
23
+ "Second-#{timeout}"
24
+ else
25
+ "Infinite, Second-4100000000"
26
+ end
27
+ xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" \
28
+ "<D:lockinfo xmlns:D=\"DAV:\">" \
29
+ "<D:lockscope><D:exclusive/></D:lockscope>" \
30
+ "<D:locktype><D:write/></D:locktype>" \
31
+ "<D:owner>null</D:owner>" \
32
+ "</D:lockinfo>"
33
+ response = request(:lock, path, headers: headers, xml: xml)
34
+
35
+ return response unless blk && response.status == 200
36
+
37
+ lock_token = response.headers["lock-token"]
38
+
39
+ begin
40
+ blk.call(response)
41
+ ensure
42
+ unlock(path, lock_token)
43
+ end
44
+ end
45
+
46
+ def unlock(path, lock_token)
47
+ request(:unlock, path, headers: { "lock-token" => lock_token })
48
+ end
49
+
50
+ def mkcol(dir)
51
+ request(:mkcol, dir)
52
+ end
53
+
54
+ def propfind(path, xml = nil)
55
+ body = case xml
56
+ when :acl
57
+ '<?xml version="1.0" encoding="utf-8" ?><D:propfind xmlns:D="DAV:"><D:prop><D:owner/>' \
58
+ "<D:supported-privilege-set/><D:current-user-privilege-set/><D:acl/></D:prop></D:propfind>"
59
+ when nil
60
+ '<?xml version="1.0" encoding="utf-8"?><DAV:propfind xmlns:DAV="DAV:"><DAV:allprop/></DAV:propfind>'
61
+ else
62
+ xml
63
+ end
64
+
65
+ request(:propfind, path, headers: { "depth" => "1" }, xml: body)
66
+ end
67
+
68
+ def proppatch(path, xml)
69
+ body = "<?xml version=\"1.0\"?>" \
70
+ "<D:propertyupdate xmlns:D=\"DAV:\" xmlns:Z=\"http://ns.example.com/standards/z39.50/\">#{xml}</D:propertyupdate>"
71
+ request(:proppatch, path, xml: body)
72
+ end
73
+ # %i[ orderpatch acl report search]
74
+ end
75
+ end
76
+ register_plugin(:webdav, WebDav)
77
+ end
78
+ end
data/lib/httpx/request.rb CHANGED
@@ -9,29 +9,6 @@ module HTTPX
9
9
  include Callbacks
10
10
  using URIExtensions
11
11
 
12
- METHODS = [
13
- # RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1
14
- :options, :get, :head, :post, :put, :delete, :trace, :connect,
15
-
16
- # RFC 2518: HTTP Extensions for Distributed Authoring -- WEBDAV
17
- :propfind, :proppatch, :mkcol, :copy, :move, :lock, :unlock,
18
-
19
- # RFC 3648: WebDAV Ordered Collections Protocol
20
- :orderpatch,
21
-
22
- # RFC 3744: WebDAV Access Control Protocol
23
- :acl,
24
-
25
- # RFC 6352: vCard Extensions to WebDAV -- CardDAV
26
- :report,
27
-
28
- # RFC 5789: PATCH Method for HTTP
29
- :patch,
30
-
31
- # draft-reschke-webdav-search: WebDAV Search
32
- :search
33
- ].freeze
34
-
35
12
  USER_AGENT = "httpx.rb/#{VERSION}"
36
13
 
37
14
  attr_reader :verb, :uri, :headers, :body, :state, :options, :response
@@ -54,8 +31,6 @@ module HTTPX
54
31
  @uri = origin.merge("#{base_path}#{@uri}")
55
32
  end
56
33
 
57
- raise(Error, "unknown method: #{verb}") unless METHODS.include?(@verb)
58
-
59
34
  @headers = @options.headers_class.new(@options.headers)
60
35
  @headers["user-agent"] ||= USER_AGENT
61
36
  @headers["accept"] ||= "*/*"
@@ -64,6 +39,18 @@ module HTTPX
64
39
  @state = :idle
65
40
  end
66
41
 
42
+ def read_timeout
43
+ @options.timeout[:read_timeout]
44
+ end
45
+
46
+ def write_timeout
47
+ @options.timeout[:write_timeout]
48
+ end
49
+
50
+ def request_timeout
51
+ @options.timeout[:request_timeout]
52
+ end
53
+
67
54
  def trailers?
68
55
  defined?(@trailers)
69
56
  end
@@ -108,6 +95,7 @@ module HTTPX
108
95
 
109
96
  def path
110
97
  path = uri.path.dup
98
+ path = +"" if path.nil?
111
99
  path << "/" if path.empty?
112
100
  path << "?#{query}" unless query.empty?
113
101
  path
@@ -174,6 +162,8 @@ module HTTPX
174
162
  Transcoder.registry("form").encode(options.form)
175
163
  elsif options.json
176
164
  Transcoder.registry("json").encode(options.json)
165
+ elsif options.xml
166
+ Transcoder.registry("xml").encode(options.xml)
177
167
  end
178
168
  return if @body.nil?
179
169
 
@@ -102,7 +102,7 @@ module HTTPX
102
102
  @requests[request] = hostname
103
103
  resolver_connection.send(request)
104
104
  @connections << connection
105
- rescue ResolveError, Resolv::DNS::EncodeError, JSON::JSONError => e
105
+ rescue ResolveError, Resolv::DNS::EncodeError => e
106
106
  @queries.delete(hostname)
107
107
  emit_resolve_error(connection, connection.origin.host, e)
108
108
  end
@@ -129,7 +129,7 @@ module HTTPX
129
129
  def parse(request, response)
130
130
  begin
131
131
  answers = decode_response_body(response)
132
- rescue Resolv::DNS::DecodeError, JSON::JSONError => e
132
+ rescue Resolv::DNS::DecodeError => e
133
133
  host, connection = @queries.first
134
134
  @queries.delete(host)
135
135
  emit_resolve_error(connection, connection.origin.host, e)
@@ -203,11 +203,6 @@ module HTTPX
203
203
 
204
204
  def decode_response_body(response)
205
205
  case response.headers["content-type"]
206
- when "application/dns-json",
207
- "application/json",
208
- %r{^application/x-javascript} # because google...
209
- payload = JSON.parse(response.to_s)
210
- payload["Answer"]
211
206
  when "application/dns-udpwireformat",
212
207
  "application/dns-message"
213
208
  Resolver.decode_dns_answer(response.to_s)
@@ -13,14 +13,14 @@ module HTTPX
13
13
  **Resolv::DNS::Config.default_config_hash,
14
14
  packet_size: 512,
15
15
  timeouts: Resolver::RESOLVE_TIMEOUT,
16
- }.freeze
16
+ }
17
17
  else
18
18
  {
19
19
  nameserver: nil,
20
20
  **Resolv::DNS::Config.default_config_hash,
21
21
  packet_size: 512,
22
22
  timeouts: Resolver::RESOLVE_TIMEOUT,
23
- }.freeze
23
+ }
24
24
  end
25
25
 
26
26
  # nameservers for ipv6 are misconfigured in certain systems;
@@ -35,6 +35,8 @@ module HTTPX
35
35
  end
36
36
  end if DEFAULTS[:nameserver]
37
37
 
38
+ DEFAULTS.freeze
39
+
38
40
  DNS_PORT = 53
39
41
 
40
42
  def_delegator :@connections, :empty?
@@ -77,7 +79,8 @@ module HTTPX
77
79
  nil
78
80
  rescue Errno::EHOSTUNREACH => e
79
81
  @ns_index += 1
80
- if @ns_index < @nameserver.size
82
+ nameserver = @nameserver
83
+ if nameserver && @ns_index < nameserver.size
81
84
  log { "resolver: failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})" }
82
85
  transition(:idle)
83
86
  else
@@ -151,10 +154,21 @@ module HTTPX
151
154
  host = connection.origin.host
152
155
  timeout = (@timeouts[host][0] -= loop_time)
153
156
 
154
- return unless timeout.negative?
157
+ return unless timeout <= 0
155
158
 
156
159
  @timeouts[host].shift
157
- if @timeouts[host].empty?
160
+
161
+ if !@timeouts[host].empty?
162
+ log { "resolver: timeout after #{timeout}s, retry(#{@timeouts[host].first}) #{host}..." }
163
+ resolve(connection)
164
+ elsif @ns_index + 1 < @nameserver.size
165
+ # try on the next nameserver
166
+ @ns_index += 1
167
+ log { "resolver: failed resolving #{host} on nameserver #{@nameserver[@ns_index - 1]} (timeout error)" }
168
+ transition(:idle)
169
+ resolve(connection)
170
+ else
171
+
158
172
  @timeouts.delete(host)
159
173
  @queries.delete(h)
160
174
 
@@ -164,9 +178,6 @@ module HTTPX
164
178
  # This loop_time passed to the exception is bogus. Ideally we would pass the total
165
179
  # resolve timeout, including from the previous retries.
166
180
  raise ResolveTimeoutError.new(loop_time, "Timed out while resolving #{connection.origin.host}")
167
- else
168
- log { "resolver: timeout after #{timeout}s, retry(#{@timeouts[host].first}) #{host}..." }
169
- resolve(connection)
170
181
  end
171
182
  end
172
183
 
@@ -31,6 +31,7 @@ module HTTPX
31
31
  @status = Integer(status)
32
32
  @headers = @options.headers_class.new(headers)
33
33
  @body = @options.response_body_class.new(self, @options)
34
+ @finished = complete?
34
35
  end
35
36
 
36
37
  def merge_headers(h)
@@ -41,15 +42,24 @@ module HTTPX
41
42
  @body.write(data)
42
43
  end
43
44
 
45
+ def content_type
46
+ @content_type ||= ContentType.new(@headers["content-type"])
47
+ end
48
+
49
+ def finished?
50
+ @finished
51
+ end
52
+
53
+ def finish!
54
+ @finished = true
55
+ @headers.freeze
56
+ end
57
+
44
58
  def bodyless?
45
59
  @request.verb == :head ||
46
60
  no_data?
47
61
  end
48
62
 
49
- def content_type
50
- @content_type ||= ContentType.new(@headers["content-type"])
51
- end
52
-
53
63
  def complete?
54
64
  bodyless? || (@request.verb == :connect && @status == 200)
55
65
  end
@@ -76,17 +86,21 @@ module HTTPX
76
86
  raise err
77
87
  end
78
88
 
79
- def json(options = nil)
80
- decode("json", options)
89
+ def json(*args)
90
+ decode("json", *args)
81
91
  end
82
92
 
83
93
  def form
84
94
  decode("form")
85
95
  end
86
96
 
97
+ def xml
98
+ decode("xml")
99
+ end
100
+
87
101
  private
88
102
 
89
- def decode(format, options = nil)
103
+ def decode(format, *args)
90
104
  # TODO: check if content-type is a valid format, i.e. "application/json" for json parsing
91
105
  transcoder = Transcoder.registry(format)
92
106
 
@@ -96,13 +110,13 @@ module HTTPX
96
110
 
97
111
  raise Error, "no decoder available for \"#{format}\"" unless decoder
98
112
 
99
- decoder.call(self, options)
113
+ decoder.call(self, *args)
100
114
  rescue Registry::Error
101
115
  raise Error, "no decoder available for \"#{format}\""
102
116
  end
103
117
 
104
118
  def no_data?
105
- @status < 200 ||
119
+ @status < 200 || # informational response
106
120
  @status == 204 ||
107
121
  @status == 205 ||
108
122
  @status == 304 || begin
@@ -339,6 +353,10 @@ module HTTPX
339
353
  end
340
354
  end
341
355
 
356
+ def finished?
357
+ true
358
+ end
359
+
342
360
  def raise_for_status
343
361
  raise @error
344
362
  end
data/lib/httpx/timers.rb CHANGED
@@ -37,9 +37,12 @@ module HTTPX
37
37
  elapsed_time = Utils.elapsed_time(@next_interval_at)
38
38
 
39
39
  @intervals.delete_if { |interval| interval.elapse(elapsed_time) <= 0 }
40
+
41
+ @next_interval_at = nil if @intervals.empty?
40
42
  end
41
43
 
42
44
  def cancel
45
+ @next_interval_at = nil
43
46
  @intervals.clear
44
47
  end
45
48
 
@@ -36,7 +36,7 @@ module HTTPX::Transcoder
36
36
  module Decoder
37
37
  module_function
38
38
 
39
- def call(response, _)
39
+ def call(response, *)
40
40
  URI.decode_www_form(response.to_s).each_with_object({}) do |(field, value), params|
41
41
  HTTPX::Transcoder.normalize_query(params, field, value, PARAM_DEPTH_LIMIT)
42
42
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "forwardable"
4
- require "json"
5
4
 
6
5
  module HTTPX::Transcoder
7
6
  module JSON
@@ -19,7 +18,7 @@ module HTTPX::Transcoder
19
18
  def_delegator :@raw, :bytesize
20
19
 
21
20
  def initialize(json)
22
- @raw = ::JSON.dump(json)
21
+ @raw = JSON.json_dump(json)
23
22
  @charset = @raw.encoding.name.downcase
24
23
  end
25
24
 
@@ -37,8 +36,25 @@ module HTTPX::Transcoder
37
36
 
38
37
  raise HTTPX::Error, "invalid json mime type (#{content_type})" unless JSON_REGEX.match?(content_type)
39
38
 
40
- ::JSON.method(:parse)
39
+ method(:json_load)
41
40
  end
41
+
42
+ # rubocop:disable Style/SingleLineMethods
43
+ if defined?(MultiJson)
44
+ def json_load(*args); MultiJson.load(*args); end
45
+ def json_dump(*args); MultiJson.dump(*args); end
46
+ elsif defined?(Oj)
47
+ def json_load(response, *args); Oj.load(response.to_s, *args); end
48
+ def json_dump(*args); Oj.dump(*args); end
49
+ elsif defined?(Yajl)
50
+ def json_load(response, *args); Yajl::Parser.new(*args).parse(response.to_s); end
51
+ def json_dump(*args); Yajl::Encoder.encode(*args); end
52
+ else
53
+ require "json"
54
+ def json_load(*args); ::JSON.parse(*args); end
55
+ def json_dump(*args); ::JSON.dump(*args); end
56
+ end
57
+ # rubocop:enable Style/SingleLineMethods
42
58
  end
43
59
  register "json", JSON
44
60
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+ require "forwardable"
5
+ require "uri"
6
+
7
+ module HTTPX::Transcoder
8
+ module Xml
9
+ using HTTPX::RegexpExtensions
10
+
11
+ module_function
12
+
13
+ MIME_TYPES = %r{\b(application|text)/(.+\+)?xml\b}.freeze
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.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
+ begin
39
+ require "nokogiri"
40
+
41
+ # rubocop:disable Lint/DuplicateMethods
42
+ def decode(response)
43
+ content_type = response.content_type.mime_type
44
+
45
+ raise HTTPX::Error, "invalid form mime type (#{content_type})" unless MIME_TYPES.match?(content_type)
46
+
47
+ Nokogiri::XML.method(:parse)
48
+ end
49
+ rescue LoadError
50
+ def decode(_response)
51
+ raise HTTPX::Error, "\"nokogiri\" is required in order to decode XML"
52
+ end
53
+ end
54
+ # rubocop:enable Lint/DuplicateMethods
55
+ end
56
+ register "xml", Xml
57
+ end
@@ -90,4 +90,5 @@ end
90
90
  require "httpx/transcoder/body"
91
91
  require "httpx/transcoder/form"
92
92
  require "httpx/transcoder/json"
93
+ require "httpx/transcoder/xml"
93
94
  require "httpx/transcoder/chunker"
data/lib/httpx/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- VERSION = "0.20.5"
4
+ VERSION = "0.21.1"
5
5
  end
data/sig/buffer.rbs CHANGED
@@ -15,7 +15,7 @@ module HTTPX
15
15
  # delegated
16
16
  def <<: (string data) -> String
17
17
  def empty?: () -> bool
18
- def bytesize: () -> Integer
18
+ def bytesize: () -> (Integer | Float)
19
19
  def clear: () -> void
20
20
  def replace: (string) -> void
21
21
 
data/sig/chainable.rbs CHANGED
@@ -33,6 +33,7 @@ module HTTPX
33
33
  | (:aws_sigv4, ?options) -> Plugins::awsSigV4Session
34
34
  | (:grpc, ?options) -> Plugins::grpcSession
35
35
  | (:response_cache, ?options) -> Plugins::sessionResponseCache
36
+ | (:circuit_breaker, ?options) -> Plugins::sessionCircuitBreaker
36
37
  | (Symbol | Module, ?options) { (Class) -> void } -> Session
37
38
  | (Symbol | Module, ?options) -> Session
38
39