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
data/UPGRADE.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Upgrade guide
2
2
 
3
+ ## 1.7
4
+
5
+ ### Ethon 0.19
6
+
7
+ Typhoeus 1.7 requires Ethon 0.19 (`>= 0.19.0`, `< 0.20.0`). Several conditions
8
+ that earlier versions ignored now surface:
9
+
10
+ - Exceptions raised by `on_body` and `on_complete` callbacks are re-raised.
11
+ - `Ethon::Easy` and `Ethon::Multi` handles cannot be inherited across `fork`.
12
+ - Unknown enum options and options the loaded libcurl does not support raise
13
+ `Ethon::Errors::InvalidValue` and `Ethon::Errors::UnsupportedOption`.
14
+ - Multipart PUT requests are sent as `multipart/form-data`.
15
+ - An empty header value suppresses libcurl's default. End the key with a
16
+ semicolon to send an empty value, for example `"X-Empty;" => ""`.
17
+ - Verbose output is written through `Ethon.logger` instead of STDERR.
18
+
19
+ ### Progress callbacks
20
+
21
+ `on_progress` callbacks run only while data is transferred. They are no longer
22
+ called with a `Response` when the request completes, and they receive only the
23
+ four transfer counters. A block written for five parameters now gets `nil` as
24
+ the fifth value.
25
+
26
+ ### Before callbacks
27
+
28
+ A `before` callback that returns a `Typhoeus::Response` short-circuits the
29
+ request. The response is treated as a mock, so it is not cached and `success?`
30
+ works for it.
31
+
3
32
  ## 0.5
4
33
 
5
34
  ### Options
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  module Typhoeus
3
5
  module Middleware
@@ -19,26 +21,77 @@ module Rack
19
21
 
20
22
  # Recursively decodes Typhoeus encoded arrays in given Hash.
21
23
  #
22
- # @param hash [Hash]. This Hash will be modified!
24
+ # @param hash [Hash, Array]. A frozen hash or array is returned as is.
23
25
  #
24
26
  # @return [Hash] Hash with properly decoded nested arrays.
25
27
  def decode!(hash)
26
- return hash unless hash.is_a?(Hash)
27
- hash.each_pair do |key,value|
28
- if value.is_a?(Hash)
29
- decode!(value)
30
- hash[key] = convert(value)
28
+ return hash if hash.frozen?
29
+ return hash unless hash.respond_to?(:each_pair) || hash.is_a?(Array)
30
+
31
+ if hash.is_a?(Array)
32
+ hash.map! { |value| decode_value(value) }
33
+ else
34
+ hash.each_pair do |key, value|
35
+ hash[key] = decode_value(value)
31
36
  end
32
37
  end
33
38
  hash
34
39
  end
35
40
 
36
41
  def decode(hash)
37
- decode!(hash.dup)
42
+ decode!(deep_dup(hash))
38
43
  end
39
44
 
40
45
  private
41
46
 
47
+ # Decode one value, keeping plain values untouched.
48
+ #
49
+ # @param [ Object ] value The value to decode.
50
+ #
51
+ # @return [ Object ] The decoded value.
52
+ def decode_value(value)
53
+ if value.respond_to?(:each_pair)
54
+ decoded = decode!(value)
55
+ convert(hash_like_to_hash(decoded))
56
+ elsif value.is_a?(Array)
57
+ decode!(value)
58
+ else
59
+ value
60
+ end
61
+ end
62
+
63
+ # Copy nested hashes and arrays so decoding cannot mutate the
64
+ # caller's params.
65
+ #
66
+ # @param [ Object ] value The value to copy.
67
+ #
68
+ # @return [ Object ] The copied value.
69
+ def deep_dup(value)
70
+ if value.respond_to?(:each_pair)
71
+ copy = {}
72
+ value.each_pair { |key, nested| copy[key] = deep_dup(nested) }
73
+ copy
74
+ elsif value.is_a?(Array)
75
+ value.map { |nested| deep_dup(nested) }
76
+ else
77
+ value
78
+ end
79
+ end
80
+
81
+ # Copy a hash-like object without calling conversion methods such as
82
+ # Parameters#to_h, which raise for unpermitted Rails parameters.
83
+ #
84
+ # @param [ Object ] value The value to copy.
85
+ #
86
+ # @return [ Hash ] The copied hash.
87
+ def hash_like_to_hash(value)
88
+ return value if value.is_a?(Hash)
89
+
90
+ copy = {}
91
+ value.each_pair { |key, nested| copy[key] = nested }
92
+ copy
93
+ end
94
+
42
95
  # Checks if Hash is an Array encoded as a Hash.
