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
  module HashConversions
3
7
  # @return <String> This hash as a query string
@@ -22,28 +26,46 @@ module HTTParty
22
26
  #
23
27
  # @example normalize_param(:name, "Bob Jones") #=> "name=Bob%20Jones&"
24
28
  def self.normalize_param(key, value)
25
- param = ''
29
+ normalized_keys = normalize_keys(key, value)
30
+
31
+ normalized_keys.flatten.each_slice(2).inject(''.dup) do |string, (k, v)|
32
+ string << "#{ERB::Util.url_encode(k)}=#{ERB::Util.url_encode(v.to_s)}&"
33
+ end
34
+ end
35
+
36
+ def self.normalize_keys(key, value)
26
37
  stack = []
38
+ normalized_keys = []
27
39
 
28
40
  if value.respond_to?(:to_ary)
29
- param << value.to_ary.map { |element| normalize_param("#{key}[]", element) }.join
41
+ if value.empty?
42
+ normalized_keys << ["#{key}[]", '']
43
+ else
44
+ normalized_keys = value.to_ary.flat_map do |element|
45
+ normalize_keys("#{key}[]", element)
46
+ end
47
+ end
30
48
  elsif value.respond_to?(:to_hash)
31
49
  stack << [key, value.to_hash]
32
50
  else
33
- param << "#{key}=#{ERB::Util.url_encode(value.to_s)}&"
51
+ normalized_keys << [key.to_s, value]
34
52
  end
35
53
 
36
54
  stack.each do |parent, hash|
37
- hash.each do |k, v|
38
- if v.respond_to?(:to_hash)
39
- stack << ["#{parent}[#{k}]", v.to_hash]
55
+ hash.each do |child_key, child_value|
56
+ if child_value.respond_to?(:to_hash)
57
+ stack << ["#{parent}[#{child_key}]", child_value.to_hash]
58
+ elsif child_value.respond_to?(:to_ary)
59
+ child_value.to_ary.each do |v|
60
+ normalized_keys << normalize_keys("#{parent}[#{child_key}][]", v).flatten
61
+ end
40
62
  else
41
- param << normalize_param("#{parent}[#{k}]", v)
63
+ normalized_keys << normalize_keys("#{parent}[#{child_key}]", child_value).flatten
42
64
  end
43
65
  end
44
66
  end
45
67
 
46
- param
68
+ normalized_keys
47
69
  end
48
70
  end
49
71
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTParty
4
+ class HeadersProcessor
5
+ attr_reader :headers, :options
6
+
7
+ def initialize(headers, options)
8
+ @headers = headers
9
+ @options = options
10
+ end
11
+
12
+ def call
13
+ return unless options[:headers]
14
+
15
+ options[:headers] = headers.merge(options[:headers]) if headers.any?
16
+ options[:headers] = Utils.stringify_keys(process_dynamic_headers)
17
+ end
18
+
19
+ private
20
+
21
+ def process_dynamic_headers
22
+ options[:headers].each_with_object({}) do |header, processed_headers|
23
+ key, value = header
24
+ processed_headers[key] = if value.respond_to?(:call)
25
+ value.arity == 0 ? value.call : value.call(options)
26
+ else
27
+ value
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module HTTParty
2
4
  module Logger
3
5
  class ApacheFormatter #:nodoc:
4
6
  TAG_NAME = HTTParty.name
5
7
 
6
- attr_accessor :level, :logger, :current_time
8
+ attr_accessor :level, :logger
7
9
 
8
10
  def initialize(logger, level)
9
11
  @logger = logger
@@ -11,11 +13,34 @@ module HTTParty
11
13
  end
12
14
 
13
15
  def format(request, response)
