httparty 0.22.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6db4806b4e495a0d454bdb0fa77d07f76dde813c424ea9973eed68f647e79ed0
4
- data.tar.gz: e03d3ddf2c05b624cfe3d816b02a6b42370b2d9528eca6ae776821f7da5f33da
3
+ metadata.gz: 5140fa45bdc5cad9c43dedc767d225a8d303df17e7684fb229e15b5b719c7581
4
+ data.tar.gz: df6926136fccc436033ebdec116d46d90c9d4cc41425e064f9caa8954d174ae2
5
5
  SHA512:
6
- metadata.gz: 81979384b9703f7354a8cddf5793bd1fff94d80b7a10ed9f982daf7ebfdfc4368df1b29ca01d08bce701b0f54f0951ed4f41cb370089cb6cbd40fa9e362d2872
7
- data.tar.gz: d11a1d72517e00b75223dc479d7df72991aa1fe3a0abf4ecc4031c8dbd1e90f0d11eb600d4ec0e07d10f64d522381f3b6297756df0eeba81cad03739860036e3
6
+ metadata.gz: 0a9f6122cbddaf9929f6c0e2ecf790438b980b9c878ebb638a7ceb0f3404faf5ab722a0eeedc7d239513176a8806c5bd7963316228ab3cedc4e5a592fdea0f63
7
+ data.tar.gz: 140dd82771ccf6d1e97d427c7e60c00728e05389124a68b1dc9a6b24ef4fdbd2356dd2d1b3e7836afa17b70020fdff7f3e2a652c34eb6945cb40ef546cfa82a4
@@ -11,6 +11,7 @@ jobs:
11
11
  - 3.1
12
12
  - 3.2
13
13
  - 3.3
14
+ - 3.4
14
15
  steps:
15
16
  - name: Check out repository code
16
17
  uses: actions/checkout@v4
data/Gemfile CHANGED
@@ -1,6 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
  gemspec
3
3
 
4
+ gem 'base64'
4
5
  gem 'rake'
5
6
  gem 'mongrel', '1.2.0.pre2'