43
96
  # Specifically will check for the Hash to have this
44
97
  # form: {'0' => v0, '1' => v1, .., 'n' => vN }
@@ -48,12 +101,12 @@ module Rack
48
101
  # @return [Boolean] True if its a encoded Array, else false.
49
102
  def encoded?(hash)
50
103
  return false if hash.empty?
51
- if hash.keys.size > 1
52
- keys = hash.keys.map{|i| i.to_i if i.respond_to?(:to_i)}.sort
53
- keys == hash.keys.size.times.to_a
54
- else
55
- hash.keys.first =~ /0/
56
- end
104
+
105
+ keys = hash.keys.map(&:to_s)
106
+ return keys.first == "0" if keys.size == 1
107
+
108
+ keys.all? { |key| key == key.to_i.to_s } &&
109
+ keys.map(&:to_i).sort == (0...keys.size).to_a
57
110
  end
58
111
 
59
112
  # If the Hash is an array encoded by typhoeus an array is returned
@@ -64,7 +117,7 @@ module Rack
64
117
  # @return [Arraya/Hash]
65
118
  def convert(hash)
66
119
  if encoded?(hash)
67
- hash.sort{ |a, b| a[0].to_i <=> b[0].to_i }.map{ |key, value| value }
120
+ hash.sort { |a, b| a[0].to_s.to_i <=> b[0].to_s.to_i }.map { |_, value| value }
68
121
  else
69
122
  hash
70
123
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack/typhoeus/middleware/params_decoder/helper'
2
4
 
3
5
  module Rack
@@ -29,27 +31,14 @@ module Rack
29
31
 
30
32
  def call(env)
31
33
  req = Rack::Request.new(env)
32
- decode(req.params).each_pair { |k, v| update_params req, k, v }
33
- @app.call(env)
34
- end
35
34
 
36
- private
35
+ # Query and form parameters are separate sources. Decoding the
36
+ # merged Rack::Request#params and writing the result to both would
37
+ # let a form value overwrite the query value for the same key.
38
+ decode(req.GET).each_pair { |k, v| req.GET[k] = v }
39
+ decode(req.POST).each_pair { |k, v| req.POST[k] = v }
37
40
 
38
- # Persist params change in environment. Extracted from:
39
- # https://github.com/rack/rack/blob/master/lib/rack/request.rb#L243
40
- def update_params(req, k, v)
41
- found = false
42
- if req.GET.has_key?(k)
43
- found = true
44
- req.GET[k] = v
45
- end
46
- if req.POST.has_key?(k)
47
- found = true
48
- req.POST[k] = v
49
- end
50
- unless found
51
- req.GET[k] = v
52
- end
41
+ @app.call(env)
53
42
  end
54
43
  end
55
44
  end
data/lib/rack/typhoeus.rb CHANGED
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rack/typhoeus/middleware/params_decoder"
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'typhoeus'
1
4
  require 'faraday'
2
5
 
3
6
  module Faraday # :nodoc:
@@ -19,21 +22,9 @@ module Faraday # :nodoc:
19
22
  #
20
23
  # response = conn.get("/")
21
24
  class Typhoeus < Faraday::Adapter
22
- self.supports_parallel = true
23
-
24
- (class << self; self; end).instance_eval do
25
- remove_method :setup_parallel_manager if method_defined? :setup_parallel_manager
26
- end
25
+ PathExpectedError = Class.new(StandardError)
27
26
 