14
- current_time = Time.now.strftime("%Y-%m-%d %H:%M:%S %z")
15
- http_method = request.http_method.name.split("::").last.upcase
16
- path = request.path.to_s
17
- content_length = response.respond_to?(:headers) ? response.headers['Content-Length'] : response['Content-Length']
18
- @logger.send @level, "[#{TAG_NAME}] [#{current_time}] #{response.code} \"#{http_method} #{path}\" #{content_length || '-'} "
16
+ @request = request
17
+ @response = response
18
+
19
+ logger.public_send level, message
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :request, :response
25
+
26
+ def message
27
+ "[#{TAG_NAME}] [#{current_time}] #{response.code} \"#{http_method} #{path}\" #{content_length || '-'} "
28
+ end
29
+
30
+ def current_time
31
+ Time.now.strftime('%Y-%m-%d %H:%M:%S %z')
32
+ end
33
+
34
+ def http_method
35
+ request.http_method.name.split('::').last.upcase
36
+ end
37
+
38
+ def path
39
+ request.path.to_s
40
+ end
41
+
42
+ def content_length
43
+ response.respond_to?(:headers) ? response.headers['Content-Length'] : response['Content-Length']
19
44
  end
20
45
  end
21
46
  end
@@ -1,47 +1,92 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module HTTParty
2
4
  module Logger
3
5
  class CurlFormatter #:nodoc:
4
6
  TAG_NAME = HTTParty.name
5
- OUT = ">"
6
- IN = "<"
7
+ OUT = '>'
8
+ IN = '<'
7
9
 
8
- attr_accessor :level, :logger, :current_time
10
+ attr_accessor :level, :logger
9
11
 
10
12
  def initialize(logger, level)
11
- @logger = logger
12
- @level = level.to_sym
13
+ @logger = logger
14
+ @level = level.to_sym
15
+ @messages = []
13
16
  end
14
17
 
15
18
  def format(request, response)
16
- messages = []
17
- time = Time.now.strftime("%Y-%m-%d %H:%M:%S %z")
18
- http_method = request.http_method.name.split("::").last.upcase
19
- path = request.path.to_s
19
+ @request = request
20
+ @response = response
20
21
 
21
- messages << print(time, OUT, "#{http_method} #{path}")
22
+ log_request
23
+ log_response
22
24
 
23
- if request.options[:headers] && request.options[:headers].size > 0
24
- request.options[:headers].each do |k, v|
25
- messages << print(time, OUT, "#{k}: #{v}")
26
- end
27
- end
25
+ logger.public_send level, messages.join("\n")
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :request, :response
31
+ attr_accessor :messages
28
32
 
29
- messages << print(time, OUT, request.raw_body)
30
- messages << print(time, OUT, "")
31
- messages << print(time, IN, "HTTP/#{response.http_version} #{response.code}")
33
+ def log_request
34
+ log_url
35
+ log_headers
36
+ log_query
37
+ log OUT, request.raw_body if request.raw_body
38
+ log OUT
39
+ end
40
+
41
+ def log_response
42
+ log IN, "HTTP/#{response.http_version} #{response.code}"
43
+ log_response_headers
44
+ log IN, "\n#{response.body}"
45
+ log IN
46
+ end
32
47
 
48
+ def log_url
49
+ http_method = request.http_method.name.split('::').last.upcase
50
+ uri = if request.options[:base_uri]
51
+ request.options[:base_uri] + request.path.path
52
+ else
53
+ request.path.to_s
54
+ end
55
+
56
+ log OUT, "#{http_method} #{uri}"
57
+ end
58
+
59
+ def log_headers
60
+ return unless request.options[:headers] && request.options[:headers].size > 0
61
+
62
+ log OUT, 'Headers: '
63
+ log_hash request.options[:headers]
64
+ end
65
+
66
+ def log_query
67
+ return unless request.options[:query]
68
+
69
+ log OUT, 'Query: '
70
+ log_hash request.options[:query]
71
+ end
72
+
73
+ def log_response_headers
33
74
  headers = response.respond_to?(:headers) ? response.headers : response
34
75
  response.each_header do |response_header|
35
- messages << print(time, IN, "#{response_header.capitalize}: #{headers[response_header]}")
76
+ log IN, "#{response_header.capitalize}: #{headers[response_header]}"
36
77
  end
