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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +217 -1
  3. data/README.md +338 -160
  4. data/UPGRADE.md +29 -0
  5. data/lib/rack/typhoeus/middleware/params_decoder/helper.rb +67 -14
  6. data/lib/rack/typhoeus/middleware/params_decoder.rb +8 -19
  7. data/lib/rack/typhoeus.rb +2 -0
  8. data/lib/typhoeus/adapters/faraday.rb +89 -28
  9. data/lib/typhoeus/cache/dalli.rb +2 -0
  10. data/lib/typhoeus/cache/rails.rb +3 -1
  11. data/lib/typhoeus/cache/redis.rb +2 -0
  12. data/lib/typhoeus/config.rb +51 -7
  13. data/lib/typhoeus/easy_factory.rb +129 -23
  14. data/lib/typhoeus/errors/invalid_option.rb +23 -0
  15. data/lib/typhoeus/errors/no_response.rb +18 -0
  16. data/lib/typhoeus/errors/no_stub.rb +8 -1
  17. data/lib/typhoeus/errors/typhoeus_error.rb +2 -0
  18. data/lib/typhoeus/errors.rb +4 -0
  19. data/lib/typhoeus/expectation.rb +33 -1
  20. data/lib/typhoeus/hydra/queueable.rb +29 -4
  21. data/lib/typhoeus/hydra.rb +349 -21
  22. data/lib/typhoeus/pool.rb +80 -17
  23. data/lib/typhoeus/railtie.rb +4 -2
  24. data/lib/typhoeus/request/actions.rb +2 -0
  25. data/lib/typhoeus/request/callbacks.rb +11 -4
  26. data/lib/typhoeus/request/marshal.rb +5 -2
  27. data/lib/typhoeus/request/streamable.rb +2 -0
  28. data/lib/typhoeus/request.rb +317 -30
  29. data/lib/typhoeus/response/cacheable.rb +2 -0
  30. data/lib/typhoeus/response/header.rb +234 -14
  31. data/lib/typhoeus/response/informations.rb +75 -12
  32. data/lib/typhoeus/response/status.rb +4 -13
  33. data/lib/typhoeus/response.rb +35 -0
  34. data/lib/typhoeus/version.rb +3 -1
  35. data/lib/typhoeus.rb +89 -26
  36. data/typhoeus.gemspec +15 -3
  37. metadata +19 -22
  38. data/lib/typhoeus/hydra/addable.rb +0 -23
  39. data/lib/typhoeus/hydra/before.rb +0 -31
  40. data/lib/typhoeus/hydra/block_connection.rb +0 -35
  41. data/lib/typhoeus/hydra/cacheable.rb +0 -15
  42. data/lib/typhoeus/hydra/memoizable.rb +0 -56
  43. data/lib/typhoeus/hydra/runnable.rb +0 -19
  44. data/lib/typhoeus/hydra/stubbable.rb +0 -28
  45. data/lib/typhoeus/request/before.rb +0 -30
  46. data/lib/typhoeus/request/block_connection.rb +0 -52
  47. data/lib/typhoeus/request/cacheable.rb +0 -38
  48. data/lib/typhoeus/request/memoizable.rb +0 -38
  49. data/lib/typhoeus/request/operations.rb +0 -40
  50. data/lib/typhoeus/request/responseable.rb +0 -29
  51. data/lib/typhoeus/request/stubbable.rb +0 -30
@@ -1,11 +1,6 @@
1
- require 'typhoeus/hydra/addable'
2
- require 'typhoeus/hydra/before'
3
- require 'typhoeus/hydra/cacheable'
4
- require 'typhoeus/hydra/block_connection'
5
- require 'typhoeus/hydra/memoizable'
1
+ # frozen_string_literal: true
2
+
6
3
  require 'typhoeus/hydra/queueable'
7
- require 'typhoeus/hydra/runnable'
8
- require 'typhoeus/hydra/stubbable'
9
4
 
10
5
  module Typhoeus
11
6
 
@@ -39,21 +34,13 @@ module Typhoeus
39
34
  # @note Callbacks are going to delay the request
40
35
  # execution.
41
36
  class Hydra
42
- include Hydra::Addable
43
- include Hydra::Runnable
44
- include Hydra::Memoizable
45
- include Hydra::Cacheable
46
- include Hydra::BlockConnection
47
- include Hydra::Stubbable
48
- include Hydra::Before
49
37
  include Hydra::Queueable
50
38
 
51
39
  # @example Set max_concurrency.
52
40
  # Typhoeus::Hydra.new(max_concurrency: 20)
53
- attr_accessor :max_concurrency
54
-
55
- # @api private
56
- attr_reader :multi
41
+ #
42
+ # @return [ Integer ] The maximum number of concurrent requests.
43
+ attr_reader :max_concurrency
57
44
 
58
45
  class << self
59
46
 
@@ -87,9 +74,350 @@ module Typhoeus
87
74
  # @see http://rubydoc.info/github/typhoeus/ethon/Ethon/Multi#initialize-instance_method
88
75
  # Ethon::Multi#initialize
89
76
  def initialize(options = {})
90
- @options = options
91
- @max_concurrency = Integer(@options.fetch(:max_concurrency, 200))
92
- @multi = Ethon::Multi.new(options.reject{|k,_| k==:max_concurrency})
77
+ @options = options.dup
78
+ @multi_creation_failed = false
79
+ self.max_concurrency = @options.fetch(:max_concurrency, 200)
80
+ end
81
+
82
+ # Return the Ethon multi, creating it on first use. Constructing a Multi
83
+ # initializes libcurl, which Ethon 0.19 refuses to do in a process that
84
+ # forked after the parent initialized it. Creating or memoizing a Hydra
85
+ # during boot must therefore not create the native handle yet.
86
+ #
87
+ # @return [ Ethon::Multi ] The multi handle.
88
+ #
89
+ # @api private
90
+ def multi
91
+ @multi ||= build_multi
92
+ end
93
+
94
+ # Add a request to the hydra.
95
+ #
96
+ # This is the hot path for every request. The checks are unrolled instead
97
+ # of spread over a chain of included modules so a request that uses no
98
+ # optional features takes the shortest route to the multi.
99
+ #
100
+ # @example Add request.
101
+ # hydra.add(request)
102
+ #
103
+ # @param [ Typhoeus::Request ] request The request to add.
104
+ #
105
+ # @return [ Object ] The result of the handling callback or the multi add.
106
+ def add(request)
107
+ request.hydra = self
108
+
109
+ before_callbacks = Typhoeus.before_callbacks_for(request)
110
+ if before_callbacks
111
+ before_callbacks.each do |callback|
112
+ value = callback.call(request)
113
+ if value.is_a?(Response)
114
+ unless request.response
115
+ # A synthetic response is a short circuit like a stub. Mark it
116
+ # so success? works and it is not written to the cache.
117
+ value.mock = true
118
+ request.execute_headers_callbacks(value)
119
+ request.on_body.each { |body_callback| body_callback.call(value.body, value) }
120
+ request.finish(value)
121
+ end
122
+ dequeue_many
123
+ return value
124
+ elsif value.nil? || value == false
125
+ dequeue_many
126
+ return value
127
+ end
128
+ end
129
+ end
130
+
131
+ unless Typhoeus.stubs_disabled?
132
+ if response = Expectation.response_for(request)
133
+ # The stub block already ran this request for real.
134
+ return if response.request.equal?(request)
135
+
136
+ request.execute_headers_callbacks(response)
137
+ request.on_body.each { |callback| callback.call(response.body, response) }
138
+ return request.finish(response)
139
+ end
140
+ end
141
+
142
+ raise Typhoeus::Errors::NoStub.new(request) if request.blocked?
143
+
144
+ if request.cacheable? && response = request.cached_response
145
+ request.finish_cached_response(response)
146
+ return dequeue_many
147
+ end
148
+
149
+ if request.memoizable?
150
+ add_memoized(request)
151
+ else
152
+ add_to_multi(request)
153
+ end
154
+ end
155
+
156
+ # Return the memory.
157
+ #
158
+ # @example Get memory.
159
+ # hydra.memory
160
+ #
161
+ # @return [ Hash ] The memory.
162
+ def memory
163
+ @memory ||= {}
164
+ end
165
+
166
+ # Requests that are waiting for an identical request to finish.
167
+ #
168
+ # @return [ Hash ]
169
+ #
170
+ # @api private
171
+ def pending
172
+ @pending ||= {}
173
+ end
174
+
175
+ # Start the hydra run.
176
+ #
177
+ # @example Start hydra run.
178
+ # hydra.run
179
+ #
180
+ # @return [ Symbol ] Return value from multi.perform.
181
+ def run
182
+ memory.clear
183
+ dequeue_many
184
+ # Nothing reached the multi (all requests were answered by a before
185
+ # callback, a stub, or the cache), so do not initialize libcurl.
186
+ return if !defined?(@multi) || @multi.nil?
187
+
188
+ multi.perform
189
+ rescue Exception => error
190
+ begin
191
+ cleanup_after_error
192
+ reset_multi if multi_handle_unusable?(error)
193
+ rescue StandardError
194
+ # Preserve the original error.
195
+ end
196
+ raise
197
+ ensure
198
+ finish_pending_after_error
199
+ memory.clear
200
+ pending.clear
201
+ end
202
+
203
+ # Set the maximum number of concurrent requests.
204
+ #
205
+ # @example Set max_concurrency.
206
+ # hydra.max_concurrency = 20
207
+ #
208
+ # @param [ Integer, String ] value The maximum number of requests to run
209
+ # at the same time.
210
+ #
211
+ # @raise [ ArgumentError ] When the value is not greater than zero.
212
+ #
213
+ # @return [ Integer ] The maximum number of concurrent requests.
214
+ def max_concurrency=(value)
215
+ concurrency = Integer(value)
216
+ if concurrency <= 0
217
+ raise ArgumentError, "max_concurrency must be greater than zero"
218
+ end
219
+ @max_concurrency = concurrency
220
+ end
221
+
222
+ private
223
+
224
+ # Add the easy handle for a request to the multi.
225
+ #
226
+ # @param [ Typhoeus::Request ] request The request to add.
227
+ #
228
+ # @return [ Object ] The result of the multi add.
229
+ def add_to_multi(request)
230
+ easy_factory = EasyFactory.new(request, self)
231
+ easy = easy_factory.get
232
+ begin
233
+ multi.add(easy)
234
+ rescue Exception
235
+ easy_factory.release_easy(easy) unless easy_factory.released?
236
+ raise
237
+ end
238
+ end
239
+
240
+ # Answer a memoized request from memory or attach it to the request that
241
+ # is already in flight.
242
+ #
243
+ # @param [ Typhoeus::Request ] request The request to add.
244
+ #
245
+ # @return [ Object ]
246
+ def add_memoized(request)
247
+ if memory.key?(request)
248
+ finish_request(request, memory[request])
249
+ dequeue_many
250
+ elsif pending.key?(request)
251
+ pending[request] << request
252
+ else
253
+ pending[request] = []
254
+ request.on_complete do |response|
255
+ # Finish from the snapshot taken before the leader's callbacks
256
+ # ran. A callback that changes the leader's response must not
257
+ # change the copy its followers receive. Keep the leader's own
258
+ # callback result as this callback's return value.
259
+ snapshot = memoized_response_for(request) || response
260
+ finish_pending(request, snapshot)
261
+ response.handled_response
262
+ end
263
+ begin
264
+ add_to_multi(request)
265
+ rescue Exception
266
+ pending.delete(request)
267
+ raise
268
+ end
269
+ end
270
+ end
271
+
272
+ # Finish all requests that waited for the given request.
273
+ #
274
+ # @param [ Typhoeus::Request ] request
275
+ # @param [ Typhoeus::Response ] response
276
+ #
277
+ # @return [ Object ] The response's handled response.
278
+ def finish_pending(request, response)
279
+ requests = delete_pending(request)
280
+ return response.handled_response unless requests
281
+
282
+ error = nil
283
+ requests.each do |pending_request|
284
+ begin
285
+ finish_request(pending_request, response)
286
+ rescue StandardError => e
287
+ error ||= e
288
+ end
289
+ end
290
+ raise error if error
291
+
292
+ response.handled_response
293
+ end
294
+
295
+ # Finish a request whose response is already available. Every request gets
296
+ # its own response object and the same header callbacks it would see for
297
+ # a stub or a cache hit.
298
+ #
299
+ # @param [ Typhoeus::Request ] request The waiting request.
300
+ # @param [ Typhoeus::Response ] response The response to copy.
301
+ #
302
+ # @return [ Object ] The response's handled response.
303
+ def finish_request(request, response)
304
+ copy = response.dup
305
+ copy.request = request
306
+ request.execute_headers_callbacks(copy)
307
+ request.finish(copy, true)
308
+ end
309
+
310
+ # Finish followers whose leader produced a response but whose callbacks
311
+ # raised before finish_pending could run.
312
+ #
313
+ # @return [ void ]
314
+ def finish_pending_after_error
315
+ pending.to_a.each do |request, requests|
316
+ response = memoized_response_for(request)
317
+ next unless response
318
+
319
+ requests.each do |pending_request|
320
+ next if pending_request.response
321
+
322
+ begin
323
+ finish_request(pending_request, response)
324
+ rescue StandardError
325
+ # Preserve the original run error.
326
+ end
327
+ end
328
+ end
329
+ end
330
+
331
+ # Remove the waiting requests without hashing the leader again.
332
+ # Request#hash includes the request options, which a callback may have
333
+ # changed after the entry was added.
334
+ #
335
+ # @param [ Typhoeus::Request ] request The leader request.
336
+ #
337
+ # @return [ Array<Typhoeus::Request>, nil ] The waiting requests.
338
+ def delete_pending(request)
339
+ requests = nil
340
+ pending.delete_if do |pending_request, waiting_requests|
341
+ next false unless pending_request.equal?(request)
342
+
343
+ requests = waiting_requests
344
+ true
345
+ end
346
+ requests
347
+ end
348
+
349
+ # Find the leader's response by identity for the same reason. A changed
350
+ # option makes the request unusable as a hash key.
351
+ #
352
+ # @param [ Typhoeus::Request ] request The leader request.
353
+ #
354
+ # @return [ Typhoeus::Response, nil ] The memoized response.
355
+ def memoized_response_for(request)
356
+ memory.each_pair do |memoized_request, response|
357
+ return response if memoized_request.equal?(request)
358
+ end
359
+ nil
360
+ end
361
+
362
+ # Clean up the easies a failed run left behind. Requests that never reached
363
+ # the multi stay queued, so a caller can retry after handling the error.
364
+ #
365
+ # @return [ void ]
366
+ def cleanup_after_error
367
+ if defined?(@multi) && @multi
368
+ multi.easy_handles.dup.each do |easy|
369
+ begin
370
+ multi.delete(easy)
371
+ rescue StandardError
372
+ # The handle may already be unusable.
373
+ end
374
+ begin
375
+ Typhoeus::Pool.release(easy, :cleanup => true)
376
+ rescue StandardError
377
+ # Keep cleaning up the remaining handles.
378
+ end
379
+ end
380
+ end
381
+ ensure
382
+ # Invalid multi options make every queued request fail the same way.
383
+ queued_requests.clear if @multi_creation_failed
384
+ end
385
+
386
+ # A fatal multi_perform error leaves the native multi handle in an
387
+ # unknown state. Replace it so the hydra can run requests again.
388
+ #
389
+ # @return [ void ]
390
+ def reset_multi
391
+ begin
392
+ multi.cleanup if multi.respond_to?(:cleanup)
393
+ rescue StandardError
394
+ # Freeing the old handle is best effort; it is unusable anyway.
395
+ end
396
+ @multi = build_multi
397
+ end
398
+
399
+ # Ethon 0.19 raises MultiPerform when libcurl reports a fatal error from
400
+ # curl_multi_perform. Earlier versions do not have the class.
401
+ #
402
+ # @param [ Exception ] error The error raised by the run.
403
+ #
404
+ # @return [ Boolean ] Whether the multi handle has to be replaced.
405
+ def multi_handle_unusable?(error)
406
+ defined?(Ethon::Errors::MultiPerform) &&
407
+ error.is_a?(Ethon::Errors::MultiPerform)
408
+ end
409
+
410
+ # Build the multi handle from the options the hydra was created with.
411
+ #
412
+ # @return [ Ethon::Multi ] The new multi handle.
413
+ #
414
+ # @api private
415
+ def build_multi
416
+ @multi_creation_failed = false
417
+ Ethon::Multi.new(@options.reject { |key, _| key == :max_concurrency })
418
+ rescue StandardError
419
+ @multi_creation_failed = true
420
+ raise
93
421
  end
