httpx 0.20.5 → 0.21.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +44 -26
- data/doc/release_notes/0_13_0.md +1 -1
- data/doc/release_notes/0_21_0.md +96 -0
- data/doc/release_notes/0_21_1.md +12 -0
- data/lib/httpx/connection/http1.rb +2 -1
- data/lib/httpx/connection.rb +47 -3
- data/lib/httpx/errors.rb +18 -0
- data/lib/httpx/extensions.rb +8 -4
- data/lib/httpx/io/unix.rb +1 -1
- data/lib/httpx/options.rb +7 -3
- data/lib/httpx/plugins/circuit_breaker/circuit.rb +76 -0
- data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +44 -0
- data/lib/httpx/plugins/circuit_breaker.rb +115 -0
- data/lib/httpx/plugins/cookies.rb +1 -1
- data/lib/httpx/plugins/expect.rb +1 -1
- data/lib/httpx/plugins/multipart/decoder.rb +1 -1
- data/lib/httpx/plugins/proxy.rb +7 -1
- data/lib/httpx/plugins/retries.rb +1 -1
- data/lib/httpx/plugins/webdav.rb +78 -0
- data/lib/httpx/request.rb +15 -25
- data/lib/httpx/resolver/https.rb +2 -7
- data/lib/httpx/resolver/native.rb +19 -8
- data/lib/httpx/response.rb +27 -9
- data/lib/httpx/timers.rb +3 -0
- data/lib/httpx/transcoder/form.rb +1 -1
- data/lib/httpx/transcoder/json.rb +19 -3
- data/lib/httpx/transcoder/xml.rb +57 -0
- data/lib/httpx/transcoder.rb +1 -0
- data/lib/httpx/version.rb +1 -1
- data/sig/buffer.rbs +1 -1
- data/sig/chainable.rbs +1 -0
- data/sig/connection.rbs +12 -4
- data/sig/errors.rbs +13 -0
- data/sig/io.rbs +6 -0
- data/sig/options.rbs +4 -1
- data/sig/plugins/circuit_breaker.rbs +61 -0
- data/sig/plugins/compression/brotli.rbs +1 -1
- data/sig/plugins/compression/deflate.rbs +1 -1
- data/sig/plugins/compression/gzip.rbs +3 -3
- data/sig/plugins/compression.rbs +1 -1
- data/sig/plugins/multipart.rbs +1 -1
- data/sig/plugins/proxy/socks5.rbs +3 -2
- data/sig/plugins/proxy.rbs +1 -1
- data/sig/registry.rbs +5 -4
- data/sig/request.rbs +7 -1
- data/sig/resolver/native.rbs +5 -2
- data/sig/response.rbs +3 -1
- data/sig/timers.rbs +1 -1
- data/sig/transcoder/json.rbs +4 -1
- data/sig/transcoder/xml.rbs +21 -0
- data/sig/transcoder.rbs +2 -2
- data/sig/utils.rbs +2 -2
- 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(
|
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
|
data/lib/httpx/plugins/expect.rb
CHANGED
data/lib/httpx/plugins/proxy.rb
CHANGED
@@ -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
|
-
|
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
|
@@ -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
|
|
data/lib/httpx/resolver/https.rb
CHANGED
@@ -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
|
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
|
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
|
-
}
|
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
|
-
}
|
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
|
-
|
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
|
157
|
+
return unless timeout <= 0
|
155
158
|
|
156
159
|
@timeouts[host].shift
|
157
|
-
|
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
|
|
data/lib/httpx/response.rb
CHANGED
@@ -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(
|
80
|
-
decode("json",
|
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,
|
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,
|
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 =
|
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
|
-
|
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
|
data/lib/httpx/transcoder.rb
CHANGED
data/lib/httpx/version.rb
CHANGED
data/sig/buffer.rbs
CHANGED
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
|
|