78
+ end
37
79
 
38
- messages << print(time, IN, "\n#{response.body}")
80
+ def log_hash(hash)
81
+ hash.each { |k, v| log(OUT, "#{k}: #{v}") }
82
+ end
39
83
 
40
- @logger.send @level, messages.join("\n")
84
+ def log(direction, line = '')
85
+ messages << "[#{TAG_NAME}] [#{current_time}] #{direction} #{line}"
41
86
  end
42
87
 
43
- def print(time, direction, line)
44
- "[#{TAG_NAME}] [#{time}] #{direction} #{line}"
88
+ def current_time
89
+ Time.now.strftime("%Y-%m-%d %H:%M:%S %z")
45
90
  end
46
91
  end
47
92
  end
@@ -1,12 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'httparty/logger/apache_formatter'
2
4
  require 'httparty/logger/curl_formatter'
5
+ require 'httparty/logger/logstash_formatter'
3
6
 
4
7
  module HTTParty
5
8
  module Logger
6
9
  def self.formatters
7
10
  @formatters ||= {
8
11
  :curl => Logger::CurlFormatter,
9
- :apache => Logger::ApacheFormatter
12
+ :apache => Logger::ApacheFormatter,
13
+ :logstash => Logger::LogstashFormatter,
10
14
  }
11
15
  end
12
16
 
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTParty
4
+ module Logger
5
+ class LogstashFormatter #:nodoc:
6
+ TAG_NAME = HTTParty.name
7
+
8
+ attr_accessor :level, :logger
9
+
10
+ def initialize(logger, level)
11
+ @logger = logger
12
+ @level = level.to_sym
13
+ end
14
+
15
+ def format(request, response)
16
+ @request = request
17
+ @response = response
18
+
19
+ logger.public_send level, logstash_message
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :request, :response
25
+
26
+ def logstash_message
27
+ require 'json'
28
+ {
29
+ '@timestamp' => current_time,
30
+ '@version' => 1,
31
+ 'content_length' => content_length || '-',
32
+ 'http_method' => http_method,
33
+ 'message' => message,
34
+ 'path' => path,
35
+ 'response_code' => response.code,
36
+ 'severity' => level,
37
+ 'tags' => [TAG_NAME],
38
+ }.to_json
39
+ end
40
+
41
+ def message
42
+ "[#{TAG_NAME}] #{response.code} \"#{http_method} #{path}\" #{content_length || '-'} "
43
+ end
44
+
45
+ def current_time
46
+ Time.now.strftime('%Y-%m-%d %H:%M:%S %z')
47
+ end
48
+
49
+ def http_method
50
+ @http_method ||= request.http_method.name.split('::').last.upcase
51
+ end
52
+
53
+ def path
54
+ @path ||= request.path.to_s
55
+ end
56
+
57
+ def content_length
58
+ @content_length ||= response.respond_to?(:headers) ? response.headers['Content-Length'] : response['Content-Length']
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module HTTParty
2
4
  module ModuleInheritableAttributes #:nodoc:
3
5
  def self.included(base)
@@ -9,12 +11,12 @@ module HTTParty
9
11
  duplicate = hash.dup
10
12
 
11
13
  duplicate.each_pair do |key, value|
12
- duplicate[key] = if value.is_a?(Hash)
13
- hash_deep_dup(value)
14
+ if value.is_a?(Hash)
15
+ duplicate[key] = hash_deep_dup(value)
14
16
  elsif value.is_a?(Proc)
15
17
  duplicate[key] = value.dup
16
18
  else
17
- value
19
+ duplicate[key] = value
18
20
  end
19
21
  end
20
22
 
@@ -27,7 +29,7 @@ module HTTParty
27
29
  @mattr_inheritable_attrs += args
28
30
 
29
31
  args.each do |arg|
