httparty 0.16.2 → 0.22.0

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 (96) 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 +23 -0
  5. data/.gitignore +2 -0
  6. data/.rubocop_todo.yml +1 -1
  7. data/Changelog.md +425 -280
  8. data/Gemfile +7 -0
  9. data/Guardfile +3 -2
  10. data/README.md +5 -5
  11. data/docs/README.md +90 -5
  12. data/examples/README.md +28 -11
  13. data/examples/aaws.rb +6 -2
  14. data/examples/body_stream.rb +14 -0
  15. data/examples/idn.rb +10 -0
  16. data/examples/microsoft_graph.rb +52 -0
  17. data/examples/multipart.rb +22 -0
  18. data/examples/peer_cert.rb +9 -0
  19. data/examples/stream_download.rb +8 -2
  20. data/httparty.gemspec +4 -3
  21. data/lib/httparty/connection_adapter.rb +44 -20
  22. data/lib/httparty/cookie_hash.rb +10 -8
  23. data/lib/httparty/decompressor.rb +102 -0
  24. data/lib/httparty/exceptions.rb +3 -1
  25. data/lib/httparty/hash_conversions.rb +10 -4
  26. data/lib/httparty/headers_processor.rb +32 -0
  27. data/lib/httparty/logger/apache_formatter.rb +31 -6
  28. data/lib/httparty/logger/curl_formatter.rb +9 -7
  29. data/lib/httparty/logger/logger.rb +5 -1
  30. data/lib/httparty/logger/logstash_formatter.rb +62 -0
  31. data/lib/httparty/module_inheritable_attributes.rb +9 -9
  32. data/lib/httparty/net_digest_auth.rb +15 -15
  33. data/lib/httparty/parser.rb +12 -5
  34. data/lib/httparty/request/body.rb +54 -27
  35. data/lib/httparty/request/multipart_boundary.rb +2 -0
  36. data/lib/httparty/request.rb +105 -107
  37. data/lib/httparty/response/headers.rb +4 -2
  38. data/lib/httparty/response.rb +52 -9
  39. data/lib/httparty/response_fragment.rb +21 -0
  40. data/lib/httparty/text_encoder.rb +72 -0
  41. data/lib/httparty/utils.rb +13 -0
  42. data/lib/httparty/version.rb +3 -1
  43. data/lib/httparty.rb +81 -33
  44. data/script/release +4 -4
  45. data/website/css/common.css +1 -1
  46. metadata +50 -107
  47. data/.simplecov +0 -1
  48. data/.travis.yml +0 -10
  49. data/features/basic_authentication.feature +0 -20
  50. data/features/command_line.feature +0 -95
  51. data/features/deals_with_http_error_codes.feature +0 -26
  52. data/features/digest_authentication.feature +0 -30
  53. data/features/handles_compressed_responses.feature +0 -27
  54. data/features/handles_multiple_formats.feature +0 -57
  55. data/features/steps/env.rb +0 -27
  56. data/features/steps/httparty_response_steps.rb +0 -56
  57. data/features/steps/httparty_steps.rb +0 -43
  58. data/features/steps/mongrel_helper.rb +0 -127
  59. data/features/steps/remote_service_steps.rb +0 -92
  60. data/features/supports_read_timeout_option.feature +0 -13
  61. data/features/supports_redirection.feature +0 -22
  62. data/features/supports_timeout_option.feature +0 -13
  63. data/spec/fixtures/delicious.xml +0 -23
  64. data/spec/fixtures/empty.xml +0 -0
  65. data/spec/fixtures/google.html +0 -3
  66. data/spec/fixtures/ssl/generate.sh +0 -29
  67. data/spec/fixtures/ssl/generated/bogushost.crt +0 -13
  68. data/spec/fixtures/ssl/generated/ca.crt +0 -16
  69. data/spec/fixtures/ssl/generated/ca.key +0 -15
  70. data/spec/fixtures/ssl/generated/selfsigned.crt +0 -14
  71. data/spec/fixtures/ssl/generated/server.crt +0 -13
  72. data/spec/fixtures/ssl/generated/server.key +0 -15
  73. data/spec/fixtures/ssl/openssl-exts.cnf +0 -9
  74. data/spec/fixtures/tiny.gif +0 -0
  75. data/spec/fixtures/twitter.csv +0 -2
  76. data/spec/fixtures/twitter.json +0 -1
  77. data/spec/fixtures/twitter.xml +0 -403
  78. data/spec/fixtures/undefined_method_add_node_for_nil.xml +0 -2
  79. data/spec/httparty/connection_adapter_spec.rb +0 -498
  80. data/spec/httparty/cookie_hash_spec.rb +0 -100
  81. data/spec/httparty/exception_spec.rb +0 -45
  82. data/spec/httparty/hash_conversions_spec.rb +0 -56
  83. data/spec/httparty/logger/apache_formatter_spec.rb +0 -41
  84. data/spec/httparty/logger/curl_formatter_spec.rb +0 -119
  85. data/spec/httparty/logger/logger_spec.rb +0 -38
  86. data/spec/httparty/net_digest_auth_spec.rb +0 -270
  87. data/spec/httparty/parser_spec.rb +0 -190
  88. data/spec/httparty/request/body_spec.rb +0 -60
  89. data/spec/httparty/request_spec.rb +0 -1312
  90. data/spec/httparty/response_spec.rb +0 -347
  91. data/spec/httparty/ssl_spec.rb +0 -74
  92. data/spec/httparty_spec.rb +0 -896
  93. data/spec/spec_helper.rb +0 -51
  94. data/spec/support/ssl_test_helper.rb +0 -47
  95. data/spec/support/ssl_test_server.rb +0 -80
  96. data/spec/support/stub_response.rb +0 -49
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTParty
4
+ # Decompresses the response body based on the Content-Encoding header.
5
+ #
6
+ # Net::HTTP automatically decompresses Content-Encoding values "gzip" and "deflate".
7
+ # This class will handle "br" (Brotli) and "compress" (LZW) if the requisite
8
+ # gems are installed. Otherwise, it returns nil if the body data cannot be
9
+ # decompressed.
10
+ #
11
+ # @abstract Read the HTTP Compression section for more information.
12
+ class Decompressor
13
+
14
+ # "gzip" and "deflate" are handled by Net::HTTP
15
+ # hence they do not need to be handled by HTTParty
16
+ SupportedEncodings = {
17
+ 'none' => :none,
18
+ 'identity' => :none,
19
+ 'br' => :brotli,
20
+ 'compress' => :lzw,
21
+ 'zstd' => :zstd
22
+ }.freeze
23
+
24
+ # The response body of the request
25
+ # @return [String]
26
+ attr_reader :body
27
+
28
+ # The Content-Encoding algorithm used to encode the body
29
+ # @return [Symbol] e.g. :gzip
30
+ attr_reader :encoding
31
+
32
+ # @param [String] body - the response body of the request
33
+ # @param [Symbol] encoding - the Content-Encoding algorithm used to encode the body
34
+ def initialize(body, encoding)
35
+ @body = body
36
+ @encoding = encoding
37
+ end
38
+
39
+ # Perform decompression on the response body
40
+ # @return [String] the decompressed body
41
+ # @return [nil] when the response body is nil or cannot decompressed
42
+ def decompress
43
+ return nil if body.nil?
44
+ return body if encoding.nil? || encoding.strip.empty?
45
+
46
+ if supports_encoding?
47
+ decompress_supported_encoding
48
+ else
49
+ nil
50
+ end
51
+ end
52
+
53
+ protected
54
+
55
+ def supports_encoding?
56
+ SupportedEncodings.keys.include?(encoding)
57
+ end
58
+
59
+ def decompress_supported_encoding
60
+ method = SupportedEncodings[encoding]
61
+ if respond_to?(method, true)
62
+ send(method)
63
+ else
64
+ raise NotImplementedError, "#{self.class.name} has not implemented a decompression method for #{encoding.inspect} encoding."
65
+ end
66
+ end
67
+
68
+ def none
69
+ body
70
+ end
71
+
72
+ def brotli
73
+ return nil unless defined?(::Brotli)
74
+ begin
75
+ ::Brotli.inflate(body)
76
+ rescue StandardError
77
+ nil
78
+ end
79
+ end
80
+
81
+ def lzw
82
+ begin
83
+ if defined?(::LZWS::String)
84
+ ::LZWS::String.decompress(body)
85
+ elsif defined?(::LZW::Simple)
86
+ ::LZW::Simple.new.decompress(body)
87
+ end
88
+ rescue StandardError
89
+ nil
90
+ end
91
+ end
92
+
93
+ def zstd
94
+ return nil unless defined?(::Zstd)
95
+ begin
96
+ ::Zstd.decompress(body)
97
+ rescue StandardError
98
+ nil
99
+ end
100
+ end
101
+ end
102
+ end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module HTTParty
2
- # @abstact Exceptions raised by HTTParty inherit from Error
4
+ # @abstract Exceptions raised by HTTParty inherit from Error
3
5
  class Error < StandardError; end
4
6
 
5
7
  # Exception raised when you attempt to set a non-existent format
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'erb'
2
4
 
3
5
  module HTTParty
@@ -26,8 +28,8 @@ module HTTParty
26
28
  def self.normalize_param(key, value)
27
29
  normalized_keys = normalize_keys(key, value)
28
30
 
29
- normalized_keys.flatten.each_slice(2).inject('') do |string, (k, v)|
30
- string + "#{k}=#{ERB::Util.url_encode(v.to_s)}&"
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)}&"
31
33
  end
32
34
  end
33
35
 
@@ -39,7 +41,9 @@ module HTTParty
39
41
  if value.empty?
40
42
  normalized_keys << ["#{key}[]", '']
41
43
  else
42
- normalized_keys = value.to_ary.flat_map { |element| normalize_keys("#{key}[]", element) }
44
+ normalized_keys = value.to_ary.flat_map do |element|
45
+ normalize_keys("#{key}[]", element)
46
+ end
43
47
  end
44
48
  elsif value.respond_to?(:to_hash)
45
49
  stack << [key, value.to_hash]
@@ -52,7 +56,9 @@ module HTTParty
52
56
  if child_value.respond_to?(:to_hash)
53
57
  stack << ["#{parent}[#{child_key}]", child_value.to_hash]
