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,16 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'zlib'
2
4
  require 'digest/sha1'
3
5
  require 'typhoeus/request/actions'
4
- require 'typhoeus/request/before'
5
- require 'typhoeus/request/block_connection'
6
- require 'typhoeus/request/cacheable'
7
6
  require 'typhoeus/request/callbacks'
8
7
  require 'typhoeus/request/marshal'
9
- require 'typhoeus/request/memoizable'
10
- require 'typhoeus/request/operations'
11
- require 'typhoeus/request/responseable'
12
8
  require 'typhoeus/request/streamable'
13
- require 'typhoeus/request/stubbable'
14
9
 
15
10
  module Typhoeus
16
11
 
@@ -23,18 +18,11 @@ module Typhoeus
23
18
  #
24
19
  # @see (see #initialize)
25
20
  class Request
26
- extend Request::Actions
21
+ extend Request::Actions
27
22
  include Request::Callbacks::Types
28
23
  include Request::Callbacks
29
24
  include Request::Streamable
30
25
  include Request::Marshal
31
- include Request::Operations
32
- include Request::Responseable
33
- include Request::Memoizable
34
- include Request::Cacheable
35
- include Request::BlockConnection
36
- include Request::Stubbable
37
- include Request::Before
38
26
 
39
27
  # Returns the provided base url.
40
28
  #
@@ -118,6 +106,184 @@ module Typhoeus
118
106
  set_defaults
119
107
  end
120
108
 