28
- remove_method :call if method_defined? :call
29
- remove_method :perform_request if method_defined? :perform_request
30
- remove_method :request if method_defined? :request
31
- remove_method :read_body if method_defined? :read_body
32
- remove_method :configure_ssl if method_defined? :configure_ssl
33
- remove_method :configure_proxy if method_defined? :configure_proxy
34
- remove_method :configure_timeout if method_defined? :configure_timeout
35
- remove_method :configure_socket if method_defined? :configure_socket
36
- remove_method :parallel? if method_defined? :parallel?
27
+ self.supports_parallel = true
37
28
 
38
29
  # Initialize the Typhoeus adapter
39
30
  #
@@ -60,8 +51,6 @@ module Faraday # :nodoc:
60
51
  ::Typhoeus::Hydra.new(options)
61
52
  end
62
53
 
63
- dependency 'typhoeus'
64
-
65
54
  # Hook into Faraday and perform the request with Typhoeus.
66
55
  #
67
56
  # @param [ Hash ] env The environment.
@@ -107,7 +96,7 @@ module Faraday # :nodoc:
107
96
  end
108
97
  end
109
98
 
110
- save_response(env, resp.code, resp.body) do |response_headers|
99
+ save_response(env, resp.code, resp.body, nil, resp.status_message) do |response_headers|
111
100
  response_headers.parse resp.response_headers
112
101
  end
113
102
  # in async mode, :response is initialized at this point
@@ -134,18 +123,78 @@ module Faraday # :nodoc:
134
123
  def configure_ssl(req, env)
135
124
  ssl = env[:ssl]
136
125
 
137
- verify_p = (ssl && ssl.fetch(:verify, true))
126
+ verify_p = ssl_option(ssl, :verify, true)
127
+ verify_host = ssl_option(ssl, :verify_hostname, true)
128
+
129
+ if verify_p && verify_host
130
+ ssl_verifyhost = req.options.fetch(:ssl_verifyhost, 2)
131
+ else
132
+ ssl_verifyhost = 0
133
+ end
138
134
 
139
- ssl_verifyhost = verify_p ? 2 : 0
140
135
  req.options[:ssl_verifyhost] = ssl_verifyhost
141
136
  req.options[:ssl_verifypeer] = verify_p