54
58
  elsif child_value.respond_to?(:to_ary)
55
- child_value.to_ary.each { |v| normalized_keys << normalize_keys("#{parent}[#{child_key}][]", v).flatten }
59
+ child_value.to_ary.each do |v|
60
+ normalized_keys << normalize_keys("#{parent}[#{child_key}][]", v).flatten
61
+ end
56
62
  else
57
63
  normalized_keys << normalize_keys("#{parent}[#{child_key}]", child_value).flatten
58
64
  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,9 +1,11 @@
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 = '>'.freeze
6
- IN = '<'.freeze
7
+ OUT = '>'
8
+ IN = '<'
7
9
 
8
10
  attr_accessor :level, :logger
9
11
 
@@ -20,7 +22,7 @@ module HTTParty
20
22
  log_request
21
23
  log_response
22
24
 
23
- logger.send level, messages.join("\n")
25
+ logger.public_send level, messages.join("\n")
24
26
  end
25
27
 
26
28
  private
@@ -44,7 +46,7 @@ module HTTParty
44
46
  end
45
47
 
46
48
  def log_url
47
- http_method = request.http_method.name.split("::").last.upcase
49
+ http_method = request.http_method.name.split('::').last.upcase
48
50
  uri = if request.options[:base_uri]
49
51
  request.options[:base_uri] + request.path.path
50
52
  else
@@ -80,11 +82,11 @@ module HTTParty
80
82
  end
81
83
 
82
84
  def log(direction, line = '')
83
- messages << "[#{TAG_NAME}] [#{time}] #{direction} #{line}"
85
+ messages << "[#{TAG_NAME}] [#{current_time}] #{direction} #{line}"
84
86
  end
85
87
 
86
- def time
87
- @time ||= Time.now.strftime("%Y-%m-%d %H:%M:%S %z")
88
+ def current_time
89
+ Time.now.strftime("%Y-%m-%d %H:%M:%S %z")
88
90
  end
