typhoeus 0.1.28 → 0.2.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.
data/.gitignore CHANGED
@@ -1,2 +1,3 @@
1
1
  .*.sw?
2
2
  *.gem
3
+ .bundle
data/CHANGELOG.markdown CHANGED
@@ -1,3 +1,25 @@
1
+ 0.2.0
2
+ ------------
3
+ * Fix warning in Request#headers from attr_accessor
4
+ * Params with array values were not parsing into the format that rack expects
5
+ [GH-39, smartocci]
6
+ * Removed Rack as a dependency [GH-45]
7
+ * Added integration hooks for VCR!
8
+
9
+ 0.1.31
10
+ ------
11
+ * Fixed bug in setting compression encoding [morhekil]
12
+ * Exposed authentication control methods through Request interface [morhekil]
13
+
14
+ 0.1.30
15
+ -----------
16
+ * Exposed CURLOPT_CONNECTTIMEOUT_MS to Requests [balexis]
17
+
18
+ 0.1.29
19
+ ------
20
+ * Fixed a memory corruption with using CURLOPT_POSTFIELDS [gravis,
21
+ 32531d0821aecc4]
22
+
1
23
  0.1.28
2
24
  ----------------
3
25
  * Added SSL cert options for Typhoeus::Easy [GH-25, gravis]
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source :rubygems
2
+
3
+ group :test do
4
+ gem 'rspec', '1.3.1'
5
+ gem 'jeweler'
6
+ gem 'json'
7
+ gem 'sinatra'
8
+ gem 'diff-lcs'
9
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,30 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ diff-lcs (1.1.2)
5
+ gemcutter (0.6.1)
6
+ git (1.2.5)
7
+ jeweler (1.4.0)
8
+ gemcutter (>= 0.1.0)
9
+ git (>= 1.2.5)
10
+ rubyforge (>= 2.0.0)
11
+ json (1.4.6)
12
+ json_pure (1.4.6)
13
+ rack (1.2.1)
14
+ rspec (1.3.1)
15
+ rubyforge (2.0.4)
16
+ json_pure (>= 1.1.7)
17
+ sinatra (1.1.0)
18
+ rack (~> 1.1)
19
+ tilt (~> 1.1)
20
+ tilt (1.1)
21
+
22
+ PLATFORMS
23
+ ruby
24
+
25
+ DEPENDENCIES
26
+ diff-lcs
27
+ jeweler
28
+ json
29
+ rspec (= 1.3.1)
30
+ sinatra
data/README.textile CHANGED
@@ -51,7 +51,7 @@ require 'json'
51
51
  request = Typhoeus::Request.new("http://www.pauldix.net",
52
52
  :body => "this is a request body",
53
53
  :method => :post,
54
- :headers => {:Accepts => "text/html"},
54
+ :headers => {:Accept => "text/html"},
55
55
  :timeout => 100, # milliseconds
56
56
  :cache_timeout => 60, # seconds
57
57
  :params => {:field1 => "a field"})
@@ -103,8 +103,8 @@ hydra.queue first_request
103
103
  hydra.queue second_request
104
104
  hydra.run # this is a blocking call that returns once all requests are complete
105
105
 
106
- first_request.handled_resposne # the value returned from the on_complete block
107
- second_request.handled_resposne # the value returned from the on_complete block (parsed JSON)
106
+ first_request.handled_response # the value returned from the on_complete block
107
+ second_request.handled_response # the value returned from the on_complete block (parsed JSON)
108
108
  </pre>
109
109
 
110
110
  The execution of that code goes something like this. The first and second requests are built and queued. When hydra is run the first and second requests run in parallel. When the first request completes, the third request is then built and queued up. The moment it is queued Hydra starts executing it. Meanwhile the second request would continue to run (or it could have completed before the first). Once the third request is done, hydra.run returns.
@@ -183,9 +183,8 @@ hydra.stub(:get, "http://localhost:3000/users")
183
183
  *Basic Authentication*
184
184
 
185
185
  <pre>
