httparty 0.20.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
@@ -47,8 +47,12 @@ module HTTParty
47
47
  end
48
48
 
49
49
  def self._load(data)
50
- http_method, path, options = Marshal.load(data)
51
- new(http_method, path, options)
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
52
56
  end
53
57
 
54
58
  attr_accessor :http_method, :options, :last_response, :redirect, :last_uri
@@ -109,6 +113,8 @@ module HTTParty
109
113
  new_uri = path.clone
110
114
  end
111
115
 
116
+ validate_uri_safety!(new_uri) unless redirect
117
+
112
118
  # avoid double query string on redirects [#12]
113
119
  unless redirect
114
120
  new_uri.query = query_string(new_uri)
@@ -149,24 +155,28 @@ module HTTParty
149
155
  chunked_body = nil
150
156
  current_http = http
151
157
 
152
- self.last_response = current_http.request(@raw_request) do |http_response|
153
- if block
154
- chunks = []
158
+ begin
159
+ self.last_response = current_http.request(@raw_request) do |http_response|
160
+ if block
161
+ chunks = []
155
162
 
156
- http_response.read_body do |fragment|
157
- encoded_fragment = encode_text(fragment, http_response['content-type'])
158
- chunks << encoded_fragment if !options[:stream_body]
159
- block.call ResponseFragment.new(encoded_fragment, http_response, current_http)
160
- 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
161
168
 
162
- chunked_body = chunks.join
169
+ chunked_body = chunks.join
170
+ end
163
171
  end
164
- end
165
172
 
166
- handle_host_redirection if response_redirects?
167
- result = handle_unauthorized
168
- result ||= handle_response(chunked_body, &block)
169
- 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
170
180
  end
171
181
 
172
182
  def handle_unauthorized(&block)
@@ -184,7 +194,7 @@ module HTTParty
184
194
  opts = options.dup
185
195
  opts.delete(:logger)
186
196
  opts.delete(:parser) if opts[:parser] && opts[:parser].is_a?(Proc)
187
- Marshal.dump([http_method, path, opts])
197
+ Marshal.dump([http_method, path, opts, last_response, @last_uri, @raw_request])
188
198
  end
189
199
 
190
200
  private
@@ -237,8 +247,17 @@ module HTTParty
237
247
  if body.multipart?
238
248
  content_type = "multipart/form-data; boundary=#{body.boundary}"
239
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
240
260
  end
241
- @raw_request.body = body.call
242
261
  end
243
262
 
244
263
  @raw_request.instance_variable_set(:@decode_content, decompress_content?)
@@ -291,24 +310,7 @@ module HTTParty
291
310
 
292
311
  def handle_response(raw_body, &block)
293
312
  if response_redirects?
294
- options[:limit] -= 1
295
- if options[:logger]
296
- logger = HTTParty::Logger.build(options[:logger], options[:log_level], options[:log_format])
297
- logger.format(self, last_response)
298
- end
299
- self.path = last_response['location']
300
- self.redirect = true
301
- if last_response.class == Net::HTTPSeeOther
302
- unless options[:maintain_method_across_redirects] && options[:resend_on_redirect]
303
- self.http_method = Net::HTTP::Get
304
- end
305
- elsif last_response.code != '307' && last_response.code != '308'
306
- unless options[:maintain_method_across_redirects]
307
- self.http_method = Net::HTTP::Get
308
- end
309
- end
310
- capture_cookies(last_response)
311
- perform(&block)
313
+ handle_redirection(&block)
312
314
  else
313
315
  raw_body ||= last_response.body
314
316
 
@@ -327,10 +329,34 @@ module HTTParty
327
329
  end
328
330
  end
329
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
+
330
356
  def handle_host_redirection
331
357
  check_duplicate_location_header
332
358
  redirect_path = options[:uri_adapter].parse(last_response['location']).normalize
333
- 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
334
360
  @changed_hosts = true
335
361
  end
336
362
 
@@ -358,6 +384,14 @@ module HTTParty
358
384
  parser.call(body, format)
359
385
  end
360
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
+
361
395
  def capture_cookies(response)
362
396
  return unless response['Set-Cookie']
363
397
  cookies_hash = HTTParty::CookieHash.new
@@ -410,5 +444,23 @@ module HTTParty
410
444
  assume_utf16_is_big_endian: assume_utf16_is_big_endian
411
445
  ).call
412
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
413
465
  end
414
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
@@ -5,7 +5,7 @@ module HTTParty
5
5
  attr_reader :text, :content_type, :assume_utf16_is_big_endian
6
6
 
7
7
  def initialize(text, assume_utf16_is_big_endian: true, content_type: nil)
8
- @text = text.dup
8
+ @text = +text
9
9
  @content_type = content_type
10
10
  @assume_utf16_is_big_endian = assume_utf16_is_big_endian
11
11
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTParty
4
- VERSION = '0.20.0'
4
+ VERSION = '0.24.2'
5
5
  end
data/lib/httparty.rb CHANGED
@@ -2,13 +2,7 @@
2
2
 
3
3
  require 'pathname'
4
4
  require 'net/http'
5
- require 'net/https'
6
5
  require 'uri'
7
- require 'zlib'
8
- require 'multi_xml'
9
- require 'mime/types'
10
- require 'json'
11
- require 'csv'
12
6
 
13
7
  require 'httparty/module_inheritable_attributes'
14
8
  require 'httparty/cookie_hash'
