httpx 0.20.5 → 0.21.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 (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