httpx 0.20.3 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
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