89
91
  end
90
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
 
@@ -14,11 +16,11 @@ module Net
14
16
 
15
17
  authenticator.authorization_header.each do |v|
16
18
  add_field('Authorization', v)
17
- end
19
+ end
18
20
 
19
21
  authenticator.cookie_header.each do |v|
20
22
  add_field('Cookie', v)
21
- end
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,7 +63,8 @@ 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 = {}
@@ -97,13 +97,13 @@ module Net
97
97
  end
98
98
 
99
99
  def random
100
- format "%x", (Time.now.to_i + rand(65535))
100
+ format '%x', (Time.now.to_i + rand(65535))
101
101
  end
102
102
 
103
103
  def request_digest
104
104
  a = [md5(a1), @response['nonce'], md5(a2)]
105
- a.insert(2, "00000001", @cnonce, @response['qop']) if qop_present?
106
- md5(a.join(":"))
105
+ a.insert(2, '00000001', @cnonce, @response['qop']) if qop_present?
106
+ md5(a.join(':'))
107
107
  end
108
108
 
109
109
  def md5(str)
@@ -113,11 +113,11 @@ module Net
113
113
  def algorithm_present?
114
114
  @response.key?('algorithm') && !@response['algorithm'].empty?
115
115
  end
116
-
116
+
117
117
  def use_md5_sess?
118
118
  algorithm_present? && @response['algorithm'] == 'MD5-sess'
119
119
  end
120
-
120
+
121
121
  def a1
122
122
  a1_user_realm_pwd = [@username, @response['realm'], @password].join(':')
123
123
  if use_md5_sess?
@@ -128,7 +128,7 @@ module Net
128
128
  end
129
129
 
130
130
  def a2
131
- [@method, @path].join(":")
131
+ [@method, @path].join(':')
132
132
  end
133
133
  end
134
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.
@@ -101,7 +103,7 @@ module HTTParty
101
103
  # @return [nil] when the response body is nil, an empty string, spaces only or "null"
102
104
  def parse
103
105
  return nil if body.nil?
104
- return nil if body == "null"
106
+ return nil if body == 'null'
105
107
  return nil if body.valid_encoding? && body.strip.empty?
106
108
  if body.valid_encoding? && body.encoding == Encoding::UTF_8
