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.
- 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
|
|