rest-client 2.0.2 → 2.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
data/history.md CHANGED
@@ -1,3 +1,36 @@
1
+ # 2.1.0.rc1
2
+
3
+ - Add a dependency on http-accept for parsing Content-Type charset headers.
4
+ This works around a bad memory leak introduced in Ruby 2.4.x (the leak is
5
+ probably a bug in MRI). (#615)
6
+ - Use mime/types/columnar from mime-types 2.6.1+, which is leaner in memory
7
+ usage than the older storage model of mime-types. (#393)
8
+ - Add `:log` option to individual requests. This allows users to set a log on a
9
+ per-request / per-resource basis instead of the kludgy global log. (#538)
10
+ - Log request duration by tracking request start and end times. Make
11
+ `log_response` a method on the Response object, and ensure the `size` method
12
+ works on RawResponse objects. (#126)
13
+ - `# => 200 OK | text/html 1270 bytes, 0.08s`
14
+ - Drop custom handling of compression and use built-in Net::HTTP support for
15
+ supported Content-Encodings like gzip and deflate. Don't set any explicit
16
+ `Accept-Encoding` header, rely instead on Net::HTTP defaults. (#597)
17
+ - Note: this changes behavior for compressed responses when using
18
+ `:raw_response => true`. Previously the raw response would not have been
19
+ uncompressed by rest-client, but now Net::HTTP will uncompress it.
20
+ - The previous fix to avoid having Netrc username/password override an
21
+ Authorization header was case-sensitive and incomplete. Fix this by
22
+ respecting existing Authorization headers, regardless of letter case. (#550)
23
+ - Handle ParamsArray payloads. Previously, rest-client would silently drop a
24
+ ParamsArray passed as the payload. Instead, automatically use
25
+ Payload::Multipart if the ParamsArray contains a file handle, or use
26
+ Payload::UrlEncoded if it doesn't. (#508)
27
+ - Gracefully handle Payload objects (Payload::Base or subclasses) that are
28
+ passed as a payload argument. Previously, `Payload.generate` would wrap a
29
+ Payload object in Payload::Streamed, creating a pointlessly nested payload.
30
+ Also add a `closed?` method to Payload objects, and don't error in
31
+ `short_inspect` if `size` returns nil. (#603)
32
+ - Test with an image in the public domain to avoid licensing complexity. (#607)
33
+
1
34
  # 2.0.2
2
35
 
3
36
  - Suppress the header override warning introduced in 2.0.1 if the value is the
@@ -2,7 +2,6 @@ require 'net/http'
2
2
  require 'openssl'
3
3
  require 'stringio'
4
4
  require 'uri'
5
- require 'zlib'
6
5
 
7
6
  require File.dirname(__FILE__) + '/restclient/version'
8
7
  require File.dirname(__FILE__) + '/restclient/platform'
@@ -5,12 +5,27 @@ module RestClient
5
5
 
6
6
  module AbstractResponse
7
7
 
8
- attr_reader :net_http_res, :request
8
+ attr_reader :net_http_res, :request, :start_time, :end_time, :duration
9
9
 
10
10
  def inspect
11
11
  raise NotImplementedError.new('must override in subclass')
12
12
  end
13
13
 
14
+ # Logger from the request, potentially nil.
15
+ def log
16
+ request.log
17
+ end
18
+
19
+ def log_response
20
+ return unless log
21
+
22
+ code = net_http_res.code
23
+ res_name = net_http_res.class.to_s.gsub(/\ANet::HTTP/, '')
24
+ content_type = (net_http_res['Content-type'] || '').gsub(/;.*\z/, '')
25
+
26
+ log << "# => #{code} #{res_name} | #{content_type} #{size} bytes, #{sprintf('%.2f', duration)}s\n"
27
+ end
28
+
14
29
  # HTTP status code
15
30
  def code
16
31
  @code ||= @net_http_res.code.to_i
@@ -31,9 +46,20 @@ module RestClient
31
46
  @raw_headers ||= @net_http_res.to_hash
32
47
  end
33
48
 
34
- def response_set_vars(net_http_res, request)
49
+ # @param [Net::HTTPResponse] net_http_res
50
+ # @param [RestClient::Request] request
51
+ # @param [Time] start_time
52
+ def response_set_vars(net_http_res, request, start_time)
35
53
  @net_http_res = net_http_res
36
54
  @request = request
55
+ @start_time = start_time
56
+ @end_time = Time.now
57
+
58
+ if @start_time
59
+ @duration = @end_time - @start_time
60
+ else
61
+ @duration = nil
62
+ end
37
63
 
38
64
  # prime redirection history
39
65
  history
@@ -148,7 +148,7 @@ module RestClient
148
148
  end
149
149
 
150
150
  # Compatibility
151
- class ExceptionWithResponse < Exception
151
+ class ExceptionWithResponse < RestClient::Exception
152
152
  end
153
153
 
154
154
  # The request failed with an error code not managed by the code
@@ -228,14 +228,14 @@ module RestClient
228
228
  # The server broke the connection prior to the request completing. Usually
229
229
  # this means it crashed, or sometimes that your network connection was
230
230
  # severed before it could complete.
231
- class ServerBrokeConnection < Exception
231
+ class ServerBrokeConnection < RestClient::Exception
232
232
  def initialize(message = 'Server broke connection')
233
233
  super nil, nil
234
234
  self.message = message
235
235
  end
236
236
  end
237
237
 
238
- class SSLCertificateNotVerified < Exception
238
+ class SSLCertificateNotVerified < RestClient::Exception
239
239
  def initialize(message = 'SSL certificate not verified')
240
240
  super nil, nil
241
241
  self.message = message
@@ -2,14 +2,22 @@ require 'tempfile'
2
2
  require 'securerandom'
3
3
  require 'stringio'
4
4
 
5
- require 'mime/types'
5
+ begin
6
+ # Use mime/types/columnar if available, for reduced memory usage
7
+ require 'mime/types/columnar'
8
+ rescue LoadError
9
+ require 'mime/types'
10
+ end
6
11
 
7
12
  module RestClient
8
13
  module Payload
9
14
  extend self
10
15
 
11
16
  def generate(params)
12
- if params.is_a?(String)
17
+ if params.is_a?(RestClient::Payload::Base)
18
+ # pass through Payload objects unchanged
19
+ params
20
+ elsif params.is_a?(String)
13
21
  Base.new(params)
14
22
  elsif params.is_a?(Hash)
15
23
  if params.delete(:multipart) == true || has_file?(params)
@@ -17,6 +25,12 @@ module RestClient
17
25
  else
18
26
  UrlEncoded.new(params)
19
27
  end
28
+ elsif params.is_a?(ParamsArray)
29
+ if _has_file?(params)
30
+ Multipart.new(params)
31
+ else
32
+ UrlEncoded.new(params)
33
+ end
20
34
  elsif params.respond_to?(:read)
21
35
  Streamed.new(params)
22
36
  else
@@ -76,12 +90,20 @@ module RestClient
76
90
  @stream.close unless @stream.closed?
77
91
  end
78
92
 
93
+ def closed?
94
+ @stream.closed?
95
+ end
96
+
79
97
  def to_s_inspect
80
98
  to_s.inspect
81
99
  end
82
100
 
83
101
  def short_inspect
84
- (size > 500 ? "#{size} byte(s) length" : to_s_inspect)
102
+ if size && size > 500
103
+ "#{size} byte(s) length"
104
+ else
105
+ to_s_inspect
106
+ end
85
107
  end
86
108
 
87
109
  end
@@ -99,6 +121,9 @@ module RestClient
99
121
  end
100
122
  end
101
123
 
124
+ # TODO (breaks compatibility): ought to use mime_for() to autodetect the
125
+ # Content-Type for stream objects that have a filename.
126
+
102
127
  alias :length :size
103
128
  end
104
129
 
@@ -13,25 +13,36 @@ module RestClient
13
13
 
14
14
  include AbstractResponse
15
15
 
16
- attr_reader :file, :request
16
+ attr_reader :file, :request, :start_time, :end_time
17
17
 
18
18
  def inspect
19
19
  "<RestClient::RawResponse @code=#{code.inspect}, @file=#{file.inspect}, @request=#{request.inspect}>"
20
20
  end
21
21
 
22
- def initialize(tempfile, net_http_res, request)
23
- @net_http_res = net_http_res
22
+ # @param [Tempfile] tempfile The temporary file containing the body
23
+ # @param [Net::HTTPResponse] net_http_res
24
+ # @param [RestClient::Request] request
25
+ # @param [Time] start_time
26
+ def initialize(tempfile, net_http_res, request, start_time=nil)
24
27
  @file = tempfile
25
- @request = request
28
+
29
+ # reopen the tempfile so we can read it
30
+ @file.open
31
+
32
+ response_set_vars(net_http_res, request, start_time)
26
33
  end
27
34
 
28
35
  def to_s
29
- @file.open
36
+ body
37
+ end
38
+
39
+ def body
40
+ @file.rewind
30
41
  @file.read
31
42
  end
32
43
 
33
44
  def size
34
- File.size file
45
+ file.size
35
46
  end
36
47
 
37
48
  end
@@ -1,9 +1,15 @@
1
1
  require 'tempfile'
2
- require 'mime/types'
3
2
  require 'cgi'
4
3
  require 'netrc'
5
4
  require 'set'
6
5
 
6
+ begin
7
+ # Use mime/types/columnar if available, for reduced memory usage
8
+ require 'mime/types/columnar'
9
+ rescue LoadError
10
+ require 'mime/types'
11
+ end
12
+
7
13
  module RestClient
8
14
  # This class is used internally by RestClient to send the request, but you can also
9
15
  # call it directly if you'd like to use a method not supported by the
@@ -92,6 +98,12 @@ module RestClient
92
98
  @block_response = args[:block_response]
93
99
  @raw_response = args[:raw_response] || false
94
100
 
101
+ @stream_log_percent = args[:stream_log_percent] || 10
102
+ if @stream_log_percent <= 0 || @stream_log_percent > 100
103
+ raise ArgumentError.new(
104
+ "Invalid :stream_log_percent #{@stream_log_percent.inspect}")
105
+ end
106
+
95
107
  @proxy = args.fetch(:proxy) if args.include?(:proxy)
96
108
 
97
109
  @ssl_opts = {}
@@ -131,9 +143,10 @@ module RestClient
131
143
  end
132
144
  end
133
145
 
134
- @tf = nil # If you are a raw request, this is your tempfile
146
+ @log = args[:log]
135
147
  @max_redirects = args[:max_redirects] || 10
136
148
  @processed_headers = make_headers headers
149
+ @processed_headers_lowercase = Hash[@processed_headers.map {|k, v| [k.downcase, v]}]
137
150
  @args = args
138
151
 
139
152
  @before_execution_proc = args[:before_execution_proc]
@@ -356,6 +369,13 @@ module RestClient
356
369
  # - headers from the payload object (e.g. Content-Type, Content-Lenth)
357
370
  # - cookie headers from #make_cookie_header
358
371
  #
372
+ # BUG: stringify_headers does not alter the capitalization of headers that
373
+ # are passed as strings, it only normalizes those passed as symbols. This
374
+ # behavior will probably remain for a while for compatibility, but it means
375
+ # that the warnings that attempt to detect accidental header overrides may
376
+ # not always work.
377
+ # https://github.com/rest-client/rest-client/issues/599
378
+ #
359
379
  # @param [Hash] user_headers User-provided headers to include
360
380
  #
361
381
  # @return [Hash<String, String>] A hash of HTTP headers => values
@@ -493,24 +513,6 @@ module RestClient
493
513
  cert_store
494
514
  end
495
515
 
496
- def self.decode content_encoding, body
497
- if (!body) || body.empty?
498
- body
499
- elsif content_encoding == 'gzip'
500
- Zlib::GzipReader.new(StringIO.new(body)).read
501
- elsif content_encoding == 'deflate'
502
- begin
503
- Zlib::Inflate.new.inflate body
504
- rescue Zlib::DataError
505
- # No luck with Zlib decompression. Let's try with raw deflate,
506
- # like some broken web servers do.
507
- Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate body
508
- end
509
- else
510
- body
511
- end
512
- end
513
-
514
516
  def redacted_uri
515
517
  if uri.password
516
518
  sanitized_uri = uri.dup
@@ -525,30 +527,29 @@ module RestClient
525
527
  redacted_uri.to_s
526
528
  end
527
529
 
530
+ # Default to the global logger if there's not a request-specific one
531
+ def log
532
+ @log || RestClient.log
533
+ end
534
+
528
535
  def log_request
529
- return unless RestClient.log
536
+ return unless log
530
537
 
531
538
  out = []
532
539
 
533
540
  out << "RestClient.#{method} #{redacted_url.inspect}"
534
541
  out << payload.short_inspect if payload
535
542
  out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
536
- RestClient.log << out.join(', ') + "\n"
537
- end
538
-
539
- def log_response res
540
- return unless RestClient.log
541
-
542
- size = if @raw_response
543
- File.size(@tf.path)
544
- else
545
- res.body.nil? ? 0 : res.body.size
546
- end
547
-
548
- RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
543
+ log << out.join(', ') + "\n"
549
544
  end
550
545
 
551
546
  # Return a hash of headers whose keys are capitalized strings
547
+ #
548
+ # BUG: stringify_headers does not fix the capitalization of headers that
549
+ # are already Strings. Leaving this behavior as is for now for
550
+ # backwards compatibility.
551
+ # https://github.com/rest-client/rest-client/issues/599
552
+ #
552
553
  def stringify_headers headers
553
554
  headers.inject({}) do |result, (key, value)|
554
555
  if key.is_a? Symbol
@@ -573,10 +574,13 @@ module RestClient
573
574
  end
574
575
  end
575
576
 
577
+ # Default headers set by RestClient. In addition to these headers, servers
578
+ # will receive headers set by Net::HTTP, such as Accept-Encoding and Host.
579
+ #
580
+ # @return [Hash<Symbol, String>]
576
581
  def default_headers
577
582
  {
578
583
  :accept => '*/*',
579
- :accept_encoding => 'gzip, deflate',
580
584
  :user_agent => RestClient::Platform.default_user_agent,
581
585
  }
582
586
  end
@@ -712,6 +716,9 @@ module RestClient
712
716
 
713
717
  log_request
714
718
 
719
+ start_time = Time.now
720
+ tempfile = nil
721
+
715
722
  net.start do |http|
716
723
  established_connection = true
717
724
 
@@ -719,10 +726,16 @@ module RestClient
719
726
  net_http_do_request(http, req, payload, &@block_response)
720
727
  else
721
728
  res = net_http_do_request(http, req, payload) { |http_response|
722
- fetch_body(http_response)
729
+ if @raw_response
730
+ # fetch body into tempfile
731
+ tempfile = fetch_body_to_tempfile(http_response)
732
+ else
733
+ # fetch body
734
+ http_response.read_body
735
+ end
736
+ http_response
723
737
  }
724
- log_response res
725
- process_result res, & block
738
+ process_result(res, start_time, tempfile, &block)
726
739
  end
727
740
  end
728
741
  rescue EOFError
@@ -762,47 +775,56 @@ module RestClient
762
775
  end
763
776
 
764
777
  def setup_credentials(req)
765
- req.basic_auth(user, password) if user && !headers.has_key?("Authorization")
778
+ if user && !@processed_headers_lowercase.include?('authorization')
779
+ req.basic_auth(user, password)
780
+ end
766
781
  end
767
782
 
768
- def fetch_body(http_response)
769
- if @raw_response
770
- # Taken from Chef, which as in turn...
771
- # Stolen from http://www.ruby-forum.com/topic/166423
772
- # Kudos to _why!
773
- @tf = Tempfile.new('rest-client.')
774
- @tf.binmode
775
- size, total = 0, http_response['Content-Length'].to_i
776
- http_response.read_body do |chunk|
777
- @tf.write chunk
778
- size += chunk.size
779
- if RestClient.log
780
- if size == 0
781
- RestClient.log << "%s %s done (0 length file)\n" % [@method, @url]
782
- elsif total == 0
783
- RestClient.log << "%s %s (zero content length)\n" % [@method, @url]
784
- else
785
- RestClient.log << "%s %s %d%% done (%d of %d)\n" % [@method, @url, (size * 100) / total, size, total]
783
+ def fetch_body_to_tempfile(http_response)
784
+ # Taken from Chef, which as in turn...
785
+ # Stolen from http://www.ruby-forum.com/topic/166423
786
+ # Kudos to _why!
787
+ tf = Tempfile.new('rest-client.')
788
+ tf.binmode
789
+
790
+ size = 0
791
+ total = http_response['Content-Length'].to_i
792
+ stream_log_bucket = nil
793
+
794
+ http_response.read_body do |chunk|
795
+ tf.write chunk
796
+ size += chunk.size
797
+ if log
798
+ if total == 0
799
+ log << "streaming %s %s (%d of unknown) [0 Content-Length]\n" % [@method.upcase, @url, size]
800
+ else
801
+ percent = (size * 100) / total
802
+ current_log_bucket, _ = percent.divmod(@stream_log_percent)
803
+ if current_log_bucket != stream_log_bucket
804
+ stream_log_bucket = current_log_bucket
805
+ log << "streaming %s %s %d%% done (%d of %d)\n" % [@method.upcase, @url, (size * 100) / total, size, total]
786
806
  end
787
807
  end
788
808
  end
789
- @tf.close
790
- @tf
791
- else
792
- http_response.read_body
793
809
  end
794
- http_response
810
+ tf.close
811
+ tf
795
812
  end
796
813
 
797
- def process_result res, & block
814
+ # @param res The Net::HTTP response object
815
+ # @param start_time [Time] Time of request start
816
+ def process_result(res, start_time, tempfile=nil, &block)
798
817
  if @raw_response
799
- # We don't decode raw requests
800
- response = RawResponse.new(@tf, res, self)
818
+ unless tempfile
819
+ raise ArgumentError.new('tempfile is required')
820
+ end
821
+ response = RawResponse.new(tempfile, res, self, start_time)
801
822
  else
802
- decoded = Request.decode(res['content-encoding'], res.body)
803
- response = Response.create(decoded, res, self)
823
+ response = Response.create(res.body, res, self, start_time)
804
824
  end
805
825
 
826
+ response.log_response
827
+
806
828
  if block_given?
807
829
  block.call(response, self, res, & block)
808
830
  else