30
- module_eval %(class << self; attr_accessor :#{arg} end)
32
+ singleton_class.attr_accessor(arg)
31
33
  end
32
34
 
33
35
  @mattr_inheritable_attrs
@@ -36,18 +38,16 @@ module HTTParty
36
38
  def inherited(subclass)
37
39
  super
38
40
  @mattr_inheritable_attrs.each do |inheritable_attribute|
39
- ivar = "@#{inheritable_attribute}"
41
+ ivar = :"@#{inheritable_attribute}"
40
42
  subclass.instance_variable_set(ivar, instance_variable_get(ivar).clone)
41
43
 
42
44
  if instance_variable_get(ivar).respond_to?(:merge)
43
- method = <<-EOM
45
+ subclass.class_eval <<~RUBY, __FILE__, __LINE__ + 1
44
46
  def self.#{inheritable_attribute}
45
47
  duplicate = ModuleInheritableAttributes.hash_deep_dup(#{ivar})
46
48
  #{ivar} = superclass.#{inheritable_attribute}.merge(duplicate)
47
49
  end
48
- EOM
49
-
50
- subclass.class_eval method
50
+ RUBY
51
51
  end
52
52
  end
53
53
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'digest/md5'
2
4
  require 'net/http'
3
5
 
@@ -12,13 +14,13 @@ module Net
12
14
  response
13
15
  )
14
16
 
15
- @header['Authorization'] = authenticator.authorization_header
16
- @header['cookie'] = append_cookies(authenticator) if response['Set-Cookie']
17
- end
17
+ authenticator.authorization_header.each do |v|
18
+ add_field('Authorization', v)
19
+ end
18
20
 
19
- def append_cookies(authenticator)
20
- cookies = @header['cookie'] ? @header['cookie'] : []
21
- cookies.concat(authenticator.cookie_header)
21
+ authenticator.cookie_header.each do |v|
22
+ add_field('Cookie', v)
23
+ end
22
24
  end
23
25
 
24
26
  class DigestAuthenticator
@@ -44,12 +46,9 @@ module Net
44
46
  header << %(algorithm="#{@response['algorithm']}") if algorithm_present?
45
47
 
46
48
  if qop_present?
47
- fields = [
48
- %(cnonce="#{@cnonce}"),
49
- %(qop="#{@response['qop']}"),
50
- "nc=00000001"
51
- ]
52
- fields.each { |field| header << field }
49
+ header << %(cnonce="#{@cnonce}")
50
+ header << %(qop="#{@response['qop']}")
51
+ header << 'nc=00000001'
53
52
  end
54
53
 
55
54
  header << %(opaque="#{@response['opaque']}") if opaque_present?
@@ -64,12 +63,15 @@ module Net
64
63
 
65
64
  def parse(response_header)
66
65
  header = response_header['www-authenticate']
67
- .gsub(/qop=(auth(?:-int)?)/, 'qop="\\1"')
66
+
67
+ header = header.gsub(/qop=(auth(?:-int)?)/, 'qop="\\1"')
68
68
 
69
69
  header =~ /Digest (.*)/
70
70
  params = {}
71
- non_quoted = $1.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 }
72
- non_quoted.gsub(/(\w+)=([^,]*)/) { params[$1] = $2 }
71
+ if $1
72
+ non_quoted = $1.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 }
73
+ non_quoted.gsub(/(\w+)=([^,]*)/) { params[$1] = $2 }
74
+ end
73
75
  params
74
76
  end
75
77
 
@@ -95,13 +97,13 @@ module Net
95
97
  end
96
98
 
97
99
  def random
98
- format "%x", (Time.now.to_i + rand(65535))
100
+ format '%x', (Time.now.to_i + rand(65535))
99
101
  end
100
102
 
101
103
  def request_digest
102
104
  a = [md5(a1), @response['nonce'], md5(a2)]
103
- a.insert(2, "00000001", @cnonce, @response['qop']) if qop_present?
104
- md5(a.join(":"))
105
+ a.insert(2, '00000001', @cnonce, @response['qop']) if qop_present?
106
+ md5(a.join(':'))
105
107
  end
