typhoeus 1.6.0 → 1.7.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/CHANGELOG.md +217 -1
- data/README.md +338 -160
- data/UPGRADE.md +29 -0
- data/lib/rack/typhoeus/middleware/params_decoder/helper.rb +67 -14
- data/lib/rack/typhoeus/middleware/params_decoder.rb +8 -19
- data/lib/rack/typhoeus.rb +2 -0
- data/lib/typhoeus/adapters/faraday.rb +89 -28
- data/lib/typhoeus/cache/dalli.rb +2 -0
- data/lib/typhoeus/cache/rails.rb +3 -1
- data/lib/typhoeus/cache/redis.rb +2 -0
- data/lib/typhoeus/config.rb +51 -7
- data/lib/typhoeus/easy_factory.rb +129 -23
- data/lib/typhoeus/errors/invalid_option.rb +23 -0
- data/lib/typhoeus/errors/no_response.rb +18 -0
- data/lib/typhoeus/errors/no_stub.rb +8 -1
- data/lib/typhoeus/errors/typhoeus_error.rb +2 -0
- data/lib/typhoeus/errors.rb +4 -0
- data/lib/typhoeus/expectation.rb +33 -1
- data/lib/typhoeus/hydra/queueable.rb +29 -4
- data/lib/typhoeus/hydra.rb +349 -21
- data/lib/typhoeus/pool.rb +80 -17
- data/lib/typhoeus/railtie.rb +4 -2
- data/lib/typhoeus/request/actions.rb +2 -0
- data/lib/typhoeus/request/callbacks.rb +11 -4
- data/lib/typhoeus/request/marshal.rb +5 -2
- data/lib/typhoeus/request/streamable.rb +2 -0
- data/lib/typhoeus/request.rb +317 -30
- data/lib/typhoeus/response/cacheable.rb +2 -0
- data/lib/typhoeus/response/header.rb +234 -14
- data/lib/typhoeus/response/informations.rb +75 -12
- data/lib/typhoeus/response/status.rb +4 -13
- data/lib/typhoeus/response.rb +35 -0
- data/lib/typhoeus/version.rb +3 -1
- data/lib/typhoeus.rb +89 -26
- data/typhoeus.gemspec +15 -3
- metadata +19 -22
- data/lib/typhoeus/hydra/addable.rb +0 -23
- data/lib/typhoeus/hydra/before.rb +0 -31
- data/lib/typhoeus/hydra/block_connection.rb +0 -35
- data/lib/typhoeus/hydra/cacheable.rb +0 -15
- data/lib/typhoeus/hydra/memoizable.rb +0 -56
- data/lib/typhoeus/hydra/runnable.rb +0 -19
- data/lib/typhoeus/hydra/stubbable.rb +0 -28
- data/lib/typhoeus/request/before.rb +0 -30
- data/lib/typhoeus/request/block_connection.rb +0 -52
- data/lib/typhoeus/request/cacheable.rb +0 -38
- data/lib/typhoeus/request/memoizable.rb +0 -38
- data/lib/typhoeus/request/operations.rb +0 -40
- data/lib/typhoeus/request/responseable.rb +0 -29
- data/lib/typhoeus/request/stubbable.rb +0 -30
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'set'
|
|
4
|
+
require 'typhoeus/errors/invalid_option'
|
|
2
5
|
|
|
3
6
|
module Typhoeus
|
|
4
7
|
|
|
@@ -35,8 +38,9 @@ module Typhoeus
|
|
|
35
38
|
|
|
36
39
|
REMOVED_OPTIONS = Set.new([:cache_key_basis, :cache_timeout, :user_agent])
|
|
37
40
|
|
|
38
|
-
SANITIZE_IGNORE = Set.new([:method, :cache_ttl, :cache])
|
|
41
|
+
SANITIZE_IGNORE = Set.new([:method, :cache_ttl, :cache, :set_read_callback])
|
|
39
42
|
SANITIZE_TIMEOUT = Set.new([:timeout_ms, :connecttimeout_ms])
|
|
43
|
+
URL_OPTIONS = Set.new([:params, :params_encoding, :escape])
|
|
40
44
|
|
|
41
45
|
# Returns the request provided.
|
|
42
46
|
#
|
|
@@ -58,6 +62,16 @@ module Typhoeus
|
|
|
58
62
|
def initialize(request, hydra = nil)
|
|
59
63
|
@request = request
|
|
60
64
|
@hydra = hydra
|
|
65
|
+
@released = false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns whether the easy was already returned to the pool.
|
|
69
|
+
#
|
|
70
|
+
# @return [ Boolean ]
|
|
71
|
+
#
|
|
72
|
+
# @api private
|
|
73
|
+
def released?
|
|
74
|
+
@released
|
|
61
75
|
end
|
|
62
76
|
|
|
63
77
|
# Return the easy in question.
|
|
@@ -77,26 +91,53 @@ module Typhoeus
|
|
|
77
91
|
#
|
|
78
92
|
# @return [ Ethon::Easy ] The easy.
|
|
79
93
|
def get
|
|
94
|
+
@released = false
|
|
80
95
|
begin
|
|
81
|
-
read_callback_body = request.options
|
|
96
|
+
read_callback_body = request.options[:set_read_callback]
|
|
97
|
+
easy = self.easy
|
|
98
|
+
url = request.base_url.to_s
|
|
99
|
+
options = sanitize(request.options)
|
|
100
|
+
options.delete(:params) if url.empty?
|
|
82
101
|
|
|
83
102
|
easy.http_request(
|
|
84
|
-
|
|
103
|
+
url,
|
|
85
104
|
request.options.fetch(:method, :get),
|
|
86
|
-
|
|
105
|
+
options
|
|
87
106
|
)
|
|
88
107
|
|
|
89
108
|
# this needs to happen after http_request because
|
|
90
109
|
# ethon will set infilesize to zero if form.empty?
|
|
91
110
|
set_read_callback(read_callback_body) if !read_callback_body.nil?
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
111
|
+
set_callback
|
|
112
|
+
rescue Exception
|
|
113
|
+
release_easy(@easy) if @easy
|
|
114
|
+
raise
|
|
95
115
|
end
|
|
96
|
-
set_callback
|
|
97
116
|
easy
|
|
98
117
|
end
|
|
99
118
|
|
|
119
|
+
# Returns the full request URL without applying unrelated request options.
|
|
120
|
+
# This keeps introspection such as Request#url and the NoStub error working
|
|
121
|
+
# even when another option would be rejected by Ethon.
|
|
122
|
+
#
|
|
123
|
+
# @return [ String ] The URL including params.
|
|
124
|
+
def url
|
|
125
|
+
@released = false
|
|
126
|
+
easy = self.easy
|
|
127
|
+
begin
|
|
128
|
+
url = request.base_url.to_s
|
|
129
|
+
options = sanitize(
|
|
130
|
+
request.options.select { |key, _| URL_OPTIONS.include?(key.to_sym) }
|
|
131
|
+
)
|
|
132
|
+
options.delete(:params) if url.empty?
|
|
133
|
+
|
|
134
|
+
easy.http_request(url, :get, options)
|
|
135
|
+
easy.url
|
|
136
|
+
ensure
|
|
137
|
+
release_easy(easy)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
100
141
|
private
|
|
101
142
|
|
|
102
143
|
def sanitize(options)
|
|
@@ -109,8 +150,21 @@ module Typhoeus
|
|
|
109
150
|
if new_option = RENAMED_OPTIONS[k.to_sym]
|
|
110
151
|
warn("Deprecated option #{k}. Please use #{new_option} instead.")
|
|
111
152
|
sanitized[new_option] = v
|
|
153
|
+
elsif new_option = CHANGED_OPTIONS[s]
|
|
154
|
+
raise Errors::InvalidOption.new(
|
|
155
|
+
s,
|
|
156
|
+
"\nPlease try #{new_option} instead of #{s}."
|
|
157
|
+
)
|
|
158
|
+
elsif REMOVED_OPTIONS.include?(s)
|
|
159
|
+
raise Errors::InvalidOption.new(s, "\nThe option #{s} was removed.")
|
|
112
160
|
# sanitize timeouts
|
|
113
161
|
elsif SANITIZE_TIMEOUT.include?(s)
|
|
162
|
+
# An unset value means "no timeout" and never reaches Ethon.
|
|
163
|
+
next if v.nil?
|
|
164
|
+
|
|
165
|
+
unless v.is_a?(Numeric)
|
|
166
|
+
raise ArgumentError, "Value '#{v}' for option '#{k}' must be a number."
|
|
167
|
+
end
|
|
114
168
|
if !v.integer?
|
|
115
169
|
warn("Value '#{v}' for option '#{k}' must be integer.")
|
|
116
170
|
end
|
|
@@ -128,15 +182,52 @@ module Typhoeus
|
|
|
128
182
|
|
|
129
183
|
def sanitize_timeout!(options, timeout)
|
|
130
184
|
timeout_ms = :"#{timeout}_ms"
|
|
131
|
-
|
|
185
|
+
value = options[timeout]
|
|
186
|
+
return options if value.nil?
|
|
187
|
+
|
|
188
|
+
unless value.is_a?(Numeric)
|
|
189
|
+
raise ArgumentError, "Value '#{value}' for option '#{timeout}' must be a number."
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
if value.round != value
|
|
132
193
|
if !options[timeout_ms]
|
|
133
|
-
options[timeout_ms] = (
|
|
194
|
+
options[timeout_ms] = (value*1000).ceil
|
|
134
195
|
end
|
|
135
|
-
options[timeout] =
|
|
196
|
+
options[timeout] = value.ceil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# CURLOPT_TIMEOUT and CURLOPT_TIMEOUT_MS set the same value, so libcurl
|
|
200
|
+
# uses the one that is applied last. Keep the millisecond option last:
|
|
201
|
+
# an explicit timeout_ms must also win over a timeout added from Config.
|
|
202
|
+
if options.key?(timeout_ms)
|
|
203
|
+
options[timeout_ms] = options.delete(timeout_ms)
|
|
136
204
|
end
|
|
137
205
|
options
|
|
138
206
|
end
|
|
139
207
|
|
|
208
|
+
public
|
|
209
|
+
|
|
210
|
+
# Releases the easy into the pool once. Easies that used a custom
|
|
211
|
+
# resolve list are cleaned up instead because libcurl keeps the
|
|
212
|
+
# resolved addresses in its DNS cache across resets.
|
|
213
|
+
#
|
|
214
|
+
# @param [ Ethon::Easy ] easy The easy to release.
|
|
215
|
+
#
|
|
216
|
+
# @return [ void ]
|
|
217
|
+
#
|
|
218
|
+
# @api private
|
|
219
|
+
def release_easy(easy)
|
|
220
|
+
return if released?
|
|
221
|
+
|
|
222
|
+
begin
|
|
223
|
+
Typhoeus::Pool.release(easy, :cleanup => !request.options[:resolve].nil?)
|
|
224
|
+
ensure
|
|
225
|
+
@released = true
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
private
|
|
230
|
+
|
|
140
231
|
# Sets on_complete callback on easy in order to be able to
|
|
141
232
|
# track progress.
|
|
142
233
|
#
|
|
@@ -161,14 +252,16 @@ module Typhoeus
|
|
|
161
252
|
request.execute_headers_callbacks(Response.new(Ethon::Easy::Mirror.from_easy(easy).options))
|
|
162
253
|
end
|
|
163
254
|
end
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
end
|
|
255
|
+
# Ethon calls progress callbacks with the four transfer counters.
|
|
256
|
+
(Typhoeus.on_progress + request.on_progress).each do |callback|
|
|
257
|
+
easy.on_progress(&callback)
|
|
168
258
|
end
|
|
169
259
|
easy.on_complete do |easy|
|
|
170
|
-
|
|
171
|
-
|
|
260
|
+
begin
|
|
261
|
+
request.finish(Response.new(easy.mirror.options))
|
|
262
|
+
ensure
|
|
263
|
+
release_easy(easy)
|
|
264
|
+
end
|
|
172
265
|
if hydra && !hydra.queued_requests.empty?
|
|
173
266
|
hydra.dequeue_many
|
|
174
267
|
end
|
|
@@ -182,6 +275,11 @@ module Typhoeus
|
|
|
182
275
|
#
|
|
183
276
|
# @return [ Ethon::Easy ] The easy.
|
|
184
277
|
def set_read_callback(body)
|
|
278
|
+
# Uploading a file means sending it from the beginning. An IO that was
|
|
279
|
+
# already read from would otherwise be shorter than the advertised
|
|
280
|
+
# infilesize and libcurl would fail with a read error.
|
|
281
|
+
rewind_body(body)
|
|
282
|
+
|
|
185
283
|
easy.infilesize_large =
|
|
186
284
|
if body.respond_to?(:bytesize)
|
|
187
285
|
body.bytesize
|
|
@@ -195,12 +293,20 @@ module Typhoeus
|
|
|
195
293
|
easy
|
|
196
294
|
end
|
|
197
295
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
296
|
+
# Rewind a seekable body so the read callback starts at its first byte.
|
|
297
|
+
# Pipes and other non-seekable bodies do not respond to rewind.
|
|
298
|
+
#
|
|
299
|
+
# @param [ Object ] body The body to rewind.
|
|
300
|
+
#
|
|
301
|
+
# @return [ void ]
|
|
302
|
+
def rewind_body(body)
|
|
303
|
+
return unless body.respond_to?(:rewind)
|
|
304
|
+
|
|
305
|
+
body.rewind
|
|
306
|
+
rescue IOError, SystemCallError
|
|
307
|
+
# A body that cannot be rewound is still usable from its current
|
|
308
|
+
# position. Do not turn that into a request setup error.
|
|
309
|
+
nil
|
|
204
310
|
end
|
|
205
311
|
end
|
|
206
312
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typhoeus
|
|
4
|
+
module Errors
|
|
5
|
+
|
|
6
|
+
# Raised for options Typhoeus knows about but Ethon does not accept.
|
|
7
|
+
#
|
|
8
|
+
# It stays compatible with Ethon's InvalidOption while carrying the
|
|
9
|
+
# replacement or removal hint that Typhoeus adds.
|
|
10
|
+
class InvalidOption < Ethon::Errors::InvalidOption
|
|
11
|
+
def initialize(option, help = nil)
|
|
12
|
+
@help = help
|
|
13
|
+
super(option)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Exception#message calls #to_s, so the hint is included everywhere the
|
|
17
|
+
# error is rendered and not only when #message is called explicitly.
|
|
18
|
+
def to_s
|
|
19
|
+
"#{super}#{@help}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'typhoeus/errors/typhoeus_error'
|
|
4
|
+
|
|
5
|
+
module Typhoeus
|
|
6
|
+
module Errors
|
|
7
|
+
|
|
8
|
+
# Raised when a request matches a stub that does not provide a usable
|
|
9
|
+
# response.
|
|
10
|
+
class NoResponse < TyphoeusError
|
|
11
|
+
def initialize(base_url, reason = nil)
|
|
12
|
+
message = "No response defined for stub: #{base_url.inspect}"
|
|
13
|
+
message = "#{message} (#{reason})" if reason
|
|
14
|
+
super(message)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Typhoeus
|
|
2
4
|
module Errors
|
|
3
5
|
|
|
@@ -5,7 +7,12 @@ module Typhoeus
|
|
|
5
7
|
# and making a real request.
|
|
6
8
|
class NoStub < TyphoeusError
|
|
7
9
|
def initialize(request)
|
|
8
|
-
|
|
10
|
+
url = begin
|
|
11
|
+
request.url
|
|
12
|
+
rescue StandardError
|
|
13
|
+
request.base_url
|
|
14
|
+
end
|
|
15
|
+
super("The connection is blocked and no stub defined: #{url}")
|
|
9
16
|
end
|
|
10
17
|
end
|
|
11
18
|
end
|
data/lib/typhoeus/errors.rb
CHANGED
data/lib/typhoeus/expectation.rb
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'typhoeus/errors/no_response'
|
|
4
|
+
|
|
1
5
|
module Typhoeus
|
|
2
6
|
|
|
3
7
|
# This class represents an expectation. It is part
|
|
@@ -49,6 +53,16 @@ module Typhoeus
|
|
|
49
53
|
# @api private
|
|
50
54
|
attr_reader :from
|
|
51
55
|
|
|
56
|
+
# Whenever an expectation is triggered this counter increases which can be used in testing to ensure that a defined stub is actually used.
|
|
57
|
+
#
|
|
58
|
+
# @example Stub a request and check if it was used
|
|
59
|
+
# expectation = Typhoeus.stub("www.example.com").and_return(Typhoeus::Response.new)
|
|
60
|
+
#
|
|
61
|
+
# Typhoeus.get("www.example.com")
|
|
62
|
+
# expectation.response_counter
|
|
63
|
+
# #=> 1
|
|
64
|
+
attr_reader :response_counter
|
|
65
|
+
|
|
52
66
|
class << self
|
|
53
67
|
|
|
54
68
|
# Returns all expectations.
|
|
@@ -179,11 +193,29 @@ module Typhoeus
|
|
|
179
193
|
# @api private
|
|
180
194
|
def response(request)
|
|
181
195
|
response = responses.fetch(@response_counter, responses.last)
|
|
196
|
+
raise Errors::NoResponse.new(base_url) if response.nil?
|
|
197
|
+
|
|
182
198
|
if response.respond_to?(:call)
|
|
183
199
|
response = response.call(request)
|
|
200
|
+
if response.nil?
|
|
201
|
+
raise Errors::NoResponse.new(
|
|
202
|
+
base_url,
|
|
203
|
+
"the response block returned nil"
|
|
204
|
+
)
|
|
205
|
+
end
|
|
184
206
|
end
|
|
207
|
+
|
|
208
|
+
unless response.is_a?(Response)
|
|
209
|
+
raise Errors::NoResponse.new(
|
|
210
|
+
base_url,
|
|
211
|
+
"expected a Typhoeus::Response, got #{response.class}"
|
|
212
|
+
)
|
|
213
|
+
end
|
|
214
|
+
|
|
185
215
|
@response_counter += 1
|
|
186
|
-
response
|
|
216
|
+
# A stub block can return the response of a real request, which must keep
|
|
217
|
+
# its real mock state and must not be finished a second time.
|
|
218
|
+
response.mock = @from || true unless response.request
|
|
187
219
|
response
|
|
188
220
|
end
|
|
189
221
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Typhoeus
|
|
2
4
|
class Hydra
|
|
3
5
|
|
|
@@ -67,17 +69,40 @@ module Typhoeus
|
|
|
67
69
|
# adds them to the hydra until max_concurrency
|
|
68
70
|
# is reached.
|
|
69
71
|
#
|
|
72
|
+
# Short-circuited requests call this method while it is already
|
|
73
|
+
# draining the queue. The guard keeps that from recursing.
|
|
74
|
+
#
|
|
70
75
|
# @example Dequeue requests.
|
|
71
76
|
# hydra.dequeue_many
|
|
72
77
|
#
|
|
73
78
|
# @since 0.6.8
|
|
74
79
|
def dequeue_many
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
return if @dequeuing_many
|
|
81
|
+
|
|
82
|
+
begin
|
|
83
|
+
@dequeuing_many = true
|
|
84
|
+
until queued_requests.empty?
|
|
85
|
+
break if easy_handle_count >= max_concurrency
|
|
86
|
+
|
|
87
|
+
add(queued_requests.shift)
|
|
88
|
+
end
|
|
89
|
+
ensure
|
|
90
|
+
@dequeuing_many = false
|
|
79
91
|
end
|
|
80
92
|
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# Count the attached easies without creating the native multi handle.
|
|
97
|
+
# Requests that a before callback, a stub, or the cache answers never
|
|
98
|
+
# need a multi, and Ethon 0.19 rejects a handle created before a fork.
|
|
99
|
+
#
|
|
100
|
+
# @return [ Integer ] The number of running easy handles.
|
|
101
|
+
def easy_handle_count
|
|
102
|
+
return 0 unless defined?(@multi) && @multi
|
|
103
|
+
|
|
104
|
+
multi.easy_handles.count
|
|
105
|
+
end
|
|
81
106
|
end
|
|
82
107
|
end
|
|
83
108
|
end
|