httpx 0.20.3 → 0.21.0
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/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
|