106
108
 
107
109
  def md5(str)
@@ -111,11 +113,11 @@ module Net
111
113
  def algorithm_present?
112
114
  @response.key?('algorithm') && !@response['algorithm'].empty?
113
115
  end
114
-
116
+
115
117
  def use_md5_sess?
116
118
  algorithm_present? && @response['algorithm'] == 'MD5-sess'
117
119
  end
118
-
120
+
119
121
  def a1
120
122
  a1_user_realm_pwd = [@username, @response['realm'], @password].join(':')
121
123
  if use_md5_sess?
@@ -126,7 +128,7 @@ module Net
126
128
  end
127
129
 
128
130
  def a2
129
- [@method, @path].join(":")
131
+ [@method, @path].join(':')
130
132
  end
131
133
  end
132
134
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module HTTParty
2
4
  # The default parser used by HTTParty, supports xml, json, html, csv and
3
5
  # plain text.
@@ -38,16 +40,18 @@ module HTTParty
38
40
  # @abstract Read the Custom Parsers section for more information.
39
41
  class Parser
40
42
  SupportedFormats = {
41
- 'text/xml' => :xml,
42
- 'application/xml' => :xml,
43
- 'application/json' => :json,
44
- 'text/json' => :json,
45
- 'application/javascript' => :plain,
46
- 'text/javascript' => :plain,
47
- 'text/html' => :html,
48
- 'text/plain' => :plain,
49
- 'text/csv' => :csv,
50
- 'application/csv' => :csv,
43
+ 'text/xml' => :xml,
44
+ 'application/xml' => :xml,
45
+ 'application/json' => :json,
46
+ 'application/vnd.api+json' => :json,
47
+ 'application/hal+json' => :json,
48
+ 'text/json' => :json,
49
+ 'application/javascript' => :plain,
50
+ 'text/javascript' => :plain,
51
+ 'text/html' => :html,
52
+ 'text/plain' => :plain,
53
+ 'text/csv' => :csv,
54
+ 'application/csv' => :csv,
51
55
  'text/comma-separated-values' => :csv
52
56
  }
53
57
 
@@ -99,8 +103,11 @@ module HTTParty
99
103
  # @return [nil] when the response body is nil, an empty string, spaces only or "null"
100
104
  def parse
101
105
  return nil if body.nil?
102
- return nil if body == "null"
106
+ return nil if body == 'null'
103
107
  return nil if body.valid_encoding? && body.strip.empty?
108
+ if body.valid_encoding? && body.encoding == Encoding::UTF_8
109
+ @body = body.gsub(/\A#{UTF8_BOM}/, '')
110
+ end
104
111
  if supports_format?
105
112
  parse_supported_format
106
113
  else
@@ -111,14 +118,19 @@ module HTTParty
111
118
  protected
112
119
 
113
120
  def xml
121
+ require 'multi_xml'
114
122
  MultiXml.parse(body)
115
123
  end
116
124
 
125
+ UTF8_BOM = "\xEF\xBB\xBF"
126
+
117
127
  def json
128
+ require 'json'
118
129
  JSON.parse(body, :quirks_mode => true, :allow_nan => true)
119
130
  end
120
131
 
121
132
  def csv
133
+ require 'csv'
122
134
  CSV.parse(body)
123
135
  end
124
136
 
@@ -135,9 +147,11 @@ module HTTParty
135
147
  end
136
148
 
137
149
  def parse_supported_format
138
- send(format)
139
- rescue NoMethodError => e
140
- raise NotImplementedError, "#{self.class.name} has not implemented a parsing method for the #{format.inspect} format.", e.backtrace
150
+ if respond_to?(format, true)
151
+ send(format)
152
+ else
153
+ raise NotImplementedError, "#{self.class.name} has not implemented a parsing method for the #{format.inspect} format."
154
+ end
141
155
  end
142
156
  end
143
157
  end