rest-client 2.0.2-x64-mingw32 → 2.1.0.rc1-x64-mingw32

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/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