httparty 0.13.7 → 0.24.2

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 (108) hide show
  1. checksums.yaml +5 -5
  2. data/.editorconfig +18 -0
  3. data/.github/dependabot.yml +6 -0
  4. data/.github/workflows/ci.yml +24 -0
  5. data/.gitignore +3 -0
  6. data/.rubocop_todo.yml +1 -1
  7. data/Changelog.md +624 -0
  8. data/Gemfile +11 -3
  9. data/Guardfile +3 -2
  10. data/README.md +18 -17
  11. data/bin/httparty +3 -1
  12. data/docs/README.md +223 -0
  13. data/examples/README.md +35 -12
  14. data/examples/aaws.rb +7 -3
  15. data/examples/body_stream.rb +14 -0
  16. data/examples/crack.rb +1 -1
  17. data/examples/custom_parsers.rb +5 -1
  18. data/examples/delicious.rb +4 -4
  19. data/examples/headers_and_user_agents.rb +7 -3
  20. data/examples/idn.rb +10 -0
  21. data/examples/logging.rb +4 -4
  22. data/examples/microsoft_graph.rb +52 -0
  23. data/examples/multipart.rb +35 -0
  24. data/examples/party_foul_mode.rb +90 -0
  25. data/examples/peer_cert.rb +9 -0
  26. data/examples/stackexchange.rb +1 -1
  27. data/examples/stream_download.rb +26 -0
  28. data/examples/tripit_sign_in.rb +17 -6
  29. data/examples/twitter.rb +2 -2
  30. data/examples/whoismyrep.rb +1 -1
  31. data/httparty.gemspec +9 -5
  32. data/lib/httparty/connection_adapter.rb +71 -24
  33. data/lib/httparty/cookie_hash.rb +10 -8
  34. data/lib/httparty/decompressor.rb +102 -0
  35. data/lib/httparty/exceptions.rb +42 -5
  36. data/lib/httparty/hash_conversions.rb +30 -8
  37. data/lib/httparty/headers_processor.rb +32 -0
  38. data/lib/httparty/logger/apache_formatter.rb +31 -6
  39. data/lib/httparty/logger/curl_formatter.rb +68 -23
  40. data/lib/httparty/logger/logger.rb +5 -1
  41. data/lib/httparty/logger/logstash_formatter.rb +62 -0
  42. data/lib/httparty/module_inheritable_attributes.rb +9 -9
  43. data/lib/httparty/net_digest_auth.rb +23 -21
  44. data/lib/httparty/parser.rb +28 -14
  45. data/lib/httparty/request/body.rb +125 -0
  46. data/lib/httparty/request/multipart_boundary.rb +13 -0
  47. data/lib/httparty/request/streaming_multipart_body.rb +190 -0
  48. data/lib/httparty/request.rb +224 -122
  49. data/lib/httparty/response/headers.rb +23 -19
  50. data/lib/httparty/response.rb +92 -13
  51. data/lib/httparty/response_fragment.rb +21 -0
  52. data/lib/httparty/text_encoder.rb +72 -0
  53. data/lib/httparty/utils.rb +13 -0
  54. data/lib/httparty/version.rb +3 -1
  55. data/lib/httparty.rb +118 -42
  56. data/script/release +4 -4
  57. data/website/css/common.css +1 -1
  58. metadata +50 -112
  59. data/.simplecov +0 -1
  60. data/.travis.yml +0 -7
  61. data/History +0 -390
  62. data/features/basic_authentication.feature +0 -20
  63. data/features/command_line.feature +0 -95
  64. data/features/deals_with_http_error_codes.feature +0 -26
  65. data/features/digest_authentication.feature +0 -30
  66. data/features/handles_compressed_responses.feature +0 -27
  67. data/features/handles_multiple_formats.feature +0 -57
  68. data/features/steps/env.rb +0 -27
  69. data/features/steps/httparty_response_steps.rb +0 -52
  70. data/features/steps/httparty_steps.rb +0 -43
  71. data/features/steps/mongrel_helper.rb +0 -127
  72. data/features/steps/remote_service_steps.rb +0 -90
  73. data/features/supports_read_timeout_option.feature +0 -13
  74. data/features/supports_redirection.feature +0 -22
  75. data/features/supports_timeout_option.feature +0 -13
  76. data/spec/fixtures/delicious.xml +0 -23
  77. data/spec/fixtures/empty.xml +0 -0
  78. data/spec/fixtures/google.html +0 -3
  79. data/spec/fixtures/ssl/generate.sh +0 -29
  80. data/spec/fixtures/ssl/generated/1fe462c2.0 +0 -16
  81. data/spec/fixtures/ssl/generated/bogushost.crt +0 -13
  82. data/spec/fixtures/ssl/generated/ca.crt +0 -16
  83. data/spec/fixtures/ssl/generated/ca.key +0 -15
  84. data/spec/fixtures/ssl/generated/selfsigned.crt +0 -14
  85. data/spec/fixtures/ssl/generated/server.crt +0 -13
  86. data/spec/fixtures/ssl/generated/server.key +0 -15
  87. data/spec/fixtures/ssl/openssl-exts.cnf +0 -9
  88. data/spec/fixtures/twitter.csv +0 -2
  89. data/spec/fixtures/twitter.json +0 -1
  90. data/spec/fixtures/twitter.xml +0 -403
  91. data/spec/fixtures/undefined_method_add_node_for_nil.xml +0 -2
  92. data/spec/httparty/connection_adapter_spec.rb +0 -468
  93. data/spec/httparty/cookie_hash_spec.rb +0 -83
  94. data/spec/httparty/exception_spec.rb +0 -38
  95. data/spec/httparty/hash_conversions_spec.rb +0 -41
  96. data/spec/httparty/logger/apache_formatter_spec.rb +0 -41
  97. data/spec/httparty/logger/curl_formatter_spec.rb +0 -18
  98. data/spec/httparty/logger/logger_spec.rb +0 -38
  99. data/spec/httparty/net_digest_auth_spec.rb +0 -230
  100. data/spec/httparty/parser_spec.rb +0 -173
  101. data/spec/httparty/request_spec.rb +0 -1073
  102. data/spec/httparty/response_spec.rb +0 -241
  103. data/spec/httparty/ssl_spec.rb +0 -74
  104. data/spec/httparty_spec.rb +0 -850
  105. data/spec/spec_helper.rb +0 -59
  106. data/spec/support/ssl_test_helper.rb +0 -47
  107. data/spec/support/ssl_test_server.rb +0 -80
  108. data/spec/support/stub_response.rb +0 -49
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+
1
5
  module HTTParty
2
6
  class Request #:nodoc:
3
7
  SupportedHTTPMethods = [
@@ -9,7 +13,10 @@ module HTTParty
9
13
  Net::HTTP::Head,
10
14
  Net::HTTP::Options,
11
15
  Net::HTTP::Move,
12
- Net::HTTP::Copy
16
+ Net::HTTP::Copy,
17
+ Net::HTTP::Mkcol,
18
+ Net::HTTP::Lock,
19
+ Net::HTTP::Unlock,
13
20
  ]
14
21
 
15
22
  SupportedURISchemes = ['http', 'https', 'webcal', nil]
@@ -26,10 +33,35 @@ module HTTParty
26
33
  end.flatten.join('&')
27
34
  end
28
35
 
36
+ JSON_API_QUERY_STRING_NORMALIZER = proc do |query|
37
+ Array(query).sort_by { |a| a[0].to_s }.map do |key, value|
38
+ if value.nil?
39
+ key.to_s
40
+ elsif value.respond_to?(:to_ary)
41
+ values = value.to_ary.map{|v| ERB::Util.url_encode(v.to_s)}
42
+ "#{key}=#{values.join(',')}"
43
+ else
44
+ HashConversions.to_params(key => value)
45
+ end
46
+ end.flatten.join('&')
47
+ end
48
+
49
+ def self._load(data)
50
+ http_method, path, options, last_response, last_uri, raw_request = Marshal.load(data)
51
+ instance = new(http_method, path, options)
52
+ instance.last_response = last_response
53
+ instance.last_uri = last_uri
54
+ instance.instance_variable_set("@raw_request", raw_request)
55
+ instance
56
+ end
57
+
29
58
  attr_accessor :http_method, :options, :last_response, :redirect, :last_uri