@@ -47,7 +41,7 @@ module HTTParty
47
41
  # [:+local_host+:] Local address to bind to before connecting.
48
42
  # [:+local_port+:] Local port to bind to before connecting.
49
43
  # [:+body_stream+:] Allow streaming to a REST server to specify a body_stream.
50
- # [:+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).
51
45
  # [:+multipart+:] Force content-type to be multipart
52
46
  #
53
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:
@@ -68,6 +62,16 @@ module HTTParty
68
62
  # * :+ssl_ca_path+: see HTTParty::ClassMethods.ssl_ca_path.
69
63
 
70
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
+
71
75
  # Turns on logging
72
76
  #
73
77
  # class Foo
@@ -84,7 +88,7 @@ module HTTParty
84
88
  #
85
89
  # class Foo
86
90
  # include HTTParty
87
- # raise_on [404, 500]
91
+ # raise_on [404, 500, '5[0-9]*']
88
92
  # end
89
93
  def raise_on(codes = [])
90
94
  default_options[:raise_on] = *codes
@@ -592,6 +596,13 @@ module HTTParty
592
596
  perform_request Net::HTTP::Unlock, path, options, &block
593
597
  end
594
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
+
595
606
  attr_reader :default_options
596
607
 
597
608
  private
@@ -607,10 +618,7 @@ module HTTParty
607
618
  end
608
619
 
609
620
  def perform_request(http_method, path, options, &block) #:nodoc:
610
- options = ModuleInheritableAttributes.hash_deep_dup(default_options).merge(options)
611
- HeadersProcessor.new(headers, options).call
612
- process_cookies(options)
613
- Request.new(http_method, path, options).perform(&block)
621
+ build_request(http_method, path, options).perform(&block)
614
622
  end
615
623
 
616
624
  def process_cookies(options) #:nodoc:
@@ -677,6 +685,10 @@ module HTTParty
677
685
  def self.options(*args, &block)
678
686
  Basement.options(*args, &block)
679
687
  end
688
+
689
+ def self.build_request(*args, &block)
690
+ Basement.build_request(*args, &block)
691
+ end
680
692
  end
681
693
 
682
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,16 +1,30 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: httparty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 0.24.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
8
  - Sandro Turriate
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-09-29 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
@@ -26,19 +40,19 @@ dependencies:
26
40
  - !ruby/object:Gem::Version
27
41
  version: 0.5.2
28
42
  - !ruby/object:Gem::Dependency
29
- name: mime-types
43
+ name: mini_mime
30
44
  requirement: !ruby/object:Gem::Requirement
31
45
  requirements:
32
- - - "~>"
46
+ - - ">="
33
47
  - !ruby/object:Gem::Version
34
- version: '3.0'
48
+ version: 1.0.0
35
49
  type: :runtime
36
50
  prerelease: false
37
51
  version_requirements: !ruby/object:Gem::Requirement
38
52
  requirements:
39
- - - "~>"
53
+ - - ">="
40
54
  - !ruby/object:Gem::Version
41
- version: '3.0'
55
+ version: 1.0.0
42
56
  description: Makes http fun! Also, makes consuming restful web services dead easy.
43
57
  email:
44
58
  - nunemaker@gmail.com
@@ -48,11 +62,11 @@ 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"
54
69
  - ".rubocop_todo.yml"
55
- - ".simplecov"
56
70
  - CONTRIBUTING.md
57
71
  - Changelog.md
58
72
  - Gemfile
@@ -77,6 +91,7 @@ files:
77
91
  - examples/microsoft_graph.rb
78
92
  - examples/multipart.rb
79
93
  - examples/nokogiri_html_parser.rb
94
+ - examples/party_foul_mode.rb
80
95
  - examples/peer_cert.rb
81
96
  - examples/rescue_json.rb
82
97
  - examples/rubyurl.rb
@@ -103,6 +118,7 @@ files:
103
118
  - lib/httparty/request.rb
104
119
  - lib/httparty/request/body.rb
105
120
  - lib/httparty/request/multipart_boundary.rb
121
+ - lib/httparty/request/streaming_multipart_body.rb
106
122
  - lib/httparty/response.rb
107
123
  - lib/httparty/response/headers.rb
108
124
  - lib/httparty/response_fragment.rb
@@ -115,7 +131,8 @@ files:
115
131
  homepage: https://github.com/jnunemaker/httparty
116
132
  licenses:
117
133
  - MIT
118
- metadata: {}
134
+ metadata:
135
+ changelog_uri: https://github.com/jnunemaker/httparty/releases
119
136
  post_install_message: When you HTTParty, you must party hard!
120
137
  rdoc_options: []
121
138
  require_paths:
@@ -124,15 +141,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
124
141
  requirements:
125
142
  - - ">="
126
143
  - !ruby/object:Gem::Version
127
- version: 2.3.0
144
+ version: 2.7.0
128
145
  required_rubygems_version: !ruby/object:Gem::Requirement
129
146
  requirements:
130
147
  - - ">="
131
148
  - !ruby/object:Gem::Version
132
149
  version: '0'
133
150
  requirements: []
134
- rubygems_version: 3.0.3
135
- signing_key:
151
+ rubygems_version: 3.3.7
152
+ signing_key:
136
153
  specification_version: 4
137
154
  summary: Makes http fun! Also, makes consuming restful web services dead easy.
138
155
  test_files: []
data/.simplecov DELETED
@@ -1 +0,0 @@
1
- SimpleCov.start "test_frameworks"