107
109
  @body = body.gsub(/\A#{UTF8_BOM}/, '')
@@ -116,16 +118,19 @@ module HTTParty
116
118
  protected
117
119
 
118
120
  def xml
121
+ require 'multi_xml'
119
122
  MultiXml.parse(body)
120
123
  end
121
124
 
122
- UTF8_BOM = "\xEF\xBB\xBF".freeze
125
+ UTF8_BOM = "\xEF\xBB\xBF"
123
126
 
124
127
  def json
128
+ require 'json'
125
129
  JSON.parse(body, :quirks_mode => true, :allow_nan => true)
126
130
  end
127
131
 
128
132
  def csv
133
+ require 'csv'
129
134
  CSV.parse(body)
130
135
  end
131
136
 
@@ -142,9 +147,11 @@ module HTTParty
142
147
  end
143
148
 
144
149
  def parse_supported_format
145
- send(format)
146
- rescue NoMethodError => e
147
- 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
148
155
  end
149
156
  end
150
157
  end
@@ -1,11 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'multipart_boundary'
2
4
 
3
5
  module HTTParty
4
6
  class Request
5
7
  class Body
6
- def initialize(params, query_string_normalizer: nil)
8
+ NEWLINE = "\r\n"
9
+ private_constant :NEWLINE
10
+
11
+ def initialize(params, query_string_normalizer: nil, force_multipart: false)
7
12
  @params = params
8
13
  @query_string_normalizer = query_string_normalizer
14
+ @force_multipart = force_multipart
9
15
  end
10
16
 
11
17
  def call
@@ -21,48 +27,49 @@ module HTTParty
21
27
  end
22
28
 
23
29
  def multipart?
24
- params.respond_to?(:to_hash) && has_file?(params.to_hash)
30
+ params.respond_to?(:to_hash) && (force_multipart || has_file?(params))
25
31
  end
26
32
 
27
33
  private
28
34
 
35
+ # https://html.spec.whatwg.org/#multipart-form-data
36
+ MULTIPART_FORM_DATA_REPLACEMENT_TABLE = {
37
+ '"' => '%22',
38
+ "\r" => '%0D',
39
+ "\n" => '%0A'
40
+ }.freeze
41
+
29
42
  def generate_multipart
30
43
  normalized_params = params.flat_map { |key, value| HashConversions.normalize_keys(key, value) }
31
44
 
32
- multipart = normalized_params.inject('') do |memo, (key, value)|
33
- memo += "--#{boundary}\r\n"
34
- memo += %(Content-Disposition: form-data; name="#{key}")
45
+ multipart = normalized_params.inject(''.dup) do |memo, (key, value)|
46
+ memo << "--#{boundary}#{NEWLINE}"
47
+ memo << %(Content-Disposition: form-data; name="#{key}")
35
48
  # value.path is used to support ActionDispatch::Http::UploadedFile
36
49
  # https://github.com/jnunemaker/httparty/pull/585
37
- memo += %(; filename="#{File.basename(value.path)}") if file?(value)
38
- memo += "\r\n"
39
- memo += "Content-Type: application/octet-stream\r\n" if file?(value)
40
- memo += "\r\n"
41
- memo += file?(value) ? value.read : value.to_s
42
- memo += "\r\n"
50
+ memo << %(; filename="#{file_name(value).gsub(/["\r\n]/, MULTIPART_FORM_DATA_REPLACEMENT_TABLE)}") if file?(value)
51
+ memo << NEWLINE
52
+ memo << "Content-Type: #{content_type(value)}#{NEWLINE}" if file?(value)
53
+ memo << NEWLINE
54
+ memo << content_body(value)
55
+ memo << NEWLINE
43
56
  end
44
57
 
45
- multipart += "--#{boundary}--\r\n"
58
+ multipart << "--#{boundary}--#{NEWLINE}"
46
59
  end
47
60
 
48
- def has_file?(hash)
49
- hash.detect do |key, value|
50
- if value.respond_to?(:to_hash) || includes_hash?(value)
51
- has_file?(value)
52
- elsif value.respond_to?(:to_ary)
53
- value.any? { |e| file?(e) }
54
- else
55
- file?(value)
56
- end
61
+ def has_file?(value)
62
+ if value.respond_to?(:to_hash)
63
+ value.to_hash.any? { |_, v| has_file?(v) }
64
+ elsif value.respond_to?(:to_ary)
65
+ value.to_ary.any? { |v| has_file?(v) }
66
+ else
67
+ file?(value)
57
68
  end
58
69
  end
59
70
 
60
71
  def file?(object)
61
- object.respond_to?(:path) && object.respond_to?(:read) # add memoization
62
- end
63
-
64
- def includes_hash?(object)
65
- object.respond_to?(:to_ary) && object.any? { |e| e.respond_to?(:hash) }
72
+ object.respond_to?(:path) && object.respond_to?(:read)
66
73
  end
67
74
 
68
75
  def normalize_query(query)
@@ -73,7 +80,27 @@ module HTTParty
73
80
  end
74
81
  end
75
82
 
76
- attr_reader :params, :query_string_normalizer
83
+ def content_body(object)
84
+ if file?(object)
85
+ object = (file = object).read
86
+ file.rewind if file.respond_to?(:rewind)
87
+ end
88
+
89
+ object.to_s
90
+ end
91
+
92
+ def content_type(object)
93
+ return object.content_type if object.respond_to?(:content_type)
94
+ require 'mini_mime'
95
+ mime = MiniMime.lookup_by_filename(object.path)
96
+ mime ? mime.content_type : 'application/octet-stream'
97
+ end
98
+
99
+ def file_name(object)
100
+ object.respond_to?(:original_filename) ? object.original_filename : File.basename(object.path)
101
+ end
102
+
103
+ attr_reader :params, :query_string_normalizer, :force_multipart
77
104
  end
78
105
  end
79
106
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'securerandom'
2
4
 
3
5
  module HTTParty