109
+ # Run the request.
110
+ #
111
+ # The before, stub, block connection, and cache checks run here explicitly
112
+ # instead of relying on the order of included modules.
113
+ #
114
+ # @example Make a request.
115
+ # Typhoeus::Request.new("www.example.com").run
116
+ #
117
+ # @return [ Response ] The response.
118
+ def run
119
+ callbacks = Typhoeus.before_callbacks_for(self)
120
+ if callbacks
121
+ callbacks.each do |callback|
122
+ value = callback.call(self)
123
+ if value.is_a?(Response)
124
+ unless response
125
+ # A synthetic response is a short circuit like a stub. Mark it
126
+ # so success? works and it is not written to the cache.
127
+ value.mock = true
128
+ execute_headers_callbacks(value)
129
+ on_body.each { |body_callback| body_callback.call(value.body, value) }
130
+ finish(value)
131
+ end
132
+ return value
133
+ end
134
+ return response if value.nil? || value == false
135
+ end
136
+ end
137
+
138
+ unless Typhoeus.stubs_disabled?
139
+ if stub_response = Expectation.response_for(self)
140
+ # The stub block already ran this request for real.
141
+ return stub_response if stub_response.request.equal?(self)
142
+
143
+ execute_headers_callbacks(stub_response)
144
+ on_body.each { |callback| callback.call(stub_response.body, stub_response) }
145
+ return finish(stub_response)
146
+ end
147
+ end
148
+
149
+ raise Typhoeus::Errors::NoStub.new(self) if blocked?
150
+
151
+ if cached = cached_response
152
+ return finish_cached_response(cached)
153
+ end
154
+
155
+ perform
156
+ end
157
+
158
+ # Add a callback that runs before the request.
159
+ #
160
+ # @example Add a callback.
161
+ # request.before { |request| request.options[:timeout] = 5 }
162
+ #
163
+ # @param [ Proc ] block The callback to run.
164
+ #
165
+ # @yield [ Typhoeus::Request ]
166
+ #
167
+ # @return [ Array<Proc> ] The callbacks.
168
+ def before(&block)
169
+ @before ||= []
170
+ @before << block if block_given?
171
+ @before
172
+ end
173
+
174
+ # Returns whether a request is blocked. Takes request.block_connection and
175
+ # the connection state of the current thread into consideration.
176
+ #
177
+ # @example Blocked?
178
+ # request.blocked?
179
+ #
180
+ # @return [ Boolean ] True if blocked, false else.
181
+ def blocked?
182
+ if block_connection.nil?
183
+ Typhoeus.connection_blocked?
184
+ else
185
+ block_connection
186
+ end
187
+ end
188
+
189
+ # Return whether a request is memoizable.
190
+ #
191
+ # Streaming requests are not memoized because their body is not retained
192
+ # and every caller has to receive its own chunks.
193
+ #
194
+ # @example Is request memoizable?
195
+ # request.memoizable?
196
+ #
197
+ # @return [ Boolean ] Return true if memoizable, false else.
198
+ def memoizable?
199
+ Typhoeus::Config.memoize &&
200
+ !streaming? &&
201
+ request_method == :get
202
+ end
203
+
204
+ # Return whether a request is cacheable.
205
+ #
206
+ # @return [ Boolean ]
207
+ def cacheable?
208
+ cache
209
+ end
210
+
211
+ # Return the cached response for the request, if there is one.
212
+ #
213
+ # @return [ Typhoeus::Response, nil ]
214
+ #
215
+ # @api private
216
+ def cached_response
217
+ cacheable? && cache.get(self)
218
+ end
219
+
220
+ # Finish a response that came from the cache and deliver it through the
221
+ # same callbacks a stubbed response uses.
222
+ #
223
+ # @param [ Typhoeus::Response ] response The cached response.
224
+ #
225
+ # @return [ Typhoeus::Response ] The response.
226
+ #
227
+ # @api private
228
+ def finish_cached_response(response)
229
+ copy = response.dup
230
+ copy.cached = true
231
+ copy.request = self
232
+ execute_headers_callbacks(copy)
233
+ on_body.each { |callback| callback.call(copy.body, copy) }
234
+ finish(copy)
235
+ end
236
+
237
+ # Return the cache TTL for the request.
238
+ #
239
+ # @return [ Integer, nil ]
240
+ def cache_ttl
241
+ options[:cache_ttl]
242
+ end
243
+
244
+ # Set the response and execute the callbacks.
245
+ #
246
+ # @param [ Typhoeus::Response ] response The response.
247
+ # @param [ Boolean ] bypass_memoization Whether to bypass memoization.
248
+ #
249
+ # @return [ Typhoeus::Response ] The response.
250
+ def finish(response, bypass_memoization = nil)
251
+ if bypass_memoization
252
+ @response = response
253
+ else
254
+ self.response = response
255
+ end
256
+ self.response.request = self
257
+ execute_callbacks
258
+ response
259
+ end
260
+
261
+ # Return the response.
262
+ #
263
+ # @return [ Typhoeus::Response, nil ]
264
+ def response
265
+ @response ||= nil
266
+ end
267
+
268
+ # Set the response. Cache and memoization hooks run explicitly before the
269
+ # response is assigned, instead of relying on the order modules were
270
+ # included in.
271
+ #
272
+ # @param [ Typhoeus::Response ] value The response to set.
273
+ def response=(value)
274
+ if cacheable? && cacheable_response?(value)
275
+ # The request exposes this response to its callbacks and the caller.
276
+ # Store a snapshot so later mutation cannot change what a cache hit
277
+ # returns.
278
+ cache.set(self, value.dup)
279
+ end
280
+ # Followers receive a copy of the stored response, and the leader's
281
+ # callbacks must not change what they see. Store a snapshot instead of
282
+ # the response the leader exposes to its own callbacks.
283
+ hydra.memory[self] = value.dup if memoizable? && hydra
284
+ @response = value
285
+ end
286
+
121
287
  # Return the url.
122
288
  # In contrast to base_url which returns the value you specified, url returns
123
289
  # the full url including the parameters.
@@ -127,10 +293,7 @@ module Typhoeus
127
293
  #
128
294
  # @since 0.5.5
129
295
  def url
130
- easy = EasyFactory.new(self).get
131
- url = easy.url
132
- Typhoeus::Pool.release(easy)
133
- url
296
+ EasyFactory.new(self).url
134
297
  end
135
298
 
136
299
  # Returns whether other is equal to self.
@@ -145,8 +308,8 @@ module Typhoeus
145
308
  # @api private
146
309
  def eql?(other)
147
310
  self.class == other.class &&
148
- self.base_url == other.base_url &&
149
- fuzzy_hash_eql?(self.options, other.options)
311
+ base_url.to_s == other.base_url.to_s &&
312
+ fuzzy_hash_eql?(normalized_options, other.normalized_options)
150
313
  end
151
314
 
152
315
  # Overrides Object#hash.