30
59
  attr_reader :path
31
60
 
32
61
  def initialize(http_method, path, o = {})
62
+ @changed_hosts = false
63
+ @credentials_sent = false
64
+
33
65
  self.http_method = http_method
34
66
  self.options = {
35
67
  limit: o.delete(:no_follow) ? 1 : 5,
@@ -50,7 +82,7 @@ module HTTParty
50
82
  @path = if uri.is_a?(uri_adapter)
51
83
  uri
52
84
  elsif String.try_convert(uri)
53
- uri_adapter.parse uri
85
+ uri_adapter.parse(uri).normalize
54
86
  else
55
87
  raise ArgumentError,
56
88
  "bad argument (expected #{uri_adapter} object or URI string)"
@@ -66,14 +98,22 @@ module HTTParty
66
98
  end
67
99
 
68
100
  def uri
69
- if redirect && path.relative? && path.path[0] != "/"
70
- last_uri_host = @last_uri.path.gsub(/[^\/]+$/, "")
101
+ if redirect && path.relative? && path.path[0] != '/'
102
+ last_uri_host = @last_uri.path.gsub(/[^\/]+$/, '')
71
103
 
72
- path.path = "/#{path.path}" if last_uri_host[-1] != "/"
73
- path.path = last_uri_host + path.path
104
+ path.path = "/#{path.path}" if last_uri_host[-1] != '/'
105
+ path.path = "#{last_uri_host}#{path.path}"
106
+ end
107
+
108
+ if path.relative? && path.host
109
+ new_uri = options[:uri_adapter].parse("#{@last_uri.scheme}:#{path}").normalize
110
+ elsif path.relative?
111
+ new_uri = options[:uri_adapter].parse("#{base_uri}#{path}").normalize
112
+ else
113
+ new_uri = path.clone
74
114
  end
75
115
 
76
- new_uri = path.relative? ? options[:uri_adapter].parse("#{base_uri}#{path}") : path.clone
116
+ validate_uri_safety!(new_uri) unless redirect
77
117
 
78
118
  # avoid double query string on redirects [#12]
79
119
  unless redirect
@@ -90,10 +130,10 @@ module HTTParty
90
130
  def base_uri
91
131
  if redirect
92
132
  base_uri = "#{@last_uri.scheme}://#{@last_uri.host}"
93
- base_uri += ":#{@last_uri.port}" if @last_uri.port != 80
133
+ base_uri = "#{base_uri}:#{@last_uri.port}" if @last_uri.port != 80
94
134
  base_uri
95
135
  else
96
- options[:base_uri]
136
+ options[:base_uri] && HTTParty.normalize_base_uri(options[:base_uri])
97
137
  end
98
138
  end
99
139
 
@@ -113,38 +153,56 @@ module HTTParty
113
153
  validate
114
154
  setup_raw_request
115
155
  chunked_body = nil
156
+ current_http = http
116
157
 
117
- self.last_response = http.request(@raw_request) do |http_response|
118
- if block
119
- chunks = []
158
+ begin
159
+ self.last_response = current_http.request(@raw_request) do |http_response|
160
+ if block
161
+ chunks = []
120
162
 
121
- http_response.read_body do |fragment|
122
- chunks << fragment unless options[:stream_body]
123
- block.call(fragment)
124
- end
163
+ http_response.read_body do |fragment|
164
+ encoded_fragment = encode_text(fragment, http_response['content-type'])
165
+ chunks << encoded_fragment if !options[:stream_body]
166
+ block.call ResponseFragment.new(encoded_fragment, http_response, current_http)
167
+ end
125
168
 
126
- chunked_body = chunks.join
169
+ chunked_body = chunks.join
170
+ end
127
171
  end
172
+
173
+ handle_host_redirection if response_redirects?
174
+ result = handle_unauthorized
175
+ result ||= handle_response(chunked_body, &block)
176
+ result
177
+ rescue *COMMON_NETWORK_ERRORS => e
178
+ raise options[:foul] ? HTTParty::NetworkError.new("#{e.class}: #{e.message}") : e
128
179
  end
180
+ end
129
181
 
130
- handle_deflation unless http_method == Net::HTTP::Head
131
- handle_response(chunked_body, &block)
182
+ def handle_unauthorized(&block)
183
+ return unless digest_auth? && response_unauthorized? && response_has_digest_auth_challenge?
184
+ return if @credentials_sent
185
+ @credentials_sent = true
186
+ perform(&block)
132
187
  end
133
188
 
134
189
  def raw_body
135
190
  @raw_request.body
136
191
  end
137
192
 
193
+ def _dump(_level)
194
+ opts = options.dup
195
+ opts.delete(:logger)
196
+ opts.delete(:parser) if opts[:parser] && opts[:parser].is_a?(Proc)
197
+ Marshal.dump([http_method, path, opts, last_response, @last_uri, @raw_request])
198
+ end
199
+
138
200
  private
139
201
 
140
202
  def http
141
203
  connection_adapter.call(uri, options)
142
204
  end
143
205
 
144
- def body
145
- options[:body].respond_to?(:to_hash) ? normalize_query(options[:body]) : options[:body]
146
- end
147
-
148
206
  def credentials
149
207
  (options[:basic_auth] || options[:digest_auth]).to_hash
150
208
  end
@@ -170,22 +228,65 @@ module HTTParty
170
228
  end
171
229
 
172
230
  def setup_raw_request
173
- @raw_request = http_method.new(request_uri(uri))
174
- @raw_request.body = body if body
231
+ if options[:headers].respond_to?(:to_hash)
232
+ headers_hash = options[:headers].to_hash
233
+ else
234
+ headers_hash = nil
235
+ end
236
+
237
+ @raw_request = http_method.new(request_uri(uri), headers_hash)
175
238
  @raw_request.body_stream = options[:body_stream] if options[:body_stream]
176
- @raw_request.initialize_http_header(options[:headers].to_hash) if options[:headers].respond_to?(:to_hash)
177
- @raw_request.basic_auth(username, password) if options[:basic_auth]
178
- setup_digest_auth if options[:digest_auth]
179
- end
180
239
 
181
- def setup_digest_auth
182
- auth_request = http_method.new(uri.request_uri)
183
- auth_request.initialize_http_header(options[:headers].to_hash) if options[:headers].respond_to?(:to_hash)
184
- res = http.request(auth_request)
240
+ if options[:body]
241
+ body = Body.new(
242
+ options[:body],
243
+ query_string_normalizer: query_string_normalizer,
244
+ force_multipart: options[:multipart]
245
+ )
246
+
247
+ if body.multipart?
248
+ content_type = "multipart/form-data; boundary=#{body.boundary}"
249
+ @raw_request['Content-Type'] = content_type
250
+ elsif options[:body].respond_to?(:to_hash) && !@raw_request['Content-Type']
251
+ @raw_request['Content-Type'] = 'application/x-www-form-urlencoded'
252
+ end
185
253
 
186
- if !res['www-authenticate'].nil? && res['www-authenticate'].length > 0
187
- @raw_request.digest_auth(username, password, res)
254
+ if body.streaming? && options[:stream_body] == true
255
+ stream = body.to_stream
256
+ @raw_request.body_stream = stream
257
+ @raw_request['Content-Length'] = stream.size.to_s
258
+ else
259
+ @raw_request.body = body.call
260
+ end
188
261
  end
262
+
263
+ @raw_request.instance_variable_set(:@decode_content, decompress_content?)
264
+
265
+ if options[:basic_auth] && send_authorization_header?
266
+ @raw_request.basic_auth(username, password)
267
+ @credentials_sent = true
268
+ end
269
+ setup_digest_auth if digest_auth? && response_unauthorized? && response_has_digest_auth_challenge?
270
+ end
271
+
272
+ def digest_auth?
273
+ !!options[:digest_auth]
274
+ end
275
+
276
+ def decompress_content?
277
+ !options[:skip_decompression]
278
+ end
279
+
280
+ def response_unauthorized?
281
+ !!last_response && last_response.code == '401'
282
+ end
283
+
284
+ def response_has_digest_auth_challenge?
285
+ !last_response['www-authenticate'].nil? && last_response['www-authenticate'].length > 0
286
+ end
287
+
288
+ def setup_digest_auth
289
+ @raw_request.digest_auth(username, password, last_response)
189
290
  end
190
291
 
191
292
  def query_string(uri)
@@ -199,116 +300,77 @@ module HTTParty
199
300
  query_string_parts << options[:query] unless options[:query].nil?
200
301
  end
201
302
 
202
- query_string_parts.reject!(&:empty?) unless query_string_parts == [""]
303
+ query_string_parts.reject!(&:empty?) unless query_string_parts == ['']
203
304
  query_string_parts.size > 0 ? query_string_parts.join('&') : nil
204
305
  end
205
306
 
206
- def get_charset
207
- content_type = last_response["content-type"]
208
- if content_type.nil?
209
- return nil
210
- end
211
-
212
- if content_type =~ /;\s*charset\s*=\s*([^=,;"\s]+)/i
213
- return $1
214
- end
215
-
216
- if content_type =~ /;\s*charset\s*=\s*"((\\.|[^\\"])+)"/i
217
- return $1.gsub(/\\(.)/, '\1')
218
- end
219
-
220
- nil
221
- end
222
-
223
- def encode_with_ruby_encoding(body, charset)
224
- encoding = Encoding.find(charset)
225
- body.force_encoding(encoding)
226
- rescue
227
- body
228
- end
229
-
230
307
  def assume_utf16_is_big_endian