6
7
  gem 'json'
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/jnunemaker/httparty/actions/workflows/ci.yml/badge.svg)](https://github.com/jnunemaker/httparty/actions/workflows/ci.yml)
4
4
 
5
- Makes http fun again! Ain't no party like a httparty, because a httparty don't stop.
5
+ Makes http fun again! Ain't no party like a httparty, because a httparty don't stop.
6
6
 
7
7
  ## Install
8
8
 
@@ -12,9 +12,8 @@ gem install httparty
12
12
 
13
13
  ## Requirements
14
14
 
15
- * Ruby 2.3.0 or higher
16
- * multi_xml
17
- * You like to party!
15
+ - Ruby 2.7.0 or higher
16
+ - You like to party!
18
17
 
19
18
  ## Examples
20
19
 
@@ -47,7 +46,8 @@ puts stack_exchange.questions
47
46
  puts stack_exchange.users
48
47
  ```
49
48
 
50
- See the [examples directory](http://github.com/jnunemaker/httparty/tree/master/examples) for even more goodies.
49
+ See the [examples directory](http://github.com/jnunemaker/httparty/tree/main/examples) for even more goodies.
50
+
51
51
  ## Command Line Interface
52
52
 
53
53
  httparty also includes the executable `httparty` which can be
@@ -63,17 +63,17 @@ httparty "https://api.stackexchange.com/2.2/questions?site=stackoverflow"
63
63
 
64
64
  ## Help and Docs
65
65
 
66
- * [Docs](https://github.com/jnunemaker/httparty/tree/master/docs)
67
- * https://github.com/jnunemaker/httparty/discussions
68
- * https://www.rubydoc.info/github/jnunemaker/httparty
66
+ - [Docs](https://github.com/jnunemaker/httparty/tree/main/docs)
67
+ - https://github.com/jnunemaker/httparty/discussions
68
+ - https://www.rubydoc.info/github/jnunemaker/httparty
69
69
 
70
70
  ## Contributing
71
71
 
72
- * Fork the project.
73
- * Run `bundle`
74
- * Run `bundle exec rake`
75
- * Make your feature addition or bug fix.
76
- * Add tests for it. This is important so I don't break it in a future version unintentionally.
77
- * Run `bundle exec rake` (No, REALLY :))
78
- * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself in another branch so I can ignore when I pull)
79
- * Send me a pull request. Bonus points for topic branches.
72
+ - Fork the project.
73
+ - Run `bundle`
74
+ - Run `bundle exec rake`
75
+ - Make your feature addition or bug fix.
76
+ - Add tests for it. This is important so I don't break it in a future version unintentionally.
77
+ - Run `bundle exec rake` (No, REALLY :))
78
+ - Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself in another branch so I can ignore when I pull)
79
+ - Send me a pull request. Bonus points for topic branches.
data/docs/README.md CHANGED
@@ -4,6 +4,7 @@ Makes http fun again!
4
4
 
5
5
  ## Table of contents
6
6
  - [Parsing JSON](#parsing-json)
7
+ - [File Uploads (Multipart)](#file-uploads-multipart)
7
8
  - [Working with SSL](#working-with-ssl)
8
9
 
9
10
  ## Parsing JSON
@@ -28,6 +29,35 @@ HTTParty.post('http://example.com', body: JSON.generate({ foo: 'bar' }), headers
28
29
  HTTParty.post('http://example.com', body: { foo: 'bar' }.to_json, headers: { 'Content-Type' => 'application/json' })
29
30
  ```
30
31
 
32
+ ## File Uploads (Multipart)
33
+
34
+ When you include a `File` object in the body, HTTParty automatically uses `multipart/form-data` encoding:
35
+
36
+ ```ruby
37
+ HTTParty.post('http://example.com/upload',
38
+ body: {
39
+ name: 'Foo Bar',
40
+ avatar: File.open('/path/to/avatar.jpg')
41
+ }
42
+ )
43
+ ```
44
+
45
+ ### Streaming Uploads for Large Files
46
+
47
+ For large file uploads, you can enable streaming mode to reduce memory usage. Instead of loading the entire file into memory, HTTParty will stream the file in chunks:
48
+
49
+ ```ruby
50
+ HTTParty.post('http://example.com/upload',
51
+ body: {
52
+ name: 'Foo Bar',
53
+ avatar: File.open('/path/to/large_file.zip')
54
+ },
55
+ stream_body: true
56
+ )
57
+ ```
58
+
59
+ **Note:** Some servers may not handle streaming uploads correctly. If you encounter issues (e.g., 400 errors), try without the `stream_body` option.
60
+
31
61
  ## Working with SSL
32
62
 
33
63
  You can use this guide to work with SSL certificates.
@@ -36,6 +66,8 @@ You can use this guide to work with SSL certificates.
36
66
 
37
67
  ```ruby
38
68
  # Use this example if you are using a pem file
69
+ # - cert.pem must contain the content of a PEM file having the private key appended (separated from the cert by a newline \n)
70
+ # - Use an empty string for the password if the cert is not password protected
39
71
 
40
72
  class Client
41
73
  include HTTParty
data/examples/README.md CHANGED
@@ -78,6 +78,7 @@
78
78
 
79
79
  * [Multipart](multipart.rb)
80
80
  * Multipart data upload _(with and without file)_
81
+ * Streaming uploads for large files with `stream_body: true`
81
82
 
82
83
  * [Uploading File](body_stream.rb)
83
84
  * Uses `body_stream` to upload file
@@ -20,3 +20,16 @@ HTTParty.post(
20
20
  email: 'example@email.com'
21
21
  }
22
22
  )
23
+
24
+
25
+ # For large file uploads, use stream_body: true to reduce memory usage.
26
+ # Instead of loading the entire file into memory, HTTParty will stream it in chunks.
27
+ # Note: Some servers may not handle streaming uploads correctly.
28
+
29
+ HTTParty.post(
30
+ 'http://localhost:3000/upload',
31
+ body: {
32
+ document: File.open('/full/path/to/large_file.zip')
33
+ },
34
+ stream_body: true
35
+ )
@@ -0,0 +1,90 @@
1
+ require 'httparty'
2
+
3
+ class APIClient
4
+ include HTTParty
5
+ base_uri 'api.example.com'
6
+
7
+ def self.fetch_user(id)
8
+ begin
9
+ get("/users/#{id}", foul: true)
10
+ rescue HTTParty::NetworkError => e
11
+ handle_network_error(e)
12
+ rescue HTTParty::ResponseError => e
13
+ handle_api_error(e)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def self.handle_network_error(error)
20
+ case error.cause
21
+ when Errno::ECONNREFUSED
22
+ {
23
+ error: :server_down,
24
+ message: "The API server appears to be down",
25
+ details: error.message
26
+ }
27
+ when Net::OpenTimeout, Timeout::Error
28
+ {
29
+ error: :timeout,
30
+ message: "The request timed out",
31
+ details: error.message
32
+ }
33
+ when SocketError
34
+ {
35
+ error: :network_error,
36
+ message: "Could not connect to the API server",
37
+ details: error.message
38
+ }
39
+ when OpenSSL::SSL::SSLError
40
+ {
41
+ error: :ssl_error,
42
+ message: "SSL certificate verification failed",
43
+ details: error.message
44
+ }
45
+ else
46
+ {
47
+ error: :unknown_network_error,
48
+ message: "An unexpected network error occurred",
49
+ details: error.message
50
+ }
51
+ end
52
+ end
53
+
54
+ def self.handle_api_error(error)
55
+ {
56
+ error: :api_error,
57
+ message: "API returned error #{error.response.code}",
58
+ details: error.response.body
59
+ }
60
+ end
61
+ end
62
+
63
+ # Example usage:
64
+
65
+ # 1. When server is down
66
+ result = APIClient.fetch_user(123)
67
+ puts "Server down example:"
68
+ puts result.inspect
69
+ puts
70
+
71
+ # 2. When request times out
72
+ result = APIClient.fetch_user(456)
73
+ puts "Timeout example:"
74
+ puts result.inspect
75
+ puts
76
+
77
+ # 3. When SSL error occurs
78
+ result = APIClient.fetch_user(789)
79
+ puts "SSL error example:"
80
+ puts result.inspect
81
+ puts
82
+
83
+ # 4. Simple example without a wrapper class
84
+ begin
85
+ HTTParty.get('https://api.example.com/users', foul: true)
86
+ rescue HTTParty::Foul => e
87
+ puts "Direct usage example:"
88
+ puts "Error type: #{e.cause.class}"
89
+ puts "Error message: #{e.message}"
90
+ end
data/httparty.gemspec CHANGED
@@ -12,6 +12,7 @@ Gem::Specification.new do |s|
12
12
  s.homepage = "https://github.com/jnunemaker/httparty"
13
13
  s.summary = 'Makes http fun! Also, makes consuming restful web services dead easy.'
14
14
  s.description = 'Makes http fun! Also, makes consuming restful web services dead easy.'
15
+ s.metadata["changelog_uri"] = 'https://github.com/jnunemaker/httparty/releases'
15
16
 
16
17
  s.required_ruby_version = '>= 2.7.0'
17
18
 
@@ -1,18 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTParty
4
+ COMMON_NETWORK_ERRORS = [
5
+ EOFError,
6
+ Errno::ECONNABORTED,
7
+ Errno::ECONNREFUSED,
8
+ Errno::ECONNRESET,
9
+ Errno::EHOSTUNREACH,
10
+ Errno::EINVAL,
11
+ Errno::ENETUNREACH,
12
+ Errno::ENOTSOCK,
13
+ Errno::EPIPE,
14
+ Errno::ETIMEDOUT,
15
+ Net::HTTPBadResponse,
16
+ Net::HTTPHeaderSyntaxError,
17
+ Net::ProtocolError,
18
+ Net::ReadTimeout,
19
+ OpenSSL::SSL::SSLError,
20
+ SocketError,
21
+ Timeout::Error # Also covers subclasses like Net::OpenTimeout
22
+ ].freeze
23
+
4
24
  # @abstract Exceptions raised by HTTParty inherit from Error
5
25
  class Error < StandardError; end
6
26
 
27
+ # @abstract Exceptions raised by HTTParty inherit from this because it is funny
28
+ # and if you don't like fun you should be using a different library.
29
+ class Foul < Error; end
30
+
7
31
  # Exception raised when you attempt to set a non-existent format
8
- class UnsupportedFormat < Error; end
32
+ class UnsupportedFormat < Foul; end
9
33
 
10
34
  # Exception raised when using a URI scheme other than HTTP or HTTPS
11
- class UnsupportedURIScheme < Error; end
35
+ class UnsupportedURIScheme < Foul; end
12
36
 
13
37
  # @abstract Exceptions which inherit from ResponseError contain the Net::HTTP
14
38
  # response object accessible via the {#response} method.
15
- class ResponseError < Error
39
+ class ResponseError < Foul
16
40
  # Returns the response of the last request
17
41
  # @return [Net::HTTPResponse] A subclass of Net::HTTPResponse, e.g.
18
42
  # Net::HTTPOK
@@ -32,4 +56,11 @@ module HTTParty
32
56
 
33
57
  # Exception that is raised when request redirects and location header is present more than once
34
58
  class DuplicateLocationHeader < ResponseError; end
59
+
60
+ # Exception that is raised when common network errors occur.
61
+ class NetworkError < Foul; end
62
+
63
+ # Exception that is raised when an absolute URI is used that doesn't match
64
+ # the configured base_uri, which could indicate an SSRF attempt.
65
+ class UnsafeURIError < Foul; end
35
66
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'multipart_boundary'
4
+ require_relative 'streaming_multipart_body'
4
5
 
5
6
  module HTTParty
6
7
  class Request
@@ -30,6 +31,22 @@ module HTTParty
30
31
  params.respond_to?(:to_hash) && (force_multipart || has_file?(params))
31
32
  end
32
33
 
34
+ def streaming?
35
+ multipart? && has_file?(params)
36
+ end
37
+
38
+ def to_stream
39
+ return nil unless streaming?
40
+ StreamingMultipartBody.new(prepared_parts, boundary)
41
+ end
42
+
43
+ def prepared_parts
44
+ normalized_params = params.flat_map { |key, value| HashConversions.normalize_keys(key, value) }
45
+ normalized_params.map do |key, value|
46
+ [key, value, file?(value)]
47
+ end
48
+ end
49
+
33
50
  private
34
51
 
35
52
  # https://html.spec.whatwg.org/#multipart-form-data
@@ -42,20 +59,20 @@ module HTTParty
42
59
  def generate_multipart
43
60
  normalized_params = params.flat_map { |key, value| HashConversions.normalize_keys(key, value) }
44
61
 
45
- multipart = normalized_params.inject(''.dup) do |memo, (key, value)|
46
- memo << "--#{boundary}#{NEWLINE}"
47
- memo << %(Content-Disposition: form-data; name="#{key}")
62
+ multipart = normalized_params.inject(''.b) do |memo, (key, value)|
63
+ memo << "--#{boundary}#{NEWLINE}".b
64
+ memo << %(Content-Disposition: form-data; name="#{key}").b
48
65
  # value.path is used to support ActionDispatch::Http::UploadedFile
49
66
  # https://github.com/jnunemaker/httparty/pull/585
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
67
+ memo << %(; filename="#{file_name(value).gsub(/["\r\n]/, MULTIPART_FORM_DATA_REPLACEMENT_TABLE)}").b if file?(value)
68
+ memo << NEWLINE.b
69
+ memo << "Content-Type: #{content_type(value)}#{NEWLINE}".b if file?(value)
70
+ memo << NEWLINE.b
54
71
  memo << content_body(value)
55
- memo << NEWLINE
72
+ memo << NEWLINE.b
56
73
  end
57
74
 
58
- multipart << "--#{boundary}--#{NEWLINE}"
75
+ multipart << "--#{boundary}--#{NEWLINE}".b
59
76
  end
60
77
 
61
78
  def has_file?(value)
@@ -83,10 +100,12 @@ module HTTParty
83
100
  def content_body(object)
84
101
  if file?(object)
85
102
  object = (file = object).read
103
+ object.force_encoding(Encoding::BINARY) if object.respond_to?(:force_encoding)
86
104
  file.rewind if file.respond_to?(:rewind)
105
+ object.to_s
106
+ else
107
+ object.to_s.b
87
108
  end
88
-
89
- object.to_s
90
109
  end
91
110
 
92
111
  def content_type(object)
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTParty
4
+ class Request
5
+ class StreamingMultipartBody
6
+ NEWLINE = "\r\n"
7
+ CHUNK_SIZE = 64 * 1024 # 64 KB chunks
8
+
9
+ def initialize(parts, boundary)
10
+ @parts = parts
11
+ @boundary = boundary
12
+ @part_index = 0
13
+ @state = :header
14
+ @current_file = nil
15
+ @header_buffer = nil
16
+ @header_offset = 0
17
+ @footer_sent = false
18
+ end
19
+
20
+ def size
21
+ @size ||= calculate_size
22
+ end
23
+
24
+ def read(length = nil, outbuf = nil)
25
+ outbuf = outbuf ? outbuf.replace(''.b) : ''.b
26
+
27
+ return read_all(outbuf) if length.nil?
28
+
29
+ while outbuf.bytesize < length
30
+ chunk = read_chunk(length - outbuf.bytesize)
31
+ break if chunk.nil?
32
+ outbuf << chunk
33
+ end
34
+
35
+ outbuf.empty? ? nil : outbuf
36
+ end
37
+
38
+ def rewind
39
+ @part_index = 0
40
+ @state = :header
41
+ @current_file = nil
42
+ @header_buffer = nil
43
+ @header_offset = 0
44
+ @footer_sent = false
45
+ @parts.each do |_key, value, _is_file|
46
+ value.rewind if value.respond_to?(:rewind)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def read_all(outbuf)
53
+ while (chunk = read_chunk(CHUNK_SIZE))
54
+ outbuf << chunk
55
+ end
56
+ outbuf.empty? ? nil : outbuf
57
+ end
58
+
59
+ def read_chunk(max_length)
60
+ loop do
61
+ return nil if @part_index >= @parts.size && @footer_sent
62
+
63
+ if @part_index >= @parts.size
64
+ @footer_sent = true
65
+ return "--#{@boundary}--#{NEWLINE}".b
66
+ end
67
+
68
+ key, value, is_file = @parts[@part_index]
69
+
70
+ case @state
71
+ when :header
72
+ chunk = read_header_chunk(key, value, is_file, max_length)
73
+ return chunk if chunk
74
+
75
+ when :body
76
+ chunk = read_body_chunk(value, is_file, max_length)
77
+ return chunk if chunk
78
+
79
+ when :newline
80
+ @state = :header
81
+ @part_index += 1
82
+ return NEWLINE.b
83
+ end
84
+ end
85
+ end
86
+
87
+ def read_header_chunk(key, value, is_file, max_length)
88
+ if @header_buffer.nil?
89
+ @header_buffer = build_part_header(key, value, is_file)
90
+ @header_offset = 0
91
+ end
92
+
93
+ remaining = @header_buffer.bytesize - @header_offset
94
+ if remaining > 0
95
+ chunk_size = [remaining, max_length].min
96
+ chunk = @header_buffer.byteslice(@header_offset, chunk_size)
97
+ @header_offset += chunk_size
98
+ return chunk
99
+ end
100
+
101
+ @header_buffer = nil
102
+ @header_offset = 0
103
+ @state = :body
104
+ nil
105
+ end
106
+
107
+ def read_body_chunk(value, is_file, max_length)
108
+ if is_file
109
+ chunk = read_file_chunk(value, max_length)
110
+ if chunk
111
+ return chunk
112
+ else
113
+ @current_file = nil
114
+ @state = :newline
115
+ return nil
116
+ end
117
+ else
118
+ @state = :newline
119
+ return value.to_s.b
120
+ end
121
+ end
122
+
123
+ def read_file_chunk(file, max_length)
124
+ chunk_size = [max_length, CHUNK_SIZE].min
125
+ chunk = file.read(chunk_size)
126
+ return nil if chunk.nil?
127
+ chunk.force_encoding(Encoding::BINARY) if chunk.respond_to?(:force_encoding)
128
+ chunk
129
+ end
130
+
131
+ def build_part_header(key, value, is_file)
132
+ header = "--#{@boundary}#{NEWLINE}".b
133
+ header << %(Content-Disposition: form-data; name="#{key}").b
134
+ if is_file
135
+ header << %(; filename="#{file_name(value).gsub(/["\r\n]/, replacement_table)}").b
136
+ header << NEWLINE.b
137
+ header << "Content-Type: #{content_type(value)}#{NEWLINE}".b
138
+ else
139
+ header << NEWLINE.b
140
+ end
141
+ header << NEWLINE.b
142
+ header
143
+ end
144
+
145
+ def calculate_size
146
+ total = 0
147
+ @parts.each do |key, value, is_file|
148
+ total += build_part_header(key, value, is_file).bytesize
149
+ total += content_size(value, is_file)
150
+ total += NEWLINE.bytesize
151
+ end
152
+ total += "--#{@boundary}--#{NEWLINE}".bytesize
153
+ total
154
+ end
155
+
156
+ def content_size(value, is_file)
157
+ if is_file
158
+ if value.respond_to?(:size)
159
+ value.size
160
+ elsif value.respond_to?(:stat)
161
+ value.stat.size
162
+ else
163
+ value.read.bytesize.tap { value.rewind }
164
+ end
165
+ else
166
+ value.to_s.b.bytesize
167
+ end
168
+ end
169
+
170
+ def content_type(object)
171
+ return object.content_type if object.respond_to?(:content_type)
172
+ require 'mini_mime'
173
+ mime = MiniMime.lookup_by_filename(object.path)
174
+ mime ? mime.content_type : 'application/octet-stream'
175
+ end
176
+
177
+ def file_name(object)
178
+ object.respond_to?(:original_filename) ? object.original_filename : File.basename(object.path)
179
+ end
180
+
181
+ def replacement_table
182
+ @replacement_table ||= {
183
+ '"' => '%22',
184
+ "\r" => '%0D',
185
+ "\n" => '%0A'
186
+ }.freeze
187
+ end
188
+ end
189
+ end
190
+ end
@@ -113,6 +113,8 @@ module HTTParty
113
113
  new_uri = path.clone
114
114
  end
115
115
 
116
+ validate_uri_safety!(new_uri) unless redirect
117
+
116
118
  # avoid double query string on redirects [#12]
117
119
  unless redirect
118
120
  new_uri.query = query_string(new_uri)
@@ -153,24 +155,28 @@ module HTTParty
153
155
  chunked_body = nil
154
156
  current_http = http
155
157
 
156
- self.last_response = current_http.request(@raw_request) do |http_response|
157
- if block
158
- chunks = []
158
+ begin
159
+ self.last_response = current_http.request(@raw_request) do |http_response|
160
+ if block
161
+ chunks = []
159
162
 
160
- http_response.read_body do |fragment|
161
- encoded_fragment = encode_text(fragment, http_response['content-type'])
162
- chunks << encoded_fragment if !options[:stream_body]
163
- block.call ResponseFragment.new(encoded_fragment, http_response, current_http)
164
- 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
165
168
 
166
- chunked_body = chunks.join
169
+ chunked_body = chunks.join
170
+ end
167
171
  end
168
- end
169
172
 
170
- handle_host_redirection if response_redirects?
171
- result = handle_unauthorized
172
- result ||= handle_response(chunked_body, &block)
173
- result
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
179
+ end
174
180
  end
175
181
 
176
182
  def handle_unauthorized(&block)
@@ -241,8 +247,17 @@ module HTTParty
241
247
  if body.multipart?
242
248
  content_type = "multipart/form-data; boundary=#{body.boundary}"
243
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
253
+
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
244
260
  end
245
- @raw_request.body = body.call
246
261
  end
247
262
 
248
263
  @raw_request.instance_variable_set(:@decode_content, decompress_content?)
@@ -429,5 +444,23 @@ module HTTParty
429
444
  assume_utf16_is_big_endian: assume_utf16_is_big_endian
430
445
  ).call
431
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
432
465
  end
433
466
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTParty
4
- VERSION = '0.22.0'
4
+ VERSION = '0.24.2'
5
5
  end
data/lib/httparty.rb CHANGED
@@ -41,7 +41,7 @@ module HTTParty
41
41
  # [:+local_host+:] Local address to bind to before connecting.
42
42
  # [:+local_port+:] Local port to bind to before connecting.
43
43
  # [:+body_stream+:] Allow streaming to a REST server to specify a body_stream.
44
- # [:+stream_body+:] Allow for streaming large files without loading them into memory.
44
+ # [:+stream_body+:] When downloading with a block, avoids accumulating the response in memory. When uploading files, streams the request body to reduce memory usage (opt-in).
45
45
  # [:+multipart+:] Force content-type to be multipart
46
46
  #
47
47
  # There are also another set of options with names corresponding to various class methods. The methods in question are those that let you set a class-wide default, and the options override the defaults on a request-by-request basis. Those options are:
@@ -62,6 +62,16 @@ module HTTParty
62
62
  # * :+ssl_ca_path+: see HTTParty::ClassMethods.ssl_ca_path.
63
63
 
64
64
  module ClassMethods
65
+ # Turns on or off the foul option.
66
+ #
67
+ # class Foo
68
+ # include HTTParty
69
+ # foul true
70
+ # end
71
+ def foul(bool)
72
+ default_options[:foul] = bool
73
+ end
74
+
65
75
  # Turns on logging
66
76
  #
67
77
  # class Foo
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: httparty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.22.0
4
+ version: 0.24.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-04-29 00:00:00.000000000 Z
12
+ date: 2026-01-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: csv
@@ -91,6 +91,7 @@ files:
91
91
  - examples/microsoft_graph.rb
92
92
  - examples/multipart.rb
93
93
  - examples/nokogiri_html_parser.rb
94
+ - examples/party_foul_mode.rb
94
95
  - examples/peer_cert.rb
95
96
  - examples/rescue_json.rb
96
97
  - examples/rubyurl.rb
@@ -117,6 +118,7 @@ files:
117
118
  - lib/httparty/request.rb
118
119
  - lib/httparty/request/body.rb
119
120
  - lib/httparty/request/multipart_boundary.rb
121
+ - lib/httparty/request/streaming_multipart_body.rb
120
122
  - lib/httparty/response.rb
121
123
  - lib/httparty/response/headers.rb
122
124
  - lib/httparty/response_fragment.rb
@@ -129,7 +131,8 @@ files:
129
131
  homepage: https://github.com/jnunemaker/httparty
130
132
  licenses:
131
133
  - MIT
132
- metadata: {}
134
+ metadata:
135
+ changelog_uri: https://github.com/jnunemaker/httparty/releases
133
136
  post_install_message: When you HTTParty, you must party hard!
134
137
  rdoc_options: []
135
138
  require_paths: