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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_13_0.md +1 -1
  3. data/doc/release_notes/0_20_4.md +17 -0
  4. data/doc/release_notes/0_20_5.md +3 -0
  5. data/doc/release_notes/0_21_0.md +94 -0
  6. data/lib/httpx/connection/http1.rb +2 -1
  7. data/lib/httpx/connection.rb +41 -2
  8. data/lib/httpx/errors.rb +18 -0
  9. data/lib/httpx/extensions.rb +36 -13
  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/compression.rb +1 -1
  16. data/lib/httpx/plugins/cookies.rb +1 -1
  17. data/lib/httpx/plugins/expect.rb +1 -1
  18. data/lib/httpx/plugins/multipart/decoder.rb +1 -1
  19. data/lib/httpx/plugins/proxy.rb +7 -1
  20. data/lib/httpx/plugins/response_cache/store.rb +39 -19
  21. data/lib/httpx/plugins/response_cache.rb +98 -8
  22. data/lib/httpx/plugins/retries.rb +1 -1
  23. data/lib/httpx/plugins/webdav.rb +78 -0
  24. data/lib/httpx/pool.rb +1 -1
  25. data/lib/httpx/request.rb +15 -25
  26. data/lib/httpx/resolver/https.rb +2 -7
  27. data/lib/httpx/resolver/multi.rb +1 -1
  28. data/lib/httpx/resolver/native.rb +2 -1
  29. data/lib/httpx/resolver/resolver.rb +12 -2
  30. data/lib/httpx/response.rb +30 -9
  31. data/lib/httpx/timers.rb +3 -0
  32. data/lib/httpx/transcoder/body.rb +1 -1
  33. data/lib/httpx/transcoder/form.rb +1 -1
  34. data/lib/httpx/transcoder/json.rb +19 -3
  35. data/lib/httpx/transcoder/xml.rb +57 -0
  36. data/lib/httpx/transcoder.rb +1 -0
  37. data/lib/httpx/version.rb +1 -1
  38. data/sig/buffer.rbs +1 -1
  39. data/sig/chainable.rbs +1 -0
  40. data/sig/connection.rbs +12 -4
  41. data/sig/errors.rbs +13 -0
  42. data/sig/io.rbs +6 -0
  43. data/sig/options.rbs +4 -1
  44. data/sig/plugins/circuit_breaker.rbs +61 -0
  45. data/sig/plugins/compression/brotli.rbs +1 -1
  46. data/sig/plugins/compression/deflate.rbs +1 -1
  47. data/sig/plugins/compression/gzip.rbs +3 -3
  48. data/sig/plugins/compression.rbs +1 -1
  49. data/sig/plugins/multipart.rbs +1 -1
  50. data/sig/plugins/proxy/socks5.rbs +3 -2
  51. data/sig/plugins/proxy.rbs +1 -1
  52. data/sig/plugins/response_cache.rbs +24 -4
  53. data/sig/registry.rbs +5 -4
  54. data/sig/request.rbs +7 -1
  55. data/sig/resolver/native.rbs +5 -2
  56. data/sig/response.rbs +6 -1
  57. data/sig/timers.rbs +1 -1
  58. data/sig/transcoder/json.rbs +4 -1
  59. data/sig/transcoder/xml.rbs +21 -0
  60. data/sig/transcoder.rbs +2 -2
  61. data/sig/utils.rbs +2 -2
  62. metadata +17 -3
@@ -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
@@ -13,35 +13,38 @@ module HTTPX::Plugins
13
13
  @store = {}
14
14
  end
15
15
 
16
- def lookup(uri)
17
- @store[uri]
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?(uri)
21
- @store.key?(uri)
28
+ def cached?(request)
29
+ lookup(request)
22
30
  end
23
31
 
24
- def cache(uri, response)
25
- @store[uri] = response
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 = @store[request.uri]
43
+ cached_response = lookup(request)
30
44
 
31
45
  return unless cached_response
32
46
 
33
- original_request = cached_response.instance_variable_get(:@request)
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
- # partial responses shall not be cached, only full ones.
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.uri)
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
- if response && ResponseCache.cached_response?(response)
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.uri)
85
+ cached_response = @options.response_cache_store.lookup(request)
68
86
 
69
87
  response.copy_from_cached(cached_response)
70
- end
71
88
 
72
- @options.response_cache_store.cache(request.uri, response) if response && ResponseCache.cacheable_response?(response)
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
@@ -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/pool.rb CHANGED
@@ -7,7 +7,7 @@ require "httpx/resolver"
7
7
 
8
8
  module HTTPX
9
9
  class Pool
10
- using ArrayExtensions
10
+ using ArrayExtensions::FilterMap
11
11
  extend Forwardable
12
12
 
13
13
  def_delegator :@timers, :after
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)
@@ -6,7 +6,7 @@ require "resolv"
6
6
  module HTTPX
7
7
  class Resolver::Multi
8
8
  include Callbacks
9
- using ArrayExtensions
9
+ using ArrayExtensions::FilterMap
10
10
 
11
11
  attr_reader :resolvers
12
12
 
@@ -77,7 +77,8 @@ module HTTPX
77
77
  nil
78
78
  rescue Errno::EHOSTUNREACH => e
79
79
  @ns_index += 1
80
- if @ns_index < @nameserver.size
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
- connection.addresses = addresses
60
- emit(:resolve, connection)
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
@@ -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
@@ -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
 
@@ -9,7 +9,7 @@ module HTTPX::Transcoder
9
9
  module_function
10
10
 
11
11
  class Encoder
12
- using HTTPX::ArrayExtensions
12
+ using HTTPX::ArrayExtensions::Sum
13
13
  extend Forwardable
14
14
 
15
15
  def_delegator :@raw, :to_s
@@ -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