231
308
  options[:assume_utf16_is_big_endian]
232
309
  end
233
310
 
234
- def encode_utf_16(body)
235
- if body.bytesize >= 2
236
- if body.getbyte(0) == 0xFF && body.getbyte(1) == 0xFE
237
- return body.force_encoding("UTF-16LE")
238
- elsif body.getbyte(0) == 0xFE && body.getbyte(1) == 0xFF
239
- return body.force_encoding("UTF-16BE")
240
- end
241
- end
242
-
243
- if assume_utf16_is_big_endian
244
- body.force_encoding("UTF-16BE")
311
+ def handle_response(raw_body, &block)
312
+ if response_redirects?
313
+ handle_redirection(&block)
245
314
  else
246
- body.force_encoding("UTF-16LE")
247
- end
248
- end
315
+ raw_body ||= last_response.body
249
316
 
250
- def _encode_body(body)
251
- charset = get_charset
317
+ body = decompress(raw_body, last_response['content-encoding']) unless raw_body.nil?
252
318
 
253
- if charset.nil?
254
- return body
255
- end
319
+ unless body.nil?
320
+ body = encode_text(body, last_response['content-type'])
256
321
 
257
- if "utf-16".casecmp(charset) == 0
258
- encode_utf_16(body)
259
- else
260
- encode_with_ruby_encoding(body, charset)
261
- end
262
- end
322
+ if decompress_content?
323
+ last_response.delete('content-encoding')
324
+ raw_body = body
325
+ end
326
+ end
263
327
 