@@ -163,22 +326,118 @@ module Typhoeus
163
326
  #
164
327
  # @return [ String ] The cache key.
165
328
  def cache_key
166
- Digest::SHA1.hexdigest "#{self.class.name}#{base_url}#{hashable_string_for(options)}"
329
+ # The cache store and the TTL only affect where and how long a response
330
+ # is stored, not what the response is. Leaving them in the key would
331
+ # silently miss the cache whenever the TTL changes.
332
+ hashable_options = normalized_options.tap do |dup_opts|
333
+ dup_opts.delete(:cache)
334
+ dup_opts.delete(:cache_ttl)
335
+ end
336
+
337
+ cache_key_components = [
338
+ self.class.name,
339
+ base_url,
340
+ hashable_string_for(hashable_options)
341
+ ]
342
+
343
+ Digest::SHA1.hexdigest(cache_key_components.join)
167
344
  end
168
345
 
169
- # Mimics libcurls POST body generation. This is not accurate, but good
346
+ # Mimics libcurl's POST body generation. This is not accurate, but good
170
347
  # enough for VCR.
171
348
  #
172
349
  # @return [ String ] The encoded body.
173
- # otherwise.
174
350
  #
175
351
  # @api private
176
352
  def encoded_body
177
- Ethon::Easy::Form.new(nil, options[:body]).to_s
353
+ easy = Typhoeus::Pool.get
354
+ begin
355
+ body_options = { :body => options[:body] }
356
+ [:params_encoding, :escape].each do |option|
357
+ body_options[option] = options[option] if options.key?(option)
358
+ end
359
+
360
+ # Let Ethon apply its escape rule for this method. The form must stay
361
+ # non-multipart because encoded_body returns a single string.
362
+ form = Ethon::Easy::Form.new(easy, options[:body])
363
+ form.define_singleton_method(:multipart?) { false }
364
+
365
+ action = if options.fetch(:method, :get).to_s.downcase == "put"
366
+ Ethon::Easy::Http::Put.new("http://localhost/", body_options)
367
+ else
368
+ Ethon::Easy::Http::Post.new("http://localhost/", body_options)
369
+ end
370
+ action.instance_variable_set(:@form, form)
371
+ action.setup(easy)
372
+ form.to_s
373
+ ensure
374
+ Typhoeus::Pool.release(easy) if easy
375
+ end
376
+ end
377
+
378
+ # Return the request method with the same defaults and spelling Ethon
379
+ # uses. Cache keys, memoization, and equality must not depend on whether
380
+ # the caller wrote :get, "GET", or omitted the default method.
381
+ #
382
+ # @return [ Symbol ] The normalized method.
383
+ #
384
+ # @api private
385
+ def request_method
386
+ (options[:method] || :get).to_s.downcase.to_sym
387
+ end
388
+
389
+ # Return the options with their implicit defaults filled in for comparison.
390
+ #
391
+ # @return [ Hash ] The normalized options.
392
+ #
393
+ # @api private
394
+ def normalized_options
395
+ normalized = options.merge(:method => request_method)
396
+ normalized.delete("method")
397
+ # GET is the default. Leaving it out keeps the cache key of an implicit
398
+ # GET stable across the normalization change.
399
+ normalized.delete(:method) if normalized[:method] == :get
400
+ normalized
178
401
  end
179
402
 
180
403
  private
181
404
 
405
+ # Perform the actual request.
406
+ #
407
+ # @return [ Typhoeus::Response ] The response.
408
+ def perform
409
+ easy_factory = EasyFactory.new(self)
410
+ easy = easy_factory.get
411
+ begin
412
+ easy.perform
413
+ rescue Exception
414
+ easy_factory.release_easy(easy) unless easy_factory.released?
415
+ raise
416
+ end
417
+ response
418
+ end
419
+
420
+ # Return the cache for the request, falling back to the global cache.
421
+ #
422
+ # @return [ Object, nil ]
423
+ def cache
424
+ return nil if options[:cache] === false
425
+ options[:cache] || Typhoeus::Config.cache
426
+ end
427
+
428
+ # A streaming response has no buffered body, and a transport failure or a
429
+ # stub is not a representation of the resource.
430
+ #
431
+ # @param [ Typhoeus::Response ] response The response to cache.
432
+ #
433
+ # @return [ Boolean ] Whether the response should be cached.
434
+ def cacheable_response?(response)
435
+ !response.cached? &&
436
+ !streaming? &&
437
+ !response.mock? &&
438
+ (response.return_code.nil? || response.return_code == :ok)
439
+ end
440
+
182
441
  # Checks if two hashes are equal or not, discarding