94
422
  end
95
423
  end
data/lib/typhoeus/pool.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thread'
2
4
 
3
5
  module Typhoeus
@@ -16,11 +18,32 @@ module Typhoeus
16
18
  #
17
19
  # @example Release easy.
18
20
  # Typhoeus::Pool.release(easy)
19
- def self.release(easy)
20
- easy.cookielist = "flush" # dump all known cookies to 'cookiejar'
21
- easy.cookielist = "all" # remove all cookies from memory for this handle
21
+ #
22
+ # @example Clean up instead of pooling.
23
+ # Typhoeus::Pool.release(easy, :cleanup => true)
24
+ def self.release(easy, cleanup: false)
25
+ reset_after_fork
26
+
27
+ if cleanup
28
+ easy.cleanup
29
+ return
30
+ end
31
+
32
+ clear_cookies(easy)
22
33
  easy.reset
23
- @mutex.synchronize { easies << easy }
34
+
35
+ discarded = nil
36
+ @mutex.synchronize do
37
+ if max_size && easies.length >= max_size
38
+ discarded = easy
39
+ else
40
+ easies << easy
41
+ end
42
+ end
43
+ discarded.cleanup if discarded
44
+ rescue StandardError => e
45
+ # Ethon 0.19+ refuses to reset handles created before a fork.
46
+ raise unless defined?(Ethon::Errors::ForkSafety) && e.is_a?(Ethon::Errors::ForkSafety)
24
47
  end
