httparty 0.21.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.
@@ -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?)
@@ -295,24 +310,7 @@ module HTTParty
295
310
 
296
311
  def handle_response(raw_body, &block)
297
312
  if response_redirects?
298
- options[:limit] -= 1
299
- if options[:logger]
300
- logger = HTTParty::Logger.build(options[:logger], options[:log_level], options[:log_format])
301
- logger.format(self, last_response)
302
- end
303
- self.path = last_response['location']
304
- self.redirect = true
305
- if last_response.class == Net::HTTPSeeOther
306
- unless options[:maintain_method_across_redirects] && options[:resend_on_redirect]
307
- self.http_method = Net::HTTP::Get
308
- end
309
- elsif last_response.code != '307' && last_response.code != '308'
310
- unless options[:maintain_method_across_redirects]
311
- self.http_method = Net::HTTP::Get
312
- end
313
- end
314
- capture_cookies(last_response)
315
- perform(&block)
313
+ handle_redirection(&block)
316
314
  else
317
315
  raw_body ||= last_response.body
318
316
 
@@ -331,10 +329,34 @@ module HTTParty
331
329
  end
332
330
  end
333
331
 
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
343
+ 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
347
+ end
348
+ end
349
+ if http_method == Net::HTTP::Get
350
+ clear_body
351
+ end
352
+ capture_cookies(last_response)
353
+ perform(&block)
354
+ end
355
+
334
356
  def handle_host_redirection
335
357
  check_duplicate_location_header
336
358
  redirect_path = options[:uri_adapter].parse(last_response['location']).normalize
337
- return if redirect_path.relative? || path.host == redirect_path.host
359
+ return if redirect_path.relative? || path.host == redirect_path.host || uri.host == redirect_path.host
338
360
  @changed_hosts = true
339
361
  end
340
362
 
@@ -362,6 +384,14 @@ module HTTParty
362
384
  parser.call(body, format)
363
385
  end
364
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
+
365
395
  def capture_cookies(response)
366
396
  return unless response['Set-Cookie']
367
397
  cookies_hash = HTTParty::CookieHash.new
@@ -414,5 +444,23 @@ module HTTParty
414
444
  assume_utf16_is_big_endian: assume_utf16_is_big_endian
415
445
  ).call
416
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
417
465
  end
418
466
  end
@@ -67,12 +67,12 @@ module HTTParty
67
67
  end
68
68
 
69
69
  # Support old multiple_choice? method from pre 2.0.0 era.
70
- if ::RUBY_VERSION >= '2.0.0' && ::RUBY_PLATFORM != 'java'
70
+ if ::RUBY_PLATFORM != 'java'
71
71
  alias_method :multiple_choice?, :multiple_choices?
72
72
  end
73
73
 
74
74
  # Support old status codes method from pre 2.6.0 era.
75
- if ::RUBY_VERSION >= '2.6.0' && ::RUBY_PLATFORM != 'java'
75
+ if ::RUBY_PLATFORM != 'java'
76
76
  alias_method :gateway_time_out?, :gateway_timeout?
77
77
  alias_method :request_entity_too_large?, :payload_too_large?
78
78
  alias_method :request_time_out?, :request_timeout?
@@ -133,7 +133,7 @@ module HTTParty
133
133
  end
134
134
 
135
135
  def throw_exception