183
442
  # first-level hash order.
184
443
  #
@@ -199,11 +458,16 @@ module Typhoeus
199
458
  def hashable_string_for(obj)
200
459
  case obj
201
460
  when Hash
202
- hashable_string_for(obj.sort_by {|sub_obj| sub_obj.first.to_s})
461
+ pairs = obj.map do |key, value|
462
+ "#{hashable_string_for(key)}#{hashable_string_for(value)}"
463
+ end
464
+ "hash:#{obj.size}:#{pairs.sort.join}"
203
465
  when Array
204
- obj.map {|sub_obj| hashable_string_for(sub_obj)}.to_s
466
+ values = obj.map { |value| hashable_string_for(value) }
467
+ "array:#{obj.size}:#{values.join}"
205
468
  else
206
- obj.to_s
469
+ value = obj.to_s
470
+ "#{obj.class}:#{value.bytesize}:#{value}"
207
471
  end
208
472
  end
209
473
 
@@ -211,13 +475,36 @@ module Typhoeus
211
475
  def set_defaults
212
476
  default_user_agent = Config.user_agent || Typhoeus::USER_AGENT
213
477
 
214
- options[:headers] = {'User-Agent' => default_user_agent}.merge(options[:headers] || {})
215
- options[:headers]['Expect'] ||= ''
478
+ headers = (options[:headers] || {}).dup
479
+ unless header_key?(headers, "User-Agent")
480
+ headers = {"User-Agent" => default_user_agent}.merge(headers)
481
+ end
482
+ # An empty value suppresses libcurl's 100-continue. A nil value opts out
483
+ # of the header entirely, which lets libcurl add it when it sees fit.
484
+ unless header_key?(headers, "Expect")
485
+ headers["Expect"] = ""
486
+ end
487
+ headers.reject! { |key, value| key.to_s.casecmp?("Expect") && value.nil? }
488
+ options[:headers] = headers
216
489
  options[:verbose] = Typhoeus::Config.verbose if options[:verbose].nil? && !Typhoeus::Config.verbose.nil?
217
490
  options[:timeout] = Typhoeus::Config.timeout if options[:timeout].nil? && !Typhoeus::Config.timeout.nil?
218
491
  options[:connecttimeout] = Typhoeus::Config.connecttimeout if options[:connecttimeout].nil? && !Typhoeus::Config.connecttimeout.nil?
219
492
  options[:maxredirs] ||= 50
220
493
  options[:proxy] = Typhoeus::Config.proxy unless options.has_key?(:proxy) || Typhoeus::Config.proxy.nil?
494
+ options[:cainfo] = ENV["SSL_CERT_FILE"] if options[:cainfo].nil? && ENV["SSL_CERT_FILE"]
495
+ options[:capath] = ENV["SSL_CERT_DIR"] if options[:capath].nil? && ENV["SSL_CERT_DIR"]
496
+ end
497
+
498
+ # Check the headers for a name without relying on its casing. Header names
499
+ # are case-insensitive and libcurl sends every spelling it is given, so a
500
+ # second default header would be added to the caller's value.
501
+ #
502
+ # @param [ Hash ] headers The request headers.
503
+ # @param [ String ] name The header name to look for.
504
+ #
505
+ # @return [ Boolean ] Whether the header is already present.
506
+ def header_key?(headers, name)
507
+ headers.keys.any? { |key| key.to_s.casecmp?(name) }
221
508
  end
222
509
  end
223
510
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Typhoeus
2
4
  class Response
3
5
  module Cacheable
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'delegate'
2
4
 
3
5
  module Typhoeus
@@ -23,8 +25,173 @@ module Typhoeus
23
25
  parse
24
26
  end
25
27
 