186
- require 'base64'
187
186
  response = Typhoeus::Request.get("http://twitter.com/statuses/followers.json",
188
- :headers => {"Authorization" => "Basic #{Base64.b64encode("#{username}:#{password}")}"})
187
+ :username => username, :password => password)
189
188
  </pre>
190
189
 
191
190
  *SSL*
@@ -227,9 +226,9 @@ e = Typhoeus::Request.get("https://example.com/action",
227
226
  end
228
227
  </pre>
229
228
 
230
- h2. NTLM authentication
229
+ h2. Advanced authentication
231
230
 
232
- Thanks for the authentication piece and this description go to Oleg Ivanov (morhekil). The major reason to start this fork was the need to perform NTLM authentication in Ruby. Now you can do it via Typhoeus::Easy interface using the following API.
231
+ Thanks for the authentication piece and this description go to Oleg Ivanov (morhekil). The major reason to start this fork was the need to perform NTLM authentication in Ruby, and other libcurl's authentications method were made possible as a result. Now you can do it via Typhoeus::Easy interface using the following API.
233
232
 
234
233
  <pre>
235
234
  e = Typhoeus::Easy.new
@@ -251,6 +250,22 @@ The following authentication types are available:
251
250
  * CURLAUTH_GSSNEGOTIATE
252
251
  * CURLAUTH_NTLM
253
252
  * CURLAUTH_DIGEST_IE
253
+ * CURLAUTH_AUTO
254
+
255
+ The last one (CURLAUTH_AUTO) is really a combination of all previous methods and is provided by Typhoeus for convenience. When you set authentication to auto, Typhoeus will retrieve the given URL first and examine it's headers to confirm what auth types are supported by the server. The it will select the strongest of available auth methods and will send the second request using the selected authentication method.
256
+
257
+ *Authentication via the quick request interface*
258
+
259
+ There's also an easy way to perform any kind of authentication via the quick request interface:
260
+
261
+ <pre>
262
+ e = Typhoeus::Request.get("http://example.com",
263
+ :username => 'username',
264
+ :password => 'password',
265
+ :method => :ntlm)
266
+ </pre>
267
+
268
+ All methods listed above is available in a shorter form - :basic, :digest, :gssnegotiate, :ntlm, :digest_ie, :auto.
254
269
 
255
270
  *Query of available auth types*
256
271
 
@@ -266,7 +281,13 @@ e = Typhoeus::Easy.new
266
281
  e.verbose = 1
267
282
  </pre>
268
283
 
269
- Please note that libcurl prints it's output to the console, so you'll need to run your scripts from the console to see the debug info.
284
+ or using the quick request:
285
+
286
+ <pre>
287
+ e = Typhoeus::Request.get("http://example.com", :verbose => true)
288
+ </pre>
289
+
290
+ Just remember that libcurl prints it's debug output to the console (to STDERR), so you'll need to run your scripts from the console to see it.
270
291
 
271
292
  h2. Benchmarks
272
293
 
data/Rakefile CHANGED
@@ -11,7 +11,6 @@ begin
11
11
  gemspec.email = "paul@pauldix.net"
12
12
  gemspec.homepage = "http://github.com/pauldix/typhoeus"
13
13
  gemspec.authors = ["Paul Dix"]
14
- gemspec.add_dependency "rack"
15
14
  gemspec.add_development_dependency "rspec"
16
15
  gemspec.add_development_dependency "jeweler"
17
16
  gemspec.add_development_dependency "diff-lcs"
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.28
1
+ 0.2.0
data/examples/twitter.rb CHANGED
@@ -1,5 +1,5 @@
1
- require File.dirname(__FILE__) + '/../lib/typhoeus.rb'
2
1
  require 'rubygems'
2
+ require File.dirname(__FILE__) + '/../lib/typhoeus.rb'
3
3
  require 'json'
4
4
 
5
5
  class Twitter
@@ -2,4 +2,5 @@ conftest.dSYM/*
2
2
  conftest.dSYM
3
3
  mkmf.log
4
4
  *.o
5
- native.bundle
5
+ native.bundleMakefile
6
+ *.bundle
data/lib/typhoeus/easy.rb CHANGED
@@ -12,9 +12,14 @@ module Typhoeus
12
12
  :CURLOPT_UPLOAD => 46,
13
13
  :CURLOPT_CUSTOMREQUEST => 10036,
14
14
  :CURLOPT_POSTFIELDS => 10015,
15
+ :CURLOPT_COPYPOSTFIELDS => 10165,
15
16
  :CURLOPT_POSTFIELDSIZE => 60,
16
17
  :CURLOPT_USERAGENT => 10018,
17
18
  :CURLOPT_TIMEOUT_MS => 155,
19
+ # Time-out connect operations after this amount of milliseconds.
20
+ # [Only works on unix-style/SIGALRM operating systems. IOW, does
21
+ # not work on Windows.
22
+ :CURLOPT_CONNECTTIMEOUT_MS => 156,
18
23
  :CURLOPT_NOSIGNAL => 99,
19
24
  :CURLOPT_HTTPHEADER => 10023,
20
25
  :CURLOPT_FOLLOWLOCATION => 52,
@@ -25,7 +30,7 @@ module Typhoeus
25
30
  :CURLOPT_PROXY => 10004,
26
31
  :CURLOPT_VERIFYPEER => 64,
27
32
  :CURLOPT_NOBODY => 44,
28
- :CURLOPT_ENCODING => 102,
33
+ :CURLOPT_ENCODING => 10000 + 102,
29
34
  :CURLOPT_SSLCERT => 10025,
30
35
  :CURLOPT_SSLCERTTYPE => 10086,
31
36
  :CURLOPT_SSLKEY => 10087,
@@ -45,15 +50,16 @@ module Typhoeus
45
50
  :CURLAUTH_DIGEST => 2,
46
51
  :CURLAUTH_GSSNEGOTIATE => 4,
47
52
  :CURLAUTH_NTLM => 8,
48
- :CURLAUTH_DIGEST_IE => 16
53
+ :CURLAUTH_DIGEST_IE => 16,
54
+ :CURLAUTH_AUTO => 16 | 8 | 4 | 2 | 1
49
55
  }
50
56
 
51
57
  def initialize
52
58
  @method = :get
53
- @post_dat_set = nil
54
59
  @headers = {}
55
60
 
56
- set_option(OPTION_VALUES[:CURLOPT_ENCODING], 'zlib') if supports_zlib?
61
+ # Enable encoding/compression support
62
+ set_option(OPTION_VALUES[:CURLOPT_ENCODING], '')
57
63
  end
58
64
 
59
65
  def headers=(hash)
@@ -100,6 +106,12 @@ module Typhoeus
100
106
  def max_redirects=(redirects)
101
107
  set_option(OPTION_VALUES[:CURLOPT_MAXREDIRS], redirects)
102
108
  end
109
+
110
+ def connect_timeout=(milliseconds)
111
+ @connect_timeout = milliseconds
112
+ set_option(OPTION_VALUES[:CURLOPT_NOSIGNAL], 1)
113
+ set_option(OPTION_VALUES[:CURLOPT_CONNECTTIMEOUT_MS], milliseconds)
114
+ end
103
115
 
104
116
  def timeout=(milliseconds)
105
117
  @timeout = milliseconds
@@ -158,8 +170,8 @@ module Typhoeus
158
170
 
159
171
  def post_data=(data)
160
172
  @post_data_set = true
161
- set_option(OPTION_VALUES[:CURLOPT_POSTFIELDS], data)
162
173
  set_option(OPTION_VALUES[:CURLOPT_POSTFIELDSIZE], data.length)
174
+ set_option(OPTION_VALUES[:CURLOPT_COPYPOSTFIELDS], data)
163
175
  end
164
176
 
165
177
  def params=(params)
@@ -167,12 +179,12 @@ module Typhoeus
167
179
  params_string = params.keys.collect do |k|
168
180
  value = params[k]
169
181
  if value.is_a? Hash
170
- value.keys.collect {|sk| Rack::Utils.escape("#{k}[#{sk}]") + "=" + Rack::Utils.escape(value[sk].to_s)}
182
+ value.keys.collect {|sk| Typhoeus::Utils.escape("#{k}[#{sk}]") + "=" + Typhoeus::Utils.escape(value[sk].to_s)}
171
183
  elsif value.is_a? Array
172
- key = Rack::Utils.escape(k.to_s)
173
- value.collect { |v| "#{key}=#{Rack::Utils.escape(v.to_s)}" }.join('&')
184
+ key = Typhoeus::Utils.escape(k.to_s)
185
+ value.collect { |v| "#{key}=#{Typhoeus::Utils.escape(v.to_s)}" }.join('&')
174
186
  else
175
- "#{Rack::Utils.escape(k.to_s)}=#{Rack::Utils.escape(params[k].to_s)}"
187
+ "#{Typhoeus::Utils.escape(k.to_s)}=#{Typhoeus::Utils.escape(params[k].to_s)}"
176
188
  end
177
189
  end.flatten.join("&")
178
190
 
@@ -0,0 +1,24 @@
1
+ module Typhoeus
2
+ class Hydra
3
+ module Callbacks
4
+ def self.extended(base)
5
+ class << base
6
+ attr_accessor :global_hooks
7
+ end
8
+ base.global_hooks = Hash.new { |h, k| h[k] = [] }
9
+ end
10
+
11
+ def after_request_before_on_complete(&block)
12
+ global_hooks[:after_request_before_on_complete] << block
13
+ end
14
+
15
+ def run_global_hooks_for(name, request)
16
+ global_hooks[name].each { |hook| hook.call(request) }
17
+ end
18
+
19
+ def clear_global_hooks
20
+ global_hooks.clear
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,45 @@
1
+ module Typhoeus
2
+ class Hydra
3
+ class NetConnectNotAllowedError < StandardError; end
4
+
5
+ module ConnectOptions
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ # This method checks to see if we should raise an error on
11
+ # a request.
12
+ #
13
+ # @raises NetConnectNotAllowedError
14
+ def check_allow_net_connect!(request)
15
+ if !Typhoeus::Hydra.allow_net_connect? and (!Typhoeus::Hydra.ignore_localhost? or !request.localhost?)
16
+ raise NetConnectNotAllowedError, "Real HTTP requests are not allowed. Unregistered request: #{request.inspect}"
17
+ end
18
+ end
19
+ private :check_allow_net_connect!
20
+
21
+ module ClassMethods
22
+ def self.extended(base)
23
+ class << base
24
+ attr_accessor :allow_net_connect
25
+ attr_accessor :ignore_localhost
26
+ end
27
+ base.allow_net_connect = true
28
+ base.ignore_localhost = false
29
+ end
30
+
31
+ # Returns whether we allow external HTTP connections.
32
+ # Useful for mocking/tests.
33
+ #
34
+ # @return [boolean] true/false
35
+ def allow_net_connect?
36
+ allow_net_connect
37
+ end
38
+
39
+ def ignore_localhost?
40
+ ignore_localhost
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,52 @@
1
+ module Typhoeus
2
+ class Hydra
3
+ module Stubbing
4
+ module SharedMethods
5
+ def stub(method, url, options = {})
6
+ stubs << HydraMock.new(url, method, options)
7
+ stubs.last
8
+ end
9
+
10
+ def clear_stubs
11
+ self.stubs = []
12
+ end
13
+
14
+ def find_stub_from_request(request)
15
+ stubs.detect { |stub| stub.matches?(request) }
16
+ end
17
+
18
+ def self.extended(base)
19
+ class << base
20
+ attr_accessor :stubs
21
+ end
22
+ base.stubs = []
23
+ end
24
+ end
25
+
26
+ def self.included(base)
27
+ base.extend(SharedMethods)
28
+ base.class_eval do
29
+ attr_accessor :stubs
30
+ end
31
+ end
32
+
33
+ def assign_to_stub(request)
34
+ m = find_stub_from_request(request)
35
+
36
+ # Fallback to global stubs.
37
+ m ||= self.class.find_stub_from_request(request)
38
+
39
+ if m
40
+ m.add_request(request)
41
+ @active_stubs << m
42
+ m
43
+ else
44
+ nil
45
+ end
46
+ end
47
+ private :assign_to_stub
48
+
49
+ include SharedMethods
50
+ end
51
+ end
52
+ end
@@ -1,5 +1,13 @@
1
+ require 'typhoeus/hydra/callbacks'
2
+ require 'typhoeus/hydra/connect_options'
3
+ require 'typhoeus/hydra/stubbing'
4
+
1
5
  module Typhoeus
2
6
  class Hydra
7
+ include ConnectOptions
8
+ include Stubbing
9
+ extend Callbacks
10
+
3
11
  def initialize(options = {})
4
12
  @memoize_requests = true
5
13
  @multi = Multi.new
@@ -7,12 +15,13 @@ module Typhoeus
7
15
  initial_pool_size = options[:initial_pool_size] || 10
8
16
  @max_concurrency = options[:max_concurrency] || 200
9
17
  initial_pool_size.times { @easy_pool << Easy.new }
10
- @stubs = []
11
18
  @memoized_requests = {}
12
19
  @retrieved_from_cache = {}
13
20
  @queued_requests = []
14
21
  @running_requests = 0
15
- @stubbed_request_count = 0
22
+
23
+ self.stubs = []
24
+ @active_stubs = []
16
25
  end
17
26
 
18
27
  def self.hydra
@@ -28,10 +37,6 @@ module Typhoeus
28
37
  @cache_getter = nil
29
38
  end
30
39
 
31
- def clear_stubs
32
- @stubs = []
33
- end
34
-
35
40
  def fire_and_forget
36
41
  @queued_requests.each {|r| queue(r, false)}
37
42
  @multi.fire_and_forget
@@ -40,6 +45,10 @@ module Typhoeus
40
45
  def queue(request, obey_concurrency_limit = true)
41
46
  return if assign_to_stub(request)
42
47
 
48
+ # At this point, we are running over live HTTP. Make sure we haven't
49
+ # disabled live requests.
50
+ check_allow_net_connect!(request)
51
+
43
52
  if @running_requests >= @max_concurrency && obey_concurrency_limit
44
53
  @queued_requests << request
45
54
  else
@@ -62,14 +71,14 @@ module Typhoeus
62
71
  end
63
72
 
64
73
  def run
65
- while @stubbed_request_count > 0
66
- @stubs.each do |m|
67
- while request = m.requests.shift
68
- @stubbed_request_count -= 1
69
- m.response.request = request
70
- handle_request(request, m.response)
71
- end
74
+ while !@active_stubs.empty?
75
+ m = @active_stubs.first
76
+ while request = m.requests.shift
77
+ response = m.response
78
+ response.request = request
79
+ handle_request(request, response)
72
80
  end
81
+ @active_stubs.delete(m)
73
82
  end
74
83
 
75
84
  @multi.perform
@@ -97,22 +106,6 @@ module Typhoeus
97
106
  @on_complete = proc
98
107
  end
99
108
 
100
- def stub(method, url)
101
- @stubs << HydraMock.new(url, method)
102
- @stubs.last
103
- end
104
-
105
- def assign_to_stub(request)
106
- m = @stubs.detect {|stub| stub.matches? request}
107
- if m
108
- m.add_request(request)
109
- @stubbed_request_count += 1
110
- else
111
- nil
112
- end
113
- end
114
- private :assign_to_stub
115
-
116
109
  def get_from_cache_or_queue(request)
117
110
  if @cache_getter
118
111
  val = @cache_getter.call(request)
@@ -132,12 +125,19 @@ module Typhoeus
132
125
  @running_requests += 1
133
126
 
134
127
  easy = @easy_pool.pop || Easy.new
128
+ easy.verbose = request.verbose
129
+ if request.username || request.password
130
+ auth = { :username => request.username, :password => request.password }
131
+ auth[:method] = Typhoeus::Easy::AUTH_TYPES["CURLAUTH_#{request.auth_method.to_s.upcase}".to_sym] if request.auth_method
132
+ easy.auth = auth
133
+ end
135
134
  easy.url = request.url
136
135
  easy.method = request.method
137
136
  easy.params = request.params if request.method == :post && !request.params.nil?
138
137
  easy.headers = request.headers if request.headers
139
138
  easy.request_body = request.body if request.body
140
139
  easy.timeout = request.timeout if request.timeout
140
+ easy.connect_timeout = request.connect_timeout if request.connect_timeout
141
141
  easy.follow_location = request.follow_location if request.follow_location
142
142
  easy.max_redirects = request.max_redirects if request.max_redirects
143
143
  easy.proxy = request.proxy if request.proxy
@@ -149,6 +149,7 @@ module Typhoeus
149
149
  easy.ssl_key_password = request.ssl_key_password
150
150
  easy.ssl_cacert = request.ssl_cacert
151
151
  easy.ssl_capath = request.ssl_capath
152
+ easy.verbose = request.verbose
152
153
 
153
154
  easy.on_success do |easy|
154
155
  queue_next
@@ -180,6 +181,9 @@ module Typhoeus
180
181
  def handle_request(request, response, live_request = true)
181
182
  request.response = response
182
183
 
184
+ self.class.run_global_hooks_for(:after_request_before_on_complete,
185
+ request)
186
+
183
187
  if live_request && request.cache_timeout && @cache_setter
184
188
  @cache_setter.call(request)
185
189
  end
@@ -205,30 +209,4 @@ module Typhoeus
205
209
  end
206
210
  private :response_from_easy
207
211
  end
208
-
209
- class HydraMock
210
- attr_reader :url, :method, :response, :requests
211
-
212
- def initialize(url, method)
213
- @url = url
214
- @method = method
215
- @requests = []
216
- end
217
-
218
- def add_request(request)
219
- @requests << request
220
- end
221
-
222
- def and_return(val)
223
- @response = val
224
- end
225
-
226
- def matches?(request)
227
- if url.kind_of?(String)
228
- request.method == method && request.url == url
229
- else
230
- request.method == method && url =~ request.url
231
- end
232
- end
233
- end
234
212
  end
@@ -0,0 +1,131 @@
1
+ module Typhoeus
2
+ class HydraMock
3
+ attr_reader :url, :method, :requests, :uri
4
+
5
+ def initialize(url, method, options = {})
6
+ @url = url
7
+ @uri = URI.parse(url) if url.kind_of?(String)
8
+ @method = method
9
+ @requests = []
10
+ @options = options
11
+ if @options[:headers]
12
+ @options[:headers] = Typhoeus::NormalizedHeaderHash.new(@options[:headers])
13
+ end
14
+
15
+ @current_response_index = 0
16
+ end
17
+
18
+ def body
19
+ @options[:body]
20
+ end
21
+
22
+ def body?
23
+ @options.has_key?(:body)
24
+ end
25
+
26
+ def headers
27
+ @options[:headers]
28
+ end
29
+
30
+ def headers?
31
+ @options.has_key?(:headers)
32
+ end
33
+
34
+ def add_request(request)
35
+ @requests << request
36
+ end
37
+
38
+ def and_return(val)
39
+ if val.respond_to?(:each)
40
+ @responses = val
41
+ else
42
+ @responses = [val]
43
+ end
44
+
45
+ # make sure to mark them as a mock.
46
+ @responses.each { |r| r.mock = true }
47
+
48
+ val
49
+ end
50
+
51
+ def response
52
+ if @current_response_index == (@responses.length - 1)
53
+ @responses.last
54
+ else
55
+ value = @responses[@current_response_index]
56
+ @current_response_index += 1
57
+ value
58
+ end
59
+ end
60
+
61
+ def matches?(request)
62
+ if !method_matches?(request) or !url_matches?(request)
63
+ return false
64
+ end
65
+
66
+ if body?
67
+ return false unless body_matches?(request)
68
+ end
69
+
70
+ if headers?
71
+ return false unless headers_match?(request)
72
+ end
73
+
74
+ true
75
+ end
76
+
77
+ private
78
+ def method_matches?(request)
79
+ self.method == :any or self.method == request.method
80
+ end
81
+
82
+ def url_matches?(request)
83
+ if url.kind_of?(String)
84
+ request_uri = URI.parse(request.url)
85
+ request_uri == self.uri
86
+ else
87
+ self.url =~ request.url
88
+ end
89
+ end
90
+
91
+ def body_matches?(request)
92
+ !request.body.nil? && !request.body.empty? && request.body == self.body
93
+ end
94
+
95
+ def headers_match?(request)
96
+ request_headers = NormalizedHeaderHash.new(request.headers)
97
+
98
+ if empty_headers?(self.headers)
99
+ empty_headers?(request_headers)
100
+ else
101
+ return false if empty_headers?(request_headers)
102
+
103
+ headers.each do |key, value|
104
+ return false unless header_value_matches?(value, request_headers[key])
105
+ end
106
+
107
+ true
108
+ end
109
+ end
110
+
111
+ def header_value_matches?(mock_value, request_value)
112
+ mock_arr = mock_value.is_a?(Array) ? mock_value : [mock_value]
113
+ request_arr = request_value.is_a?(Array) ? request_value : [request_value]
114
+
115
+ return false unless mock_arr.size == request_arr.size
116
+ mock_arr.all? do |value|
117
+ request_arr.any? { |a| value === a }
118
+ end
119
+ end
120
+
121
+ def empty_headers?(headers)
122
+ # We consider the default User-Agent header to be empty since
123
+ # Typhoeus always adds that.
124
+ headers.nil? || headers.empty? || default_typhoeus_headers?(headers)
125
+ end
126
+
127
+ def default_typhoeus_headers?(headers)
128
+ headers.size == 1 && headers['User-Agent'] == Typhoeus::USER_AGENT
129
+ end
130
+ end
131
+ end
@@ -11,7 +11,7 @@ module Typhoeus
11
11
  end
12
12
 
13
13
  def add(easy)
14
- easy.set_headers()
14
+ easy.set_headers() if easy.headers.empty?
15
15
  @easy_handles << easy
16
16
  multi_add_handle(easy)
17
17
  end