264
- def encode_body(body)
265
- if "".respond_to?(:encoding)
266
- _encode_body(body)
267
- else
268
- body
328
+ Response.new(self, last_response, lambda { parse_response(body) }, body: raw_body)
269
329
  end
270
330
  end
271
331
 
272
- def handle_response(body, &block)
273
- if response_redirects?
274
- options[:limit] -= 1
275
- if options[:logger]
276
- logger = HTTParty::Logger.build(options[:logger], options[:log_level], options[:log_format])
277
- logger.format(self, last_response)
332
+ def handle_redirection(&block)
333
+ options[:limit] -= 1
334
+ if options[:logger]
335
+ logger = HTTParty::Logger.build(options[:logger], options[:log_level], options[:log_format])
336
+ logger.format(self, last_response)
337
+ end
338
+ self.path = last_response['location']
339
+ self.redirect = true
340
+ if last_response.class == Net::HTTPSeeOther
341
+ unless options[:maintain_method_across_redirects] && options[:resend_on_redirect]
342
+ self.http_method = Net::HTTP::Get
278
343
  end
279
- self.path = last_response['location']
280
- self.redirect = true
281
- if last_response.class == Net::HTTPSeeOther
282
- unless options[:maintain_method_across_redirects] && options[:resend_on_redirect]
283
- self.http_method = Net::HTTP::Get
284
- end
285
- elsif last_response.code != '307' && last_response.code != '308'
286
- unless options[:maintain_method_across_redirects]
287
- self.http_method = Net::HTTP::Get
288
- end
344
+ elsif last_response.code != '307' && last_response.code != '308'
345
+ unless options[:maintain_method_across_redirects]
346
+ self.http_method = Net::HTTP::Get
289
347
  end