25
48
 
26
49
  # Return an easy from the pool.
@@ -30,22 +53,34 @@ module Typhoeus
30
53
  #
31
54
  # @return [ Ethon::Easy ] The easy.
32
55
  def self.get
33
- @mutex.synchronize do
34
- if @pid == Process.pid
35
- easies.pop
36
- else
37
- # Process has forked. Clear all easies to avoid sockets being
38
- # shared between processes.
39
- @pid = Process.pid
40
- easies.clear
41
- nil
42
- end
43
- end || Ethon::Easy.new
56
+ reset_after_fork
57
+ @mutex.synchronize { easies.pop } || Ethon::Easy.new
44
58
  end
45
59
 
46
- # Clear the pool
60
+ # Clear the pool and release the native easy handles it owns.
61
+ #
62
+ # @example Clear the pool.
63
+ # Typhoeus::Pool.clear
64
+ #
65
+ # @return [ Array<Ethon::Easy> ] The now empty pool.
47
66
  def self.clear
48
- @mutex.synchronize { easies.clear }
67
+ reset_after_fork
68
+ cleared = @mutex.synchronize do
69
+ current = easies.dup
70
+ easies.clear
71
+ current
72
+ end
73
+
74
+ cleared.each do |easy|
75
+ begin
76
+ easy.cleanup
77
+ rescue StandardError
78
+ # Clearing the pool is best effort. A handle that cannot be freed
79
+ # must not keep the other pooled handles alive.
80
+ end
81
+ end
82
+
83
+ easies
49
84
  end
