httparty 0.8.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of httparty might be problematic. Click here for more details.
- data/.gitignore +1 -0
- data/.travis.yml +8 -0
- data/Gemfile +8 -1
- data/Guardfile +16 -0
- data/History +34 -0
- data/README.md +79 -0
- data/bin/httparty +10 -4
- data/cucumber.yml +1 -1
- data/examples/crack.rb +19 -0
- data/examples/headers_and_user_agents.rb +6 -0
- data/examples/nokogiri_html_parser.rb +22 -0
- data/features/steps/remote_service_steps.rb +1 -1
- data/httparty.gemspec +2 -2
- data/lib/httparty.rb +81 -30
- data/lib/httparty/connection_adapter.rb +116 -0
- data/lib/httparty/cookie_hash.rb +2 -2
- data/lib/httparty/core_extensions.rb +23 -0
- data/lib/httparty/hash_conversions.rb +1 -1
- data/lib/httparty/module_inheritable_attributes.rb +11 -1
- data/lib/httparty/net_digest_auth.rb +20 -7
- data/lib/httparty/parser.rb +10 -6
- data/lib/httparty/request.rb +34 -51
- data/lib/httparty/response.rb +17 -40
- data/lib/httparty/response/headers.rb +31 -0
- data/lib/httparty/version.rb +1 -1
- data/spec/httparty/connection_adapter_spec.rb +206 -0
- data/spec/httparty/cookie_hash_spec.rb +3 -4
- data/spec/httparty/net_digest_auth_spec.rb +33 -11
- data/spec/httparty/parser_spec.rb +16 -0
- data/spec/httparty/request_spec.rb +78 -101
- data/spec/httparty/response_spec.rb +41 -20
- data/spec/httparty/ssl_spec.rb +13 -5
- data/spec/httparty_spec.rb +84 -2
- data/spec/spec.opts +0 -1
- data/spec/spec_helper.rb +8 -0
- data/spec/support/ssl_test_helper.rb +28 -6
- data/spec/support/ssl_test_server.rb +19 -8
- data/spec/support/stub_response.rb +13 -0
- metadata +53 -61
- data/README.rdoc +0 -54
@@ -0,0 +1,116 @@
|
|
1
|
+
module HTTParty
|
2
|
+
# Default connection adapter that returns a new Net::HTTP each time
|
3
|
+
#
|
4
|
+
# == Custom Connection Factories
|
5
|
+
#
|
6
|
+
# If you like to implement your own connection adapter, subclassing
|
7
|
+
# HTTPParty::ConnectionAdapter will make it easier. Just override
|
8
|
+
# the #connection method. The uri and options attributes will have
|
9
|
+
# all the info you need to construct your http connection. Whatever
|
10
|
+
# you return from your connection method needs to adhere to the
|
11
|
+
# Net::HTTP interface as this is what HTTParty expects.
|
12
|
+
#
|
13
|
+
# @example log the uri and options
|
14
|
+
# class LoggingConnectionAdapter < HTTParty::ConnectionAdapter
|
15
|
+
# def connection
|
16
|
+
# puts uri
|
17
|
+
# puts options
|
18
|
+
# Net::HTTP.new(uri)
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# @example count number of http calls
|
23
|
+
# class CountingConnectionAdapter < HTTParty::ConnectionAdapter
|
24
|
+
# @@count = 0
|
25
|
+
#
|
26
|
+
# self.count
|
27
|
+
# @@count
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# def connection
|
31
|
+
# self.count += 1
|
32
|
+
# super
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# === Configuration
|
37
|
+
# There is lots of configuration data available for your connection adapter
|
38
|
+
# in the #options attribute. It is up to you to interpret them within your
|
39
|
+
# connection adapter. Take a look at the implementation of
|
40
|
+
# HTTParty::ConnectionAdapter#connection for examples of how they are used.
|
41
|
+
# Something are probably interesting are as follows:
|
42
|
+
# * :+timeout+: timeout in seconds
|
43
|
+
# * :+debug_output+: see HTTParty::ClassMethods.debug_output.
|
44
|
+
# * :+pem+: contains pem data. see HTTParty::ClassMethods.pem.
|
45
|
+
# * :+ssl_ca_file+: see HTTParty::ClassMethods.ssl_ca_file.
|
46
|
+
# * :+ssl_ca_path+: see HTTParty::ClassMethods.ssl_ca_path.
|
47
|
+
# * :+connection_adapter_options+: contains the hash your passed to HTTParty.connection_adapter when you configured your connection adapter
|
48
|
+
class ConnectionAdapter
|
49
|
+
|
50
|
+
def self.call(uri, options)
|
51
|
+
new(uri, options).connection
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_reader :uri, :options
|
55
|
+
|
56
|
+
def initialize(uri, options={})
|
57
|
+
raise ArgumentError, "uri must be a URI, not a #{uri.class}" unless uri.kind_of? URI
|
58
|
+
|
59
|
+
@uri = uri
|
60
|
+
@options = options
|
61
|
+
end
|
62
|
+
|
63
|
+
def connection
|
64
|
+
http = Net::HTTP.new(uri.host, uri.port, options[:http_proxyaddr], options[:http_proxyport], options[:http_proxyuser], options[:http_proxypass])
|
65
|
+
|
66
|
+
http.use_ssl = ssl_implied?(uri)
|
67
|
+
|
68
|
+
attach_ssl_certificates(http, options)
|
69
|
+
|
70
|
+
if options[:timeout] && (options[:timeout].is_a?(Integer) || options[:timeout].is_a?(Float))
|
71
|
+
http.open_timeout = options[:timeout]
|
72
|
+
http.read_timeout = options[:timeout]
|
73
|
+
end
|
74
|
+
|
75
|
+
if options[:debug_output]
|
76
|
+
http.set_debug_output(options[:debug_output])
|
77
|
+
end
|
78
|
+
|
79
|
+
return http
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
def ssl_implied?(uri)
|
84
|
+
uri.port == 443 || uri.instance_of?(URI::HTTPS)
|
85
|
+
end
|
86
|
+
|
87
|
+
def attach_ssl_certificates(http, options)
|
88
|
+
if http.use_ssl?
|
89
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
90
|
+
|
91
|
+
# Client certificate authentication
|
92
|
+
if options[:pem]
|
93
|
+
http.cert = OpenSSL::X509::Certificate.new(options[:pem])
|
94
|
+
http.key = OpenSSL::PKey::RSA.new(options[:pem], options[:pem_password])
|
95
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
96
|
+
end
|
97
|
+
|
98
|
+
# SSL certificate authority file and/or directory
|
99
|
+
if options[:ssl_ca_file]
|
100
|
+
http.ca_file = options[:ssl_ca_file]
|
101
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
102
|
+
end
|
103
|
+
|
104
|
+
if options[:ssl_ca_path]
|
105
|
+
http.ca_path = options[:ssl_ca_path]
|
106
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
107
|
+
end
|
108
|
+
|
109
|
+
# This is only Ruby 1.9+
|
110
|
+
if options[:ssl_version] && http.respond_to?(:ssl_version=)
|
111
|
+
http.ssl_version = options[:ssl_version]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/httparty/cookie_hash.rb
CHANGED
@@ -6,4 +6,27 @@ module HTTParty
|
|
6
6
|
instance_methods.each { |m| undef_method m unless m =~ /^__|instance_eval/ }
|
7
7
|
end
|
8
8
|
end
|
9
|
+
|
10
|
+
unless defined?(Net::HTTP::Patch)
|
11
|
+
class Net::HTTP
|
12
|
+
def patch(path, data, initheader = nil, dest = nil, &block) #:nodoc:
|
13
|
+
res = nil
|
14
|
+
request(Patch.new(path, initheader), data) {|r|
|
15
|
+
r.read_body dest, &block
|
16
|
+
res = r
|
17
|
+
}
|
18
|
+
unless @newimpl
|
19
|
+
res.value
|
20
|
+
return res, res.body
|
21
|
+
end
|
22
|
+
res
|
23
|
+
end
|
24
|
+
|
25
|
+
class Patch < Net::HTTPRequest
|
26
|
+
METHOD = 'PATCH'
|
27
|
+
REQUEST_HAS_BODY = true
|
28
|
+
RESPONSE_HAS_BODY = true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
9
32
|
end
|
@@ -4,6 +4,16 @@ module HTTParty
|
|
4
4
|
base.extend(ClassMethods)
|
5
5
|
end
|
6
6
|
|
7
|
+
# borrowed from Rails 3.2 ActiveSupport
|
8
|
+
def self.hash_deep_dup(h)
|
9
|
+
duplicate = h.dup
|
10
|
+
duplicate.each_pair do |k,v|
|
11
|
+
tv = duplicate[k]
|
12
|
+
duplicate[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? hash_deep_dup(tv) : v
|
13
|
+
end
|
14
|
+
duplicate
|
15
|
+
end
|
16
|
+
|
7
17
|
module ClassMethods #:nodoc:
|
8
18
|
def mattr_inheritable(*args)
|
9
19
|
@mattr_inheritable_attrs ||= [:mattr_inheritable_attrs]
|
@@ -22,7 +32,7 @@ module HTTParty
|
|
22
32
|
if instance_variable_get(ivar).respond_to?(:merge)
|
23
33
|
method = <<-EOM
|
24
34
|
def self.#{inheritable_attribute}
|
25
|
-
#{ivar} = superclass.#{inheritable_attribute}.merge #{ivar}
|
35
|
+
#{ivar} = superclass.#{inheritable_attribute}.merge ModuleInheritableAttributes.hash_deep_dup(#{ivar})
|
26
36
|
end
|
27
37
|
EOM
|
28
38
|
subclass.class_eval method
|
@@ -20,15 +20,24 @@ module Net
|
|
20
20
|
|
21
21
|
def authorization_header
|
22
22
|
@cnonce = md5(random)
|
23
|
-
header = [
|
23
|
+
header = [
|
24
|
+
%Q(Digest username="#{@username}"),
|
24
25
|
%Q(realm="#{@response['realm']}"),
|
25
26
|
%Q(nonce="#{@response['nonce']}"),
|
26
27
|
%Q(uri="#{@path}"),
|
27
|
-
%Q(response="#{request_digest}")
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
28
|
+
%Q(response="#{request_digest}"),
|
29
|
+
]
|
30
|
+
|
31
|
+
if qop_present?
|
32
|
+
fields = [
|
33
|
+
%Q(cnonce="#{@cnonce}"),
|
34
|
+
%Q(qop="#{@response['qop']}"),
|
35
|
+
%Q(nc="00000001")
|
36
|
+
]
|
37
|
+
fields.each { |field| header << field }
|
38
|
+
end
|
39
|
+
|
40
|
+
header << %Q(opaque="#{@response['opaque']}") if opaque_present?
|
32
41
|
header
|
33
42
|
end
|
34
43
|
|
@@ -41,6 +50,10 @@ module Net
|
|
41
50
|
params
|
42
51
|
end
|
43
52
|
|
53
|
+
def opaque_present?
|
54
|
+
@response.has_key?('opaque') and not @response['opaque'].empty?
|
55
|
+
end
|
56
|
+
|
44
57
|
def qop_present?
|
45
58
|
@response.has_key?('qop') and not @response['qop'].empty?
|
46
59
|
end
|
@@ -51,7 +64,7 @@ module Net
|
|
51
64
|
|
52
65
|
def request_digest
|
53
66
|
a = [md5(a1), @response['nonce'], md5(a2)]
|
54
|
-
a.insert(2, "
|
67
|
+
a.insert(2, "00000001", @cnonce, @response['qop']) if qop_present?
|
55
68
|
md5(a.join(":"))
|
56
69
|
end
|
57
70
|
|
data/lib/httparty/parser.rb
CHANGED
@@ -93,12 +93,11 @@ module HTTParty
|
|
93
93
|
@body = body
|
94
94
|
@format = format
|
95
95
|
end
|
96
|
-
private_class_method :new
|
97
96
|
|
98
97
|
# @return [Object] the parsed body
|
99
|
-
# @return [nil] when the response body is nil
|
98
|
+
# @return [nil] when the response body is nil, an empty string, spaces only or "null"
|
100
99
|
def parse
|
101
|
-
return nil if body.nil? || body.empty?
|
100
|
+
return nil if body.nil? || body.strip.empty? || body == "null"
|
102
101
|
if supports_format?
|
103
102
|
parse_supported_format
|
104
103
|
else
|
@@ -113,7 +112,12 @@ module HTTParty
|
|
113
112
|
end
|
114
113
|
|
115
114
|
def json
|
116
|
-
|
115
|
+
# https://github.com/sferik/rails/commit/5e62670131dfa1718eaf21ff8dd3371395a5f1cc
|
116
|
+
if MultiJson.respond_to?(:adapter)
|
117
|
+
MultiJson.load(body)
|
118
|
+
else
|
119
|
+
MultiJson.decode(body)
|
120
|
+
end
|
117
121
|
end
|
118
122
|
|
119
123
|
def yaml
|
@@ -134,8 +138,8 @@ module HTTParty
|
|
134
138
|
|
135
139
|
def parse_supported_format
|
136
140
|
send(format)
|
137
|
-
rescue NoMethodError
|
138
|
-
raise NotImplementedError, "#{self.class.name} has not implemented a parsing method for the #{format.inspect} format."
|
141
|
+
rescue NoMethodError => e
|
142
|
+
raise NotImplementedError, "#{self.class.name} has not implemented a parsing method for the #{format.inspect} format.", e.backtrace
|
139
143
|
end
|
140
144
|
end
|
141
145
|
end
|
data/lib/httparty/request.rb
CHANGED
@@ -3,6 +3,7 @@ module HTTParty
|
|
3
3
|
SupportedHTTPMethods = [
|
4
4
|
Net::HTTP::Get,
|
5
5
|
Net::HTTP::Post,
|
6
|
+
Net::HTTP::Patch,
|
6
7
|
Net::HTTP::Put,
|
7
8
|
Net::HTTP::Delete,
|
8
9
|
Net::HTTP::Head,
|
@@ -32,7 +33,8 @@ module HTTParty
|
|
32
33
|
:limit => o.delete(:no_follow) ? 1 : 5,
|
33
34
|
:default_params => {},
|
34
35
|
:follow_redirects => true,
|
35
|
-
:parser => Parser
|
36
|
+
:parser => Parser,
|
37
|
+
:connection_adapter => ConnectionAdapter
|
36
38
|
}.merge(o)
|
37
39
|
end
|
38
40
|
|
@@ -67,60 +69,36 @@ module HTTParty
|
|
67
69
|
options[:parser]
|
68
70
|
end
|
69
71
|
|
70
|
-
def
|
71
|
-
|
72
|
-
setup_raw_request
|
73
|
-
self.last_response = http.request(@raw_request)
|
74
|
-
handle_deflation
|
75
|
-
handle_response
|
72
|
+
def connection_adapter
|
73
|
+
options[:connection_adapter]
|
76
74
|
end
|
77
75
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
76
|
+
def perform(&block)
|
77
|
+
validate
|
78
|
+
setup_raw_request
|
79
|
+
chunked_body = nil
|
83
80
|
|
84
|
-
|
85
|
-
if
|
86
|
-
|
87
|
-
http.key = OpenSSL::PKey::RSA.new(options[:pem], options[:pem_password])
|
88
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
89
|
-
end
|
81
|
+
self.last_response = http.request(@raw_request) do |http_response|
|
82
|
+
if block
|
83
|
+
chunks = []
|
90
84
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
end
|
85
|
+
http_response.read_body do |fragment|
|
86
|
+
chunks << fragment
|
87
|
+
block.call(fragment)
|
88
|
+
end
|
96
89
|
|
97
|
-
|
98
|
-
http.ca_path = options[:ssl_ca_path]
|
99
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
90
|
+
chunked_body = chunks.join
|
100
91
|
end
|
101
92
|
end
|
102
|
-
end
|
103
|
-
|
104
|
-
def http
|
105
|
-
http = Net::HTTP.new(uri.host, uri.port, options[:http_proxyaddr], options[:http_proxyport])
|
106
|
-
http.use_ssl = ssl_implied?
|
107
|
-
|
108
|
-
if options[:timeout] && (options[:timeout].is_a?(Integer) || options[:timeout].is_a?(Float))
|
109
|
-
http.open_timeout = options[:timeout]
|
110
|
-
http.read_timeout = options[:timeout]
|
111
|
-
end
|
112
|
-
|
113
|
-
attach_ssl_certificates(http)
|
114
|
-
|
115
|
-
if options[:debug_output]
|
116
|
-
http.set_debug_output(options[:debug_output])
|
117
|
-
end
|
118
93
|
|
119
|
-
|
94
|
+
handle_deflation
|
95
|
+
handle_response(chunked_body)
|
120
96
|
end
|
121
97
|
|
122
|
-
|
123
|
-
|
98
|
+
private
|
99
|
+
|
100
|
+
def http
|
101
|
+
connection_adapter.call(uri, options)
|
124
102
|
end
|
125
103
|
|
126
104
|
def body
|
@@ -160,7 +138,10 @@ module HTTParty
|
|
160
138
|
end
|
161
139
|
|
162
140
|
def setup_digest_auth
|
163
|
-
|
141
|
+
auth_request = http_method.new(uri.request_uri)
|
142
|
+
auth_request.initialize_http_header(options[:headers])
|
143
|
+
res = http.request(auth_request)
|
144
|
+
|
164
145
|
if res['www-authenticate'] != nil && res['www-authenticate'].length > 0
|
165
146
|
@raw_request.digest_auth(username, password, res)
|
166
147
|
end
|
@@ -180,8 +161,7 @@ module HTTParty
|
|
180
161
|
query_string_parts.size > 0 ? query_string_parts.join('&') : nil
|
181
162
|
end
|
182
163
|
|
183
|
-
|
184
|
-
def handle_response
|
164
|
+
def handle_response(body)
|
185
165
|
if response_redirects?
|
186
166
|
options[:limit] -= 1
|
187
167
|
self.path = last_response['location']
|
@@ -190,18 +170,21 @@ module HTTParty
|
|
190
170
|
capture_cookies(last_response)
|
191
171
|
perform
|
192
172
|
else
|
193
|
-
|
173
|
+
body = body || last_response.body
|
174
|
+
Response.new(self, last_response, lambda { parse_response(body) }, :body => body)
|
194
175
|
end
|
195
176
|
end
|
196
177
|
|
197
178
|
# Inspired by Ruby 1.9
|
198
179
|
def handle_deflation
|
199
180
|
case last_response["content-encoding"]
|
200
|
-
when "gzip"
|
181
|
+
when "gzip", "x-gzip"
|
201
182
|
body_io = StringIO.new(last_response.body)
|
202
183
|
last_response.body.replace Zlib::GzipReader.new(body_io).read
|
184
|
+
last_response.delete('content-encoding')
|
203
185
|
when "deflate"
|
204
186
|
last_response.body.replace Zlib::Inflate.inflate(last_response.body)
|
187
|
+
last_response.delete('content-encoding')
|
205
188
|
end
|
206
189
|
end
|
207
190
|
|
@@ -241,7 +224,7 @@ module HTTParty
|
|
241
224
|
|
242
225
|
def validate
|
243
226
|
raise HTTParty::RedirectionTooDeep.new(last_response), 'HTTP redirects too deep' if options[:limit].to_i <= 0
|
244
|
-
raise ArgumentError, 'only get, post, put, delete, head, and options methods are supported' unless SupportedHTTPMethods.include?(http_method)
|
227
|
+
raise ArgumentError, 'only get, post, patch, put, delete, head, and options methods are supported' unless SupportedHTTPMethods.include?(http_method)
|
245
228
|
raise ArgumentError, ':headers must be a hash' if options[:headers] && !options[:headers].is_a?(Hash)
|
246
229
|
raise ArgumentError, 'only one authentication method, :basic_auth or :digest_auth may be used at a time' if options[:basic_auth] && options[:digest_auth]
|
247
230
|
raise ArgumentError, ':basic_auth must be a hash' if options[:basic_auth] && !options[:basic_auth].is_a?(Hash)
|
data/lib/httparty/response.rb
CHANGED
@@ -1,46 +1,21 @@
|
|
1
1
|
module HTTParty
|
2
2
|
class Response < HTTParty::BasicObject #:nodoc:
|
3
|
-
class Headers
|
4
|
-
include ::Net::HTTPHeader
|
5
|
-
|
6
|
-
def initialize(header)
|
7
|
-
@header = header
|
8
|
-
end
|
9
|
-
|
10
|
-
def ==(other)
|
11
|
-
@header == other
|
12
|
-
end
|
13
|
-
|
14
|
-
def inspect
|
15
|
-
@header.inspect
|
16
|
-
end
|
17
|
-
|
18
|
-
def method_missing(name, *args, &block)
|
19
|
-
if @header.respond_to?(name)
|
20
|
-
@header.send(name, *args, &block)
|
21
|
-
else
|
22
|
-
super
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
def respond_to?(method)
|
27
|
-
super || @header.respond_to?(method)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
|
32
3
|
def self.underscore(string)
|
33
4
|
string.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z])([A-Z])/,'\1_\2').downcase
|
34
5
|
end
|
35
6
|
|
36
|
-
attr_reader :request, :response, :
|
7
|
+
attr_reader :request, :response, :body, :headers
|
37
8
|
|
38
|
-
def initialize(request, response,
|
39
|
-
@request
|
40
|
-
@response
|
41
|
-
@body
|
42
|
-
@
|
43
|
-
@headers
|
9
|
+
def initialize(request, response, parsed_block, options={})
|
10
|
+
@request = request
|
11
|
+
@response = response
|
12
|
+
@body = response.body || options[:body]
|
13
|
+
@parsed_block = parsed_block
|
14
|
+
@headers = Headers.new(response.to_hash)
|
15
|
+
end
|
16
|
+
|
17
|
+
def parsed_response
|
18
|
+
@parsed_response ||= @parsed_block.call
|
44
19
|
end
|
45
20
|
|
46
21
|
def class
|
@@ -53,7 +28,7 @@ module HTTParty
|
|
53
28
|
|
54
29
|
def inspect
|
55
30
|
inspect_id = "%x" % (object_id * 2)
|
56
|
-
%(#<#{self.class}:0x#{inspect_id}
|
31
|
+
%(#<#{self.class}:0x#{inspect_id} parsed_response=#{parsed_response.inspect}, @response=#{response.inspect}, @headers=#{headers.inspect}>)
|
57
32
|
end
|
58
33
|
|
59
34
|
CODES_TO_OBJ = ::Net::HTTPResponse::CODE_CLASS_TO_OBJ.merge ::Net::HTTPResponse::CODE_TO_OBJ
|
@@ -64,10 +39,10 @@ module HTTParty
|
|
64
39
|
klass === response
|
65
40
|
end
|
66
41
|
end
|
67
|
-
|
42
|
+
|
68
43
|
def respond_to?(name)
|
69
|
-
return true if [:request
|
70
|
-
parsed_response.respond_to?(name)
|
44
|
+
return true if [:request, :response, :parsed_response, :body, :headers].include?(name)
|
45
|
+
parsed_response.respond_to?(name) || response.respond_to?(name)
|
71
46
|
end
|
72
47
|
|
73
48
|
protected
|
@@ -83,3 +58,5 @@ module HTTParty
|
|
83
58
|
end
|
84
59
|
end
|
85
60
|
end
|
61
|
+
|
62
|
+
require 'httparty/response/headers'
|