httpx 0.20.3 → 0.21.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/doc/release_notes/0_13_0.md +1 -1
- data/doc/release_notes/0_20_4.md +17 -0
- data/doc/release_notes/0_20_5.md +3 -0
- data/doc/release_notes/0_21_0.md +94 -0
- data/lib/httpx/connection/http1.rb +2 -1
- data/lib/httpx/connection.rb +41 -2
- data/lib/httpx/errors.rb +18 -0
- data/lib/httpx/extensions.rb +36 -13
- 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/compression.rb +1 -1
- 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/response_cache/store.rb +39 -19
- data/lib/httpx/plugins/response_cache.rb +98 -8
- data/lib/httpx/plugins/retries.rb +1 -1
- data/lib/httpx/plugins/webdav.rb +78 -0
- data/lib/httpx/pool.rb +1 -1
- data/lib/httpx/request.rb +15 -25
- data/lib/httpx/resolver/https.rb +2 -7
- data/lib/httpx/resolver/multi.rb +1 -1
- data/lib/httpx/resolver/native.rb +2 -1
- data/lib/httpx/resolver/resolver.rb +12 -2
- data/lib/httpx/response.rb +30 -9
- data/lib/httpx/timers.rb +3 -0
- data/lib/httpx/transcoder/body.rb +1 -1
- 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/plugins/response_cache.rbs +24 -4
- data/sig/registry.rbs +5 -4
- data/sig/request.rbs +7 -1
- data/sig/resolver/native.rbs +5 -2
- data/sig/response.rbs +6 -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 +17 -3
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
|
@@ -13,35 +13,38 @@ module HTTPX::Plugins
|
|
13
13
|
@store = {}
|
14
14
|
end
|
15
15
|
|
16
|
-
def lookup(
|
17
|
-
@store[
|
16
|
+
def lookup(request)
|
17
|
+
responses = @store[request.response_cache_key]
|
18
|
+
|
19
|
+
return unless responses
|
20
|
+
|
21
|
+
response = responses.find(&method(:match_by_vary?).curry(2)[request])
|
22
|
+
|
23
|
+
return unless response && response.fresh?
|
24
|
+
|
25
|
+
response
|
18
26
|
end
|
19
27
|
|
20
|
-
def cached?(
|
21
|
-
|
28
|
+
def cached?(request)
|
29
|
+
lookup(request)
|
22
30
|
end
|
23
31
|
|
24
|
-
def cache(
|
25
|
-
|
32
|
+
def cache(request, response)
|
33
|
+
return unless ResponseCache.cacheable_request?(request) && ResponseCache.cacheable_response?(response)
|
34
|
+
|
35
|
+
responses = (@store[request.response_cache_key] ||= [])
|
36
|
+
|
37
|
+
responses.reject!(&method(:match_by_vary?).curry(2)[request])
|
38
|
+
|
39
|
+
responses << response
|
26
40
|
end
|
27
41
|
|
28
42
|
def prepare(request)
|
29
|
-
cached_response =
|
43
|
+
cached_response = lookup(request)
|
30
44
|
|
31
45
|
return unless cached_response
|
32
46
|
|
33
|
-
|
34
|
-
|
35
|
-
if (vary = cached_response.headers["vary"])
|
36
|
-
if vary == "*"
|
37
|
-
return unless request.headers.same_headers?(original_request.headers)
|
38
|
-
else
|
39
|
-
return unless vary.split(/ *, */).all? do |cache_field|
|
40
|
-
cache_field.downcase!
|
41
|
-
!original_request.headers.key?(cache_field) || request.headers[cache_field] == original_request.headers[cache_field]
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
47
|
+
return unless match_by_vary?(request, cached_response)
|
45
48
|
|
46
49
|
if !request.headers.key?("if-modified-since") && (last_modified = cached_response.headers["last-modified"])
|
47
50
|
request.headers.add("if-modified-since", last_modified)
|
@@ -51,6 +54,23 @@ module HTTPX::Plugins
|
|
51
54
|
request.headers.add("if-none-match", etag)
|
52
55
|
end
|
53
56
|
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def match_by_vary?(request, response)
|
61
|
+
vary = response.vary
|
62
|
+
|
63
|
+
return true unless vary
|
64
|
+
|
65
|
+
original_request = response.instance_variable_get(:@request)
|
66
|
+
|
67
|
+
return request.headers.same_headers?(original_request.headers) if vary == %w[*]
|
68
|
+
|
69
|
+
vary.all? do |cache_field|
|
70
|
+
cache_field.downcase!
|
71
|
+
!original_request.headers.key?(cache_field) || request.headers[cache_field] == original_request.headers[cache_field]
|
72
|
+
end
|
73
|
+
end
|
54
74
|
end
|
55
75
|
end
|
56
76
|
end
|
@@ -9,7 +9,9 @@ module HTTPX
|
|
9
9
|
#
|
10
10
|
module ResponseCache
|
11
11
|
CACHEABLE_VERBS = %i[get head].freeze
|
12
|
+
CACHEABLE_STATUS_CODES = [200, 203, 206, 300, 301, 410].freeze
|
12
13
|
private_constant :CACHEABLE_VERBS
|
14
|
+
private_constant :CACHEABLE_STATUS_CODES
|
13
15
|
|
14
16
|
class << self
|
15
17
|
def load_dependencies(*)
|
@@ -17,14 +19,28 @@ module HTTPX
|
|
17
19
|
end
|
18
20
|
|
19
21
|
def cacheable_request?(request)
|
20
|
-
CACHEABLE_VERBS.include?(request.verb)
|
22
|
+
CACHEABLE_VERBS.include?(request.verb) &&
|
23
|
+
(
|
24
|
+
!request.headers.key?("cache-control") || !request.headers.get("cache-control").include?("no-store")
|
25
|
+
)
|
21
26
|
end
|
22
27
|
|
23
28
|
def cacheable_response?(response)
|
24
29
|
response.is_a?(Response) &&
|
25
|
-
|
30
|
+
(
|
31
|
+
response.cache_control.nil? ||
|
32
|
+
# TODO: !response.cache_control.include?("private") && is shared cache
|
33
|
+
!response.cache_control.include?("no-store")
|
34
|
+
) &&
|
35
|
+
CACHEABLE_STATUS_CODES.include?(response.status) &&
|
36
|
+
# RFC 2616 13.4 - A response received with a status code of 200, 203, 206, 300, 301 or
|
37
|
+
# 410 MAY be stored by a cache and used in reply to a subsequent
|
38
|
+
# request, subject to the expiration mechanism, unless a cache-control
|
39
|
+
# directive prohibits caching. However, a cache that does not support
|
40
|
+
# the Range and Content-Range headers MUST NOT cache 206 (Partial
|
41
|
+
# Content) responses.
|
26
42
|
response.status != 206 && (
|
27
|
-
response.headers.key?("etag") || response.headers.key?("last-modified-at")
|
43
|
+
response.headers.key?("etag") || response.headers.key?("last-modified-at") || response.fresh?
|
28
44
|
)
|
29
45
|
end
|
30
46
|
|
@@ -52,7 +68,7 @@ module HTTPX
|
|
52
68
|
|
53
69
|
def build_request(*)
|
54
70
|
request = super
|
55
|
-
return request unless ResponseCache.cacheable_request?(request) && @options.response_cache_store.cached?(request
|
71
|
+
return request unless ResponseCache.cacheable_request?(request) && @options.response_cache_store.cached?(request)
|
56
72
|
|
57
73
|
@options.response_cache_store.prepare(request)
|
58
74
|
|
@@ -62,25 +78,99 @@ module HTTPX
|
|
62
78
|
def fetch_response(request, *)
|
63
79
|
response = super
|
64
80
|
|
65
|
-
|
81
|
+
return unless response
|
82
|
+
|
83
|
+
if ResponseCache.cached_response?(response)
|
66
84
|
log { "returning cached response for #{request.uri}" }
|
67
|
-
cached_response = @options.response_cache_store.lookup(request
|
85
|
+
cached_response = @options.response_cache_store.lookup(request)
|
68
86
|
|
69
87
|
response.copy_from_cached(cached_response)
|
70
|
-
end
|
71
88
|
|
72
|
-
|
89
|
+
else
|
90
|
+
@options.response_cache_store.cache(request, response)
|
91
|
+
end
|
73
92
|
|
74
93
|
response
|
75
94
|
end
|
76
95
|
end
|
77
96
|
|
97
|
+
module RequestMethods
|
98
|
+
def response_cache_key
|
99
|
+
@response_cache_key ||= Digest::SHA1.hexdigest("httpx-response-cache-#{@verb}#{@uri}")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
78
103
|
module ResponseMethods
|
79
104
|
def copy_from_cached(other)
|
80
105
|
@body = other.body
|
81
106
|
|
82
107
|
@body.__send__(:rewind)
|
83
108
|
end
|
109
|
+
|
110
|
+
# A response is fresh if its age has not yet exceeded its freshness lifetime.
|
111
|
+
def fresh?
|
112
|
+
if cache_control
|
113
|
+
return false if cache_control.include?("no-cache")
|
114
|
+
|
115
|
+
# check age: max-age
|
116
|
+
max_age = cache_control.find { |directive| directive.start_with?("s-maxage") }
|
117
|
+
|
118
|
+
max_age ||= cache_control.find { |directive| directive.start_with?("max-age") }
|
119
|
+
|
120
|
+
max_age = max_age[/age=(\d+)/, 1] if max_age
|
121
|
+
|
122
|
+
max_age = max_age.to_i if max_age
|
123
|
+
|
124
|
+
return max_age > age if max_age
|
125
|
+
end
|
126
|
+
|
127
|
+
# check age: expires
|
128
|
+
if @headers.key?("expires")
|
129
|
+
begin
|
130
|
+
expires = Time.httpdate(@headers["expires"])
|
131
|
+
rescue ArgumentError
|
132
|
+
return true
|
133
|
+
end
|
134
|
+
|
135
|
+
return (expires - Time.now).to_i.positive?
|
136
|
+
end
|
137
|
+
|
138
|
+
true
|
139
|
+
end
|
140
|
+
|
141
|
+
def cache_control
|
142
|
+
return @cache_control if defined?(@cache_control)
|
143
|
+
|
144
|
+
@cache_control = begin
|
145
|
+
return unless @headers.key?("cache-control")
|
146
|
+
|
147
|
+
@headers["cache-control"].split(/ *, */)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def vary
|
152
|
+
return @vary if defined?(@vary)
|
153
|
+
|
154
|
+
@vary = begin
|
155
|
+
return unless @headers.key?("vary")
|
156
|
+
|
157
|
+
@headers["vary"].split(/ *, */)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def age
|
164
|
+
return @headers["age"].to_i if @headers.key?("age")
|
165
|
+
|
166
|
+
(Time.now - date).to_i
|
167
|
+
end
|
168
|
+
|
169
|
+
def date
|
170
|
+
@date ||= Time.httpdate(@headers["date"])
|
171
|
+
rescue NoMethodError, ArgumentError
|
172
|
+
Time.now.httpdate
|
173
|
+
end
|
84
174
|
end
|
85
175
|
end
|
86
176
|
register_plugin :response_cache, ResponseCache
|
@@ -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/pool.rb
CHANGED
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)
|
data/lib/httpx/resolver/multi.rb
CHANGED
@@ -77,7 +77,8 @@ module HTTPX
|
|
77
77
|
nil
|
78
78
|
rescue Errno::EHOSTUNREACH => e
|
79
79
|
@ns_index += 1
|
80
|
-
|
80
|
+
nameserver = @nameserver
|
81
|
+
if nameserver && @ns_index < nameserver.size
|
81
82
|
log { "resolver: failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})" }
|
82
83
|
transition(:idle)
|
83
84
|
else
|
@@ -8,6 +8,8 @@ module HTTPX
|
|
8
8
|
include Callbacks
|
9
9
|
include Loggable
|
10
10
|
|
11
|
+
using ArrayExtensions::Intersect
|
12
|
+
|
11
13
|
RECORD_TYPES = {
|
12
14
|
Socket::AF_INET6 => Resolv::DNS::Resource::IN::AAAA,
|
13
15
|
Socket::AF_INET => Resolv::DNS::Resource::IN::A,
|
@@ -48,6 +50,10 @@ module HTTPX
|
|
48
50
|
addresses.map! do |address|
|
49
51
|
address.is_a?(IPAddr) ? address : IPAddr.new(address.to_s)
|
50
52
|
end
|
53
|
+
|
54
|
+
# double emission check
|
55
|
+
return if connection.addresses && !addresses.intersect?(connection.addresses)
|
56
|
+
|
51
57
|
log { "resolver: answer #{connection.origin.host}: #{addresses.inspect}" }
|
52
58
|
if @pool && # if triggered by early resolve, pool may not be here yet
|
53
59
|
!connection.io &&
|
@@ -56,8 +62,12 @@ module HTTPX
|
|
56
62
|
addresses.first.to_s != connection.origin.host.to_s
|
57
63
|
log { "resolver: A response, applying resolution delay..." }
|
58
64
|
@pool.after(0.05) do
|
59
|
-
|
60
|
-
|
65
|
+
# double emission check
|
66
|
+
unless connection.addresses && addresses.intersect?(connection.addresses)
|
67
|
+
|
68
|
+
connection.addresses = addresses
|
69
|
+
emit(:resolve, connection)
|
70
|
+
end
|
61
71
|
end
|
62
72
|
else
|
63
73
|
connection.addresses = addresses
|
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
|
@@ -310,9 +324,12 @@ module HTTPX
|
|
310
324
|
|
311
325
|
class ErrorResponse
|
312
326
|
include Loggable
|
327
|
+
extend Forwardable
|
313
328
|
|
314
329
|
attr_reader :request, :error
|
315
330
|
|
331
|
+
def_delegator :@request, :uri
|
332
|
+
|
316
333
|
def initialize(request, error, options)
|
317
334
|
@request = request
|
318
335
|
@error = error
|
@@ -336,6 +353,10 @@ module HTTPX
|
|
336
353
|
end
|
337
354
|
end
|
338
355
|
|
356
|
+
def finished?
|
357
|
+
true
|
358
|
+
end
|
359
|
+
|
339
360
|
def raise_for_status
|
340
361
|
raise @error
|
341
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
|