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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.mailmap +10 -0
- data/.rubocop +2 -0
- data/.rubocop-disables.yml +27 -24
- data/.rubocop.yml +5 -0
- data/.travis.yml +2 -1
- data/AUTHORS +8 -0
- data/README.md +119 -7
- data/Rakefile +12 -4
- data/history.md +33 -0
- data/lib/restclient.rb +0 -1
- data/lib/restclient/abstract_response.rb +28 -2
- data/lib/restclient/exceptions.rb +3 -3
- data/lib/restclient/payload.rb +28 -3
- data/lib/restclient/raw_response.rb +17 -6
- data/lib/restclient/request.rb +89 -67
- data/lib/restclient/resource.rb +16 -6
- data/lib/restclient/response.rb +14 -4
- data/lib/restclient/utils.rb +47 -8
- data/lib/restclient/version.rb +2 -2
- data/rest-client.gemspec +1 -0
- data/spec/ISS.jpg +0 -0
- data/spec/helpers.rb +37 -5
- data/spec/integration/httpbin_spec.rb +41 -0
- data/spec/integration/integration_spec.rb +0 -7
- data/spec/unit/abstract_response_spec.rb +7 -7
- data/spec/unit/payload_spec.rb +51 -19
- data/spec/unit/raw_response_spec.rb +6 -2
- data/spec/unit/request2_spec.rb +8 -8
- data/spec/unit/request_spec.rb +51 -63
- data/spec/unit/resource_spec.rb +7 -7
- data/spec/unit/response_spec.rb +33 -22
- data/spec/unit/restclient_spec.rb +3 -2
- data/spec/unit/utils_spec.rb +10 -10
- metadata +29 -7
- data/spec/unit/master_shake.jpg +0 -0
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
|
data/lib/restclient.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/restclient/payload.rb
CHANGED
@@ -2,14 +2,22 @@ require 'tempfile'
|
|
2
2
|
require 'securerandom'
|
3
3
|
require 'stringio'
|
4
4
|
|
5
|
-
|
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?(
|
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
|
-
|
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
|
-
|
23
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
45
|
+
file.size
|
35
46
|
end
|
36
47
|
|
37
48
|
end
|
data/lib/restclient/request.rb
CHANGED
@@ -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
|
-
@
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
778
|
+
if user && !@processed_headers_lowercase.include?('authorization')
|
779
|
+
req.basic_auth(user, password)
|
780
|
+
end
|
766
781
|
end
|
767
782
|
|
768
|
-
def
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
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
|
-
|
810
|
+
tf.close
|
811
|
+
tf
|
795
812
|
end
|
796
813
|
|
797
|
-
|
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
|
-
|
800
|
-
|
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
|
-
|
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
|