290
- capture_cookies(last_response)
291
- perform(&block)
292
- else
293
- body ||= last_response.body
294
- body = encode_body(body)
295
- Response.new(self, last_response, lambda { parse_response(body) }, body: body)
296
348
  end
349
+ if http_method == Net::HTTP::Get
350
+ clear_body
351
+ end
352
+ capture_cookies(last_response)
353
+ perform(&block)
297
354
  end
298
355
 
299
- # Inspired by Ruby 1.9
300
- def handle_deflation
301
- case last_response["content-encoding"]
302
- when "gzip", "x-gzip"
303
- body_io = StringIO.new(last_response.body)
304
- last_response.body.replace Zlib::GzipReader.new(body_io).read
305
- last_response.delete('content-encoding')
306
- when "deflate"
307
- last_response.body.replace Zlib::Inflate.inflate(last_response.body)
308
- last_response.delete('content-encoding')
356
+ def handle_host_redirection
357
+ check_duplicate_location_header
358
+ redirect_path = options[:uri_adapter].parse(last_response['location']).normalize
359
+ return if redirect_path.relative? || path.host == redirect_path.host || uri.host == redirect_path.host
360
+ @changed_hosts = true
361
+ end
362
+
363
+ def check_duplicate_location_header
364
+ location = last_response.get_fields('location')
365
+ if location.is_a?(Array) && location.count > 1
366
+ raise DuplicateLocationHeader.new(last_response)
309
367
  end
310
368
  end
311
369
 
370
+ def send_authorization_header?
371
+ !@changed_hosts
372
+ end
373
+
312
374
  def response_redirects?
313
375
  case last_response
314
376
  when Net::HTTPNotModified # 304
@@ -322,11 +384,20 @@ module HTTParty
322
384
  parser.call(body, format)
323
385
  end
324
386
 
387
+ # Some Web Application Firewalls reject incoming GET requests that have a body
388
+ # if we redirect, and the resulting verb is GET then we will clear the body that
389
+ # may be left behind from the initiating request
390
+ def clear_body
391
+ options[:body] = nil
392
+ @raw_request.body = nil
393
+ end
394
+
325
395
  def capture_cookies(response)
326
396
  return unless response['Set-Cookie']
327
397
  cookies_hash = HTTParty::CookieHash.new
328
398
  cookies_hash.add_cookies(options[:headers].to_hash['Cookie']) if options[:headers] && options[:headers].to_hash['Cookie']
329
399
  response.get_fields('Set-Cookie').each { |cookie| cookies_hash.add_cookies(cookie) }
400
+
330
401
  options[:headers] ||= {}
331
402
  options[:headers]['Cookie'] = cookies_hash.to_cookie_string
332
403
  end
@@ -358,7 +429,38 @@ module HTTParty
358
429
  if path.userinfo
359
430
  username, password = path.userinfo.split(':')
360
431
  options[:basic_auth] = {username: username, password: password}
432
+ @credentials_sent = true
361
433
  end
362
434
  end