142
- req.options[:sslversion] = ssl[:version] if ssl[:version]
143
- req.options[:sslcert] = ssl[:client_cert] if ssl[:client_cert]
144
- req.options[:sslkey] = ssl[:client_key] if ssl[:client_key]
145
- req.options[:cainfo] = ssl[:ca_file] if ssl[:ca_file]
146
- req.options[:capath] = ssl[:ca_path] if ssl[:ca_path]
147
- client_cert_passwd_key = [:client_cert_passwd, :client_certificate_password].detect { |name| ssl.key?(name) }
148
- req.options[:keypasswd] = ssl[client_cert_passwd_key] if client_cert_passwd_key
137
+ ssl_version = ssl_version_for(ssl)
138
+ req.options[:sslversion] = ssl_version if ssl_version
139
+
140
+ client_cert = ssl_option(ssl, :client_cert)
141
+ req.options[:sslcert] = path_for(client_cert, :client_cert) if client_cert
142
+ client_key = ssl_option(ssl, :client_key)
143
+ req.options[:sslkey] = path_for(client_key, :client_key) if client_key
144
+ ca_file = ssl_option(ssl, :ca_file)
145
+ req.options[:cainfo] = ca_file if ca_file
146
+ ca_path = ssl_option(ssl, :ca_path)
147
+ req.options[:capath] = ca_path if ca_path
148
+
149
+ client_cert_passwd_key = [:client_cert_passwd, :client_certificate_password].detect do |name|
150
+ !ssl_option(ssl, name).nil?
151
+ end
152
+ req.options[:keypasswd] = ssl_option(ssl, client_cert_passwd_key) if client_cert_passwd_key
153
+ end
154
+
155
+ # Return an SSL option from a Faraday SSLOptions struct or a Hash.
156
+ # Faraday leaves unset options nil and treats nil as the default, for
157
+ # example `verify_hostname?` is true unless the value is false.
158
+ #
159
+ # @return [ Object ] The option value or the default.
160
+ def ssl_option(ssl, name, default = nil)
161
+ return default unless ssl
162
+
163
+ value =
164
+ if ssl.is_a?(Hash)
165
+ ssl.fetch(name, default)
166
+ elsif ssl.respond_to?(name)
167
+ ssl.public_send(name)
168
+ end
169
+
170
+ value.nil? ? default : value
171
+ end
172
+
173
+ # Translate an OpenSSL/Faraday SSL version to Ethon's spelling.
174
+ #
175
+ # @return [ Symbol, nil ] The Ethon SSL version.
176
+ def ssl_version_for(ssl)
177
+ version = ssl_option(ssl, :version)
178
+ min_version = ssl_option(ssl, :min_version)
179
+ max_version = ssl_option(ssl, :max_version)
180
+
181
+ if version.nil? && min_version && min_version == max_version
182
+ version = min_version
183
+ elsif version.nil? && (min_version || max_version)
184
+ raise ArgumentError, "Typhoeus does not support ssl.min_version/max_version"
185
+ end
186
+
187
+ return nil unless version
188
+
189
+ normalized = version.to_s.downcase.to_sym
190
+ normalized = :default if normalized == :sslv23
191
+
192
+ supported = Ethon::Curl.easy_options(nil)[:sslversion][:opts]
193
+ unless supported.key?(normalized)
194
+ raise ArgumentError, "Unknown SSL version #{version.inspect}"
195
+ end
196
+
197
+ normalized
149
198
  end
150
199
 
151
200
  def configure_proxy(req, env)
@@ -155,14 +204,17 @@ module Faraday # :nodoc:
155
204
  req.options[:proxy] = "#{proxy[:uri].scheme}://#{proxy[:uri].host}:#{proxy[:uri].port}"
156
205
 
157
206
  if proxy[:user] && proxy[:password]
158
- req.options[:proxyauth] = :any
207
+ req.options[:proxyauth] ||= :basic
159
208
  req.options[:proxyuserpwd] = "#{proxy[:user]}:#{proxy[:password]}"
160
209
  end
161
210
  end
162
211
 
163
212
  def configure_timeout(req, env)
164
213
  env_req = env[:request]
165
- req.options[:timeout_ms] = (env_req[:timeout] * 1000).to_i if env_req[:timeout]
214
+ # Faraday exposes the read timeout separately. It falls back to the
215
+ # general timeout, like Faraday's built-in adapters do.
216
+ read_timeout = env_req[:read_timeout] || env_req[:timeout]
217
+ req.options[:timeout_ms] = (read_timeout * 1000).to_i if read_timeout
166
218
  req.options[:connecttimeout_ms] = (env_req[:open_timeout] * 1000).to_i if env_req[:open_timeout]
167
219
  end
168
220
 
@@ -175,6 +227,15 @@ module Faraday # :nodoc:
175
227
  def parallel?(env)
176
228
  !!env[:parallel_manager]
177
229
  end
230
+
231
+ def path_for(value, name)
232
+ return value if value.is_a?(String)
233
+ return value.to_path if value.respond_to?(:to_path)
234
+
235
+ raise PathExpectedError, "Typhoeus expects a path for `#{name}`"
236
+ end
178
237
  end
179
238
  end
180
239
  end
240
+
241
+ Faraday::Adapter.register_middleware :typhoeus => :Typhoeus if Faraday::Adapter.respond_to?(:register_middleware)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Typhoeus
2
4
  module Cache
3
5
  # This module provides a simple way to cache HTTP responses using Dalli.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Typhoeus
2
4
  module Cache
3
5
  # This module provides a simple way to cache HTTP responses in using the Rails cache.