50
85
 
51
86
  # Use yielded easy, will be released automatically afterwards.
@@ -63,8 +98,36 @@ module Typhoeus
63
98
 
64
99
  private
65
100
 
101
+ # Drop state inherited from a parent process. The previous mutex may have
102
+ # been locked by a thread that does not exist after the fork, so it is
103
+ # replaced without locking it to avoid a deadlock.
104
+ def self.reset_after_fork
105
+ return if @pid == Process.pid
106
+
107
+ # Replace the lock before publishing the new pid so a thread that sees
108
+ # the new pid cannot pick up the inherited lock.
109
+ @mutex = Mutex.new
110
+ @easies = []
111
+ @pid = Process.pid
112
+ end
113
+
114
+ def self.max_size
115
+ Typhoeus::Config.max_idle_easies if defined?(Typhoeus::Config)
116
+ end
117
+
66
118
  def self.easies
67
119
  @easies ||= []
68
120
  end
121
+
122
+ # Flush cookies to the configured jar and clear them from the handle.
123
+ # libcurl builds without cookie support reject CURLOPT_COOKIELIST; that
124
+ # must not prevent the handle from being recycled.
125
+ def self.clear_cookies(easy)
126
+ easy.cookielist = "flush"
127
+ easy.cookielist = "all"
128
+ rescue StandardError => e
129
+ raise unless defined?(Ethon::Errors::UnsupportedOption) &&
130
+ e.is_a?(Ethon::Errors::UnsupportedOption)
131
+ end
69
132
  end