136
- if @request.options[:raise_on] && @request.options[:raise_on].include?(code)
136
+ if @request.options[:raise_on].to_a.detect { |c| code.to_s.match(/#{c.to_s}/) }
137
137
  ::Kernel.raise ::HTTParty::ResponseError.new(@response), "Code #{code} - #{body}"
138
138
  end
139
139
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTParty
4
- VERSION = '0.21.0'
4
+ VERSION = '0.24.2'
5
5
  end
data/lib/httparty.rb CHANGED
@@ -3,11 +3,6 @@
3
3
  require 'pathname'
4
4
  require 'net/http'
5
5
  require 'uri'
6
- require 'zlib'
7
- require 'multi_xml'
8
- require 'mini_mime'
9
- require 'json'
10
- require 'csv'
11
6
 
12
7
  require 'httparty/module_inheritable_attributes'
13
8
  require 'httparty/cookie_hash'
@@ -46,7 +41,7 @@ module HTTParty
46
41
  # [:+local_host+:] Local address to bind to before connecting.
47
42
  # [:+local_port+:] Local port to bind to before connecting.
48
43
  # [:+body_stream+:] Allow streaming to a REST server to specify a body_stream.
49
- # [:+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).
50
45
  # [:+multipart+:] Force content-type to be multipart
51
46
  #
52
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:
@@ -67,6 +62,16 @@ module HTTParty
67
62
  # * :+ssl_ca_path+: see HTTParty::ClassMethods.ssl_ca_path.
68
63
 
69
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
+
70
75
  # Turns on logging
71
76
  #
72
77
  # class Foo
@@ -83,7 +88,7 @@ module HTTParty
83
88
  #
84
89
  # class Foo
85
90
  # include HTTParty
86
- # raise_on [404, 500]
91
+ # raise_on [404, 500, '5[0-9]*']
87
92
  # end
88
93
  def raise_on(codes = [])
89
94
  default_options[:raise_on] = *codes
@@ -591,6 +596,13 @@ module HTTParty
591
596
  perform_request Net::HTTP::Unlock, path, options, &block
592
597
  end
593
598
 
599
+ def build_request(http_method, path, options = {})
600
+ options = ModuleInheritableAttributes.hash_deep_dup(default_options).merge(options)
601
+ HeadersProcessor.new(headers, options).call
602
+ process_cookies(options)
603
+ Request.new(http_method, path, options)
604
+ end
605
+
594
606
  attr_reader :default_options
595
607
 
596
608
  private
@@ -606,10 +618,7 @@ module HTTParty
606
618
  end
607
619
 
608
620
  def perform_request(http_method, path, options, &block) #:nodoc:
609
- options = ModuleInheritableAttributes.hash_deep_dup(default_options).merge(options)
610
- HeadersProcessor.new(headers, options).call
611
- process_cookies(options)
612
- Request.new(http_method, path, options).perform(&block)
621
+ build_request(http_method, path, options).perform(&block)
613
622
  end
614
623
 
615
624
  def process_cookies(options) #:nodoc:
@@ -676,6 +685,10 @@ module HTTParty
676
685
  def self.options(*args, &block)
677
686
  Basement.options(*args, &block)
678
687
  end
688
+
689
+ def self.build_request(*args, &block)
690
+ Basement.build_request(*args, &block)
691
+ end
679
692
  end
680
693
 
681
694
  require 'httparty/hash_conversions'
data/script/release CHANGED
@@ -18,9 +18,9 @@ gem_name=httparty
18
18
  rm -rf $gem_name-*.gem
19
19
  gem build -q $gem_name.gemspec
20
20
 
21
- # Make sure we're on the master branch.
22
- (git branch | grep -q '* master') || {
23
- echo "Only release from the master branch."
21
+ # Make sure we're on the main branch.
22
+ (git branch | grep -q '* main') || {
23
+ echo "Only release from the main branch."
24
24
  exit 1
25
25
  }
26
26
 
@@ -39,4 +39,4 @@ git fetch -t origin
39
39
 
40
40
  # Tag it and bag it.
41
41
  gem push $gem_name-*.gem && git tag "$tag" &&
42
- git push origin master && git push origin "$tag"
42
+ git push origin main && git push origin "$tag"
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.21.0
4
+ version: 0.24.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
@@ -9,8 +9,22 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-12-30 00:00:00.000000000 Z
12
+ date: 2026-01-14 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: csv
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
14
28
  - !ruby/object:Gem::Dependency
15
29
  name: multi_xml
16
30
  requirement: !ruby/object:Gem::Requirement
@@ -48,6 +62,7 @@ extensions: []
48
62
  extra_rdoc_files: []
49
63
  files:
50
64
  - ".editorconfig"
65
+ - ".github/dependabot.yml"
51
66
  - ".github/workflows/ci.yml"
52
67
  - ".gitignore"
53
68
  - ".rubocop.yml"
@@ -76,6 +91,7 @@ files:
76
91
  - examples/microsoft_graph.rb
77
92
  - examples/multipart.rb
78
93
  - examples/nokogiri_html_parser.rb
94
+ - examples/party_foul_mode.rb
79
95
  - examples/peer_cert.rb
80
96
  - examples/rescue_json.rb
81
97
  - examples/rubyurl.rb
@@ -102,6 +118,7 @@ files:
102
118
  - lib/httparty/request.rb
103
119
  - lib/httparty/request/body.rb
104
120
  - lib/httparty/request/multipart_boundary.rb
121
+ - lib/httparty/request/streaming_multipart_body.rb
105
122
  - lib/httparty/response.rb
106
123
  - lib/httparty/response/headers.rb
107
124
  - lib/httparty/response_fragment.rb
@@ -114,7 +131,8 @@ files:
114
131
  homepage: https://github.com/jnunemaker/httparty
115
132
  licenses:
116
133
  - MIT
117
- metadata: {}
134
+ metadata:
135
+ changelog_uri: https://github.com/jnunemaker/httparty/releases
118
136
  post_install_message: When you HTTParty, you must party hard!
119
137
  rdoc_options: []
120
138
  require_paths:
@@ -123,7 +141,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
123
141
  requirements:
124
142
  - - ">="
125
143
  - !ruby/object:Gem::Version
126
- version: 2.3.0
144
+ version: 2.7.0
127
145
  required_rubygems_version: !ruby/object:Gem::Requirement
128
146
  requirements:
129
147
  - - ">="