@@ -17,7 +19,7 @@ module Typhoeus
17
19
  end
18
20
 
19
21
  def get(request)
20
- @cache.read(request)
22
+ @cache.read(request.cache_key)
21
23
  end
22
24
 
23
25
  def set(request, response)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Typhoeus
2
4
  module Cache
3
5
  # This module provides a simple way to cache HTTP responses in Redis.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Typhoeus
2
4
 
3
5
  # The Typhoeus configuration used to set global
@@ -22,8 +24,8 @@ module Typhoeus
22
24
  #
23
25
  # @return [ Boolean ]
24
26
  #
25
- # @see Typhoeus::Request::BlockConnection
26
- # @see Typhoeus::Hydra::BlockConnection
27
+ # @see Typhoeus::Request#run
28
+ # @see Typhoeus::Hydra
27
29
  # @see Typhoeus#with_connection
28
30
  # @see Typhoeus::Errors::NoStub
29
31
  attr_accessor :block_connection
@@ -33,11 +35,11 @@ module Typhoeus
33
35
  # @return [ Boolean ]
34
36
  #
35
37
  # @see Typhoeus::Hydra
36
- # @see Typhoeus::Hydra::Memoizable
38
+ # @see Typhoeus::Request#run
37
39
  attr_accessor :memoize
38
40
 
39
- # Defines whether curls debug output is shown.
40
- # Unfortunately it prints to stderr.
41
+ # Defines whether curl's debug output is shown. The output is written
42
+ # through Ethon.logger at the debug level.
41
43
  #
42
44
  # @return [ Boolean ]
43
45
  #
@@ -64,10 +66,33 @@ module Typhoeus
64
66
  #
65
67
  # @return [ Object ]
66
68
  #
67
- # @see Typhoeus::Hydra::Cacheable
68
- # @see Typhoeus::Request::Cacheable
69
+ # @see Typhoeus::Hydra
70
+ # @see Typhoeus::Request#run
69
71
  attr_accessor :cache
70
72
 
73
+ # Defines optional hydra features that should be skipped. Currently
74
+ # `:before` is supported. Skipping {Typhoeus.before} saves a callback
75
+ # lookup and invocation for every request that is added to a hydra.
76
+ #
77
+ # @example Skip before callbacks.
78
+ # Typhoeus::Config.exclude_features = [:before]
79
+ #
80
+ # @return [ Array<Symbol>, nil ]
81
+ #
82
+ # @see Typhoeus::Hydra#add
83
+ attr_accessor :exclude_features
84
+
85
+ # Returns whether an optional feature is enabled.
86
+ #
87
+ # @param [ Symbol ] feature The feature name.
88
+ #
89
+ # @return [ Boolean ] Whether the feature is enabled.
90
+ #
91
+ # @api private
92
+ def feature_enabled?(feature)
93
+ exclude_features.nil? || !exclude_features.include?(feature)
94
+ end
95
+
71
96
  # Defines whether to use a default user agent.
72
97
  #
73
98
  # @return [ String ]
@@ -81,5 +106,24 @@ module Typhoeus
81
106
  #
82
107
  # @see Typhoeus::Request#set_defaults
83
108
  attr_accessor :proxy
109
+
110
+ # Defines the maximum number of idle easy handles kept in the
111
+ # process-wide pool. Defaults to 20. Set it to nil to keep every
112
+ # handle, or to 0 to disable pooling.
113
+ #
114
+ # @return [ Integer, nil ]
115
+ #
116
+ # @see Typhoeus::Pool
117
+ attr_reader :max_idle_easies
118
+
119
+ def max_idle_easies=(value)
120
+ if !value.nil? && (!value.is_a?(Integer) || value < 0)
121
+ raise ArgumentError, "max_idle_easies must be nil or a non-negative Integer"
122
+ end
123
+
124
+ @max_idle_easies = value
125
+ end
126
+
127
+ @max_idle_easies = 20
84
128
  end
85
129
  end