70
133
  end
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "typhoeus"
2
4
 
3
5
  module Rails
4
6
  module Typhoeus
5
7
  class Railtie < Rails::Railtie
6
8
  # Need to include the Typhoeus middleware.
7
- initializer "include the identity map" do |app|
8
- app.config.middleware.use "Rack::Typhoeus::Middleware::ParamsDecoder"
9
+ initializer "typhoeus.params_decoder" do |app|
10
+ app.config.middleware.use Rack::Typhoeus::Middleware::ParamsDecoder
9
11
  end
10
12
  end
11
13
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Typhoeus
2
4
  class Request
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Typhoeus
2
4
  class Request
3
5
 
@@ -92,6 +94,9 @@ module Typhoeus
92
94
 
93
95
  # Set on_progress callback.
94
96
  #
97
+ # The callback is invoked by libcurl while the request transfers data.
98
+ # It is not called when the request completes.
99
+ #
95
100
  # @example Set on_progress.
96
101
  # request.on_progress do |dltotal, dlnow, ultotal, ulnow|
97
102
  # puts "dltotal (#{dltotal}), dlnow (#{dlnow}), ultotal (#{ultotal}), ulnow (#{ulnow})"
@@ -99,7 +104,8 @@ module Typhoeus
99
104
  #
100
105
  # @param [ Block ] block The block to execute.
101
106
  #
102
- # @yield [ Typhoeus::Response ]
107
+ # @yield [ Integer, Integer, Integer, Integer ] The download total,
108
+ # downloaded bytes, upload total, and uploaded bytes.
103
109
  #
104
110
  # @return [ Array<Block> ] All on_progress blocks.
105
111
  def on_progress(&block)
@@ -124,8 +130,9 @@ module Typhoeus
124
130
  end
125
131
 
126
132
  # Execute necessary callback and yields response. This
127
- # include in every case on_complete and on_progress, on_success
128
- # if successful and on_failure if not.
133
+ # includes on_complete in every case, on_success if successful
134
+ # and on_failure if not. Progress callbacks are invoked by libcurl
135
+ # while the transfer is running and are not called here.
129
136
  #
130
137
  # @example Execute callbacks.
131
138
  # request.execute_callbacks
@@ -134,7 +141,7 @@ module Typhoeus
134
141
  #
135
142
  # @api private
136
143
  def execute_callbacks
137
- callbacks = Typhoeus.on_complete + Typhoeus.on_progress + on_complete + on_progress
144
+ callbacks = Typhoeus.on_complete + on_complete
138
145
 
139
146
  if response && response.success?
140
147
  callbacks += Typhoeus.on_success + on_success
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Typhoeus
2
4
  class Request
3
5
 
@@ -5,9 +7,10 @@ module Typhoeus
5
7
  module Marshal
6
8
 
7
9
  # Return the important data needed to serialize this Request, except the
8
- # request callbacks and `hydra`, since they cannot be marshalled.
10
+ # request callbacks, `hydra`, and the response. The response may contain
11
+ # arbitrary callback results and can be serialized separately.
9
12
  def marshal_dump
10
- unmarshallable = %w(@on_complete @on_success @on_failure @on_progress @on_headers @on_body @hydra)
13
+ unmarshallable = %w(@on_complete @on_success @on_failure @on_progress @on_headers @on_body @before @hydra @response)
11
14
  (instance_variables - unmarshallable - unmarshallable.map(&:to_sym)).map do |name|
12
15
  [name, instance_variable_get(name)]
13
16
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Typhoeus
2
4
  class Request
3
5