28
+ def initialize_dup(other)
29
+ super
30
+ copy_delegate(other)
31
+ end
32
+ private :initialize_dup
33
+
34
+ def initialize_clone(other, freeze: nil)
35
+ if freeze.nil?
36
+ super(other)
37
+ else
38
+ super(other, freeze: freeze)
39
+ end
40
+ copy_delegate(other)
41
+ end
42
+ private :initialize_clone
43
+
26
44
  def [](key)
27
- fetch(key) { @sanitized[key.to_s.downcase] }
45
+ @sanitized[convert_key(key)]
46
+ end
47
+
48
+ def []=(key, value)
49
+ # Store string keys so the delegated Hash and the case-insensitive
50
+ # index describe the same header set even when a caller used a symbol.
51
+ key = key.to_s
52
+ delete_underlying_key(key)
53
+ __getobj__[key] = value
54
+ @sanitized[convert_key(key)] = value
55
+ end
56
+ alias_method :store, :[]=
57
+
58
+ def merge(other)
59
+ merged = dup
60
+ merged.update(other) do |key, old, new|
61
+ block_given? ? yield(key, old, new) : new
62
+ end
63
+ merged
64
+ end
65
+
66
+ def delete(key)
67
+ value = self[key]
68
+ delete_underlying_key(key)
69
+ @sanitized.delete(convert_key(key))
70
+ value
71
+ end
72
+
73
+ def clear
74
+ __getobj__.clear
75
+ @sanitized.clear
76
+ self
77
+ end
78
+
79
+ def update(other)
80
+ other.each_pair do |key, value|
81
+ value = yield(key, self[key], value) if block_given? && include?(key)
82
+ self[key] = value
83
+ end
84
+ self
85
+ end
86
+ alias_method :merge!, :update
87
+
88
+ def replace(other)
89
+ clear
90
+ update(other)
91
+ end
92
+
93
+ def delete_if(&block)
94
+ return enum_for(:delete_if) unless block
95
+
96
+ super
97
+ rebuild_sanitized
98
+ self
99
+ end
100
+
101
+ def keep_if(&block)
102
+ return enum_for(:keep_if) unless block
103
+
104
+ super
105
+ rebuild_sanitized
106
+ self
107
+ end
108
+
109
+ def select!(&block)
110
+ return enum_for(:select!) unless block
111
+
112
+ result = super
113
+ rebuild_sanitized
114
+ result ? self : nil
115
+ end
116
+ alias_method :filter!, :select!
117
+
118
+ def reject!(&block)
119
+ return enum_for(:reject!) unless block
120
+
121
+ result = super
122
+ rebuild_sanitized
123
+ result ? self : nil
124
+ end
125
+
126
+ def shift
127
+ result = super
128
+ rebuild_sanitized
129
+ result
130
+ end
131
+
132
+ def compact!
133
+ result = super
134
+ rebuild_sanitized
135
+ result ? self : nil
136
+ end
137
+
138
+ def transform_keys!(*args, &block)
139
+ return enum_for(:transform_keys!, *args) if !block && args.empty?
140
+
141
+ super
142
+ rebuild_sanitized
143
+ self
144
+ end
145
+
146
+ def transform_values!(&block)
147
+ return enum_for(:transform_values!) unless block
148
+
149
+ super
150
+ rebuild_sanitized
151
+ self
152
+ end
153
+
154
+ def include?(key)
155
+ @sanitized.include?(convert_key(key))
156
+ end
157
+ alias_method :has_key?, :include?
158
+ alias_method :key?, :include?
159
+ alias_method :member?, :include?
160
+
161
+ def fetch(key, *extras, &block)
162
+ if block
163
+ # Hash#fetch yields the key the caller asked for, not a normalized
164
+ # one. Keep that behavior while looking up case-insensitively.
165
+ @sanitized.fetch(convert_key(key), *extras) { block.call(key) }
166
+ else
167
+ @sanitized.fetch(convert_key(key), *extras)
168
+ end
169
+ end
170
+
171
+ def dig(key, *identifiers)
172
+ @sanitized.dig(convert_key(key), *identifiers)
173
+ end
174
+
175
+ def assoc(key)
176
+ @sanitized.assoc(convert_key(key))
177
+ end
178
+
179
+ def values_at(*keys)
180
+ keys.map! { |key| convert_key(key) }
181
+ @sanitized.values_at(*keys)
182
+ end
183
+
184
+ def fetch_values(*indices, &block)
185
+ indices.map do |key|
186
+ converted = convert_key(key)
187
+ if @sanitized.key?(converted)
188
+ @sanitized[converted]
189
+ elsif block
190
+ block.call(key)
191
+ else
192
+ @sanitized.fetch(converted)
193
+ end
194
+ end
28
195
  end