435
+
436
+ def decompress(body, encoding)
437
+ Decompressor.new(body, encoding).decompress
438
+ end
439
+
440
+ def encode_text(text, content_type)
441
+ TextEncoder.new(
442
+ text,
443
+ content_type: content_type,
444
+ assume_utf16_is_big_endian: assume_utf16_is_big_endian
445
+ ).call
446
+ end
447
+
448
+ def validate_uri_safety!(new_uri)
449
+ return if options[:skip_uri_validation]
450
+
451
+ configured_base_uri = options[:base_uri]
452
+ return unless configured_base_uri
453
+
454
+ normalized_base = options[:uri_adapter].parse(
455
+ HTTParty.normalize_base_uri(configured_base_uri)
456
+ )
457
+
458
+ return if new_uri.host == normalized_base.host
459
+
460
+ raise UnsafeURIError,
461
+ "Requested URI '#{new_uri}' has host '#{new_uri.host}' but the " \
462
+ "configured base_uri '#{normalized_base}' has host '#{normalized_base.host}'. " \
463
+ "This request could send credentials to an unintended server."
464
+ end
363
465
  end
364
466
  end
@@ -1,31 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+
1
5
  module HTTParty
2
6
  class Response #:nodoc:
3
- class Headers
7
+ class Headers < ::SimpleDelegator
4
8
  include ::Net::HTTPHeader
5
9
 
6
- def initialize(header = {})
7
- @header = header
10
+ def initialize(header_values = nil)
11
+ @header = {}
12
+ if header_values
13
+ header_values.each_pair do |k,v|
14
+ if v.is_a?(Array)
15
+ v.each do |sub_v|
16
+ add_field(k, sub_v)
17
+ end
18
+ else
19
+ add_field(k, v)
20
+ end
21
+ end
22
+ end
23
+ super(@header)
8
24
  end
9
25
 
10
26
  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
27
+ if other.is_a?(::Net::HTTPHeader)
28
+ @header == other.instance_variable_get(:@header)
29
+ elsif other.is_a?(Hash)
30
+ @header == other || @header == Headers.new(other).instance_variable_get(:@header)
23
31
  end
24
32
  end
25
-
26
- def respond_to?(method, include_all = false)
27
- super || @header.respond_to?(method, include_all)
28
- end
29
33
  end
30
34
  end
31
35
  end
@@ -1,9 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module HTTParty
2
- class Response < BasicObject
4
+ class Response < Object
3
5
  def self.underscore(string)
4
6
  string.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z])([A-Z])/, '\1_\2').downcase
5
7
  end
6
8
 
9
+ def self._load(data)
10
+ req, resp, parsed_resp, resp_body = Marshal.load(data)
11
+
12
+ new(req, resp, -> { parsed_resp }, body: resp_body)
13
+ end
14
+
7
15
  attr_reader :request, :response, :body, :headers
8
16
 
9
17
  def initialize(request, response, parsed_block, options = {})
@@ -14,50 +22,102 @@ module HTTParty
14
22
  @headers = Headers.new(response.to_hash)
15
23
 
16
24
  if request.options[:logger]
17
- logger = ::HTTParty::Logger.build(request.options[:logger], request.options[:log_level], request.options[:log_format])
25
+ logger = ::HTTParty::Logger.build(
26
+ request.options[:logger],
27
+ request.options[:log_level],
28
+ request.options[:log_format]
29
+ )
18
30
  logger.format(request, self)
19
31
  end
32
+
33
+ throw_exception
20
34
  end
21
35
 
22
36
  def parsed_response
23
37
  @parsed_response ||= @parsed_block.call
24
38
  end
25
39
 
26
- def class
27
- Response
28
- end
29
-
30
40
  def code
31
41
  response.code.to_i
32
42
  end
33
43
 
44
+ def http_version
45
+ response.http_version
46
+ end
47
+
34
48
  def tap
35
49
  yield self
36
50
  self
37
51
  end
38
52
 
39
53
  def inspect
40
- inspect_id = ::Kernel::format "%x", (object_id * 2)
54
+ inspect_id = ::Kernel::format '%x', (object_id * 2)
41
55
  %(#<#{self.class}:0x#{inspect_id} parsed_response=#{parsed_response.inspect}, @response=#{response.inspect}, @headers=#{headers.inspect}>)
42
56
  end
43
57
 
44
58
  CODES_TO_OBJ = ::Net::HTTPResponse::CODE_CLASS_TO_OBJ.merge ::Net::HTTPResponse::CODE_TO_OBJ
45
59
 
46
60
  CODES_TO_OBJ.each do |response_code, klass|
47
- name = klass.name.sub("Net::HTTP", '')
48
- define_method("#{underscore(name)}?") do
61
+ name = klass.name.sub('Net::HTTP', '')
62
+ name = "#{underscore(name)}?".to_sym
63
+
64
+ define_method(name) do
49
65
  klass === response
50
66
  end
51
67
  end
52
68
 
53
69
  # Support old multiple_choice? method from pre 2.0.0 era.
54
- if ::RUBY_VERSION >= "2.0.0" && ::RUBY_PLATFORM != "java"
70
+ if ::RUBY_PLATFORM != 'java'
55
71
  alias_method :multiple_choice?, :multiple_choices?
56
72
  end
57
73
 
58
- def respond_to?(name, include_all = false)
59
- return true if [:request, :response, :parsed_response, :body, :headers].include?(name)
60
- parsed_response.respond_to?(name, include_all) || response.respond_to?(name, include_all)
74
+ # Support old status codes method from pre 2.6.0 era.
75
+ if ::RUBY_PLATFORM != 'java'
76
+ alias_method :gateway_time_out?, :gateway_timeout?
77
+ alias_method :request_entity_too_large?, :payload_too_large?
78
+ alias_method :request_time_out?, :request_timeout?
79
+ alias_method :request_uri_too_long?, :uri_too_long?
80
+ alias_method :requested_range_not_satisfiable?, :range_not_satisfiable?
81
+ end
82
+
83
+ def nil?
84
+ warn_about_nil_deprecation
85
+ response.nil? || response.body.nil? || response.body.empty?
86
+ end
87
+
88
+ def to_s
89
+ if !response.nil? && !response.body.nil? && response.body.respond_to?(:to_s)
90
+ response.body.to_s
91
+ else
92
+ inspect
93
+ end
94
+ end
95
+
96
+ def pretty_print(pp)
97
+ if !parsed_response.nil? && parsed_response.respond_to?(:pretty_print)
98
+ parsed_response.pretty_print(pp)
99
+ else
100
+ super
101
+ end
102
+ end
103
+
104
+ def display(port=$>)
105
+ if !parsed_response.nil? && parsed_response.respond_to?(:display)
106
+ parsed_response.display(port)
107
+ elsif !response.nil? && !response.body.nil? && response.body.respond_to?(:display)
108
+ response.body.display(port)
109
+ else
110
+ port.write(inspect)
111
+ end
112
+ end
113
+
114
+ def respond_to_missing?(name, *args)
115
+ return true if super
116
+ parsed_response.respond_to?(name) || response.respond_to?(name)
117
+ end
118
+
119
+ def _dump(_level)
120
+ Marshal.dump([request, response, parsed_response, body])
61
121
  end
62
122
 
63
123
  protected
@@ -71,6 +131,25 @@ module HTTParty
71
131
  super
72
132
  end
73
133
  end
134
+
135
+ def throw_exception
136
+ if @request.options[:raise_on].to_a.detect { |c| code.to_s.match(/#{c.to_s}/) }
137
+ ::Kernel.raise ::HTTParty::ResponseError.new(@response), "Code #{code} - #{body}"
138
+ end
139
+ end
140
+
141
+ private
142
+
143
+ def warn_about_nil_deprecation
144
+ trace_line = caller.reject { |line| line.include?('httparty') }.first
145
+ warning = "[DEPRECATION] HTTParty will no longer override `response#nil?`. " \
146
+ "This functionality will be removed in future versions. " \
147
+ "Please, add explicit check `response.body.nil? || response.body.empty?`. " \
148
+ "For more info refer to: https://github.com/jnunemaker/httparty/issues/568\n" \
149
+ "#{trace_line}"
150
+
151
+ warn(warning)
152
+ end
74
153
  end
75
154
  end
76
155