29
196
 
30
197
  # Parses the raw header.
@@ -32,12 +199,11 @@ module Typhoeus
32
199
  # @example Parse header.
33
200
  # header.parse
34
201
  def parse
35
- case @raw
36
- when Hash
37
- raw.each do |k, v|
202
+ if raw.respond_to?(:each_pair)
203
+ raw.each_pair do |k, v|
38
204
  process_pair(k, v)
39
205
  end
40
- when String
206
+ elsif raw.is_a?(String)
41
207
  raw.split(/\r?\n(?!\s)/).each do |header|
42
208
  header.strip!
43
209
  next if header.empty? || header.start_with?( 'HTTP/' )
@@ -60,8 +226,9 @@ module Typhoeus
60
226
  #
61
227
  # @return [ void ]
62
228
  def process_pair(key, value)
63
- set_value(key, value, self)
64
- @sanitized[key.downcase] = self[key]
229
+ sanitized_key = convert_key(key)
230
+ set_value(sanitized_key, duplicate_value(value), @sanitized)
231
+ self[key] = @sanitized[sanitized_key]
65
232
  end
66
233
 
67
234
  # Sets value for key in specified hash
@@ -90,14 +257,67 @@ module Typhoeus
90
257
  @raw || ''
91
258
  end
92
259
 
93
- # Sets the default proc for the specified hash independent of the Ruby version.
94
- #
95
- # @return [ void ]
96
- def set_default_proc_on(hash, default_proc)
97
- if hash.respond_to?(:default_proc=)
98
- hash.default_proc = default_proc
260
+ def convert_key(key)
261
+ key.to_s.downcase
262
+ end
263
+
264
+ def duplicate_value(value)
265
+ case value
266
+ when Array
267
+ value.map { |nested| duplicate_value(nested) }
268
+ when String
269
+ value.dup
99
270
  else
100
- hash.replace(Hash.new(&default_proc).merge(hash))
271
+ value
272
+ end
273
+ end
274
+
275
+ # DelegateClass copies only the delegated hash itself. Rebuild both
276
+ # hashes so later mutations cannot reach the original header. The
277
+ # sanitized index is built from the copied hash so both stores keep
278
+ # referring to the same values, as they do on the original.
279
+ def copy_delegate(other)
280
+ copy = {}
281
+ other.__getobj__.each do |key, value|
282
+ copy[key] = duplicate_value(value)
283
+ end
284
+ __setobj__(copy)
285
+
286
+ @sanitized = {}
287
+ copy.each do |key, value|
288
+ converted = convert_key(key)
289
+ if @sanitized.key?(converted)
290
+ existing = @sanitized[converted]
291
+ existing = [existing] unless existing.is_a?(Array)
292
+ existing.concat(value.is_a?(Array) ? value : [value])
293
+ @sanitized[converted] = existing
294
+ else
295
+ @sanitized[converted] = value
296
+ end
297
+ end
298
+ end
299
+
300
+ def delete_underlying_key(key)
301
+ converted = convert_key(key)
302
+ __getobj__.keys.each do |existing_key|
303
+ __getobj__.delete(existing_key) if convert_key(existing_key) == converted
304
+ end
305
+ end
306
+
307
+ # Rebuild the case-insensitive index after a Hash mutator that cannot be
308
+ # expressed through #[]=.
309
+ def rebuild_sanitized
310
+ @sanitized = {}
311
+ __getobj__.each do |key, value|
312
+ converted = convert_key(key)
313
+ if @sanitized.key?(converted)
314
+ existing = @sanitized[converted]
315
+ existing = [existing] unless existing.is_a?(Array)
316
+ existing.concat(value.is_a?(Array) ? value : [value])
317
+ @sanitized[converted] = existing
318
+ else
319
+ @sanitized[converted] = value
320
+ end
101
321
  end
102
322
  end
103
323
  end