httparty 0.19.0 → 0.24.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.
@@ -0,0 +1,188 @@
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
+ end
139
+ header << NEWLINE.b
140
+ header
141
+ end
142
+
143
+ def calculate_size
144
+ total = 0
145
+ @parts.each do |key, value, is_file|
146
+ total += build_part_header(key, value, is_file).bytesize
147
+ total += content_size(value, is_file)
148
+ total += NEWLINE.bytesize
149
+ end
150
+ total += "--#{@boundary}--#{NEWLINE}".bytesize
151
+ total
152
+ end
153
+
154
+ def content_size(value, is_file)
155
+ if is_file
156
+ if value.respond_to?(:size)
157
+ value.size
158
+ elsif value.respond_to?(:stat)
159
+ value.stat.size
160
+ else
161
+ value.read.bytesize.tap { value.rewind }
162
+ end
163
+ else
164
+ value.to_s.b.bytesize
165
+ end
166
+ end
167
+
168
+ def content_type(object)
169
+ return object.content_type if object.respond_to?(:content_type)
170
+ require 'mini_mime'
171
+ mime = MiniMime.lookup_by_filename(object.path)
172
+ mime ? mime.content_type : 'application/octet-stream'
173
+ end
174
+
175
+ def file_name(object)
176
+ object.respond_to?(:original_filename) ? object.original_filename : File.basename(object.path)
177
+ end
178
+
179
+ def replacement_table
180
+ @replacement_table ||= {
181
+ '"' => '%22',
182
+ "\r" => '%0D',
183
+ "\n" => '%0A'
184
+ }.freeze
185
+ end
186
+ end
187
+ end
188
+ end
@@ -46,6 +46,15 @@ module HTTParty
46
46
  end.flatten.join('&')
47
47
  end
48
48
 
49
+ def self._load(data)
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
56
+ end
57
+
49
58
  attr_accessor :http_method, :options, :last_response, :redirect, :last_uri
50
59
  attr_reader :path
51
60
 
@@ -104,6 +113,8 @@ module HTTParty
104
113
  new_uri = path.clone
105
114
  end
106
115
 
116
+ validate_uri_safety!(new_uri) unless redirect
117
+
107
118
  # avoid double query string on redirects [#12]
108
119
  unless redirect
109
120
  new_uri.query = query_string(new_uri)
@@ -144,24 +155,28 @@ module HTTParty
144
155
  chunked_body = nil
145
156
  current_http = http
146
157
 
147
- self.last_response = current_http.request(@raw_request) do |http_response|
148
- if block
149
- chunks = []
158
+ begin
159
+ self.last_response = current_http.request(@raw_request) do |http_response|
160
+ if block
161
+ chunks = []
150
162
 
151
- http_response.read_body do |fragment|
152
- encoded_fragment = encode_text(fragment, http_response['content-type'])
153
- chunks << encoded_fragment if !options[:stream_body]
154
- block.call ResponseFragment.new(encoded_fragment, http_response, current_http)
155
- 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
156
168
 
157
- chunked_body = chunks.join
169
+ chunked_body = chunks.join
170
+ end
158
171
  end
159
- end
160
172
 
161
- handle_host_redirection if response_redirects?
162
- result = handle_unauthorized
163
- result ||= handle_response(chunked_body, &block)
164
- 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
165
180
  end
166
181
 
167
182
  def handle_unauthorized(&block)
@@ -175,6 +190,13 @@ module HTTParty
175
190
  @raw_request.body
176
191
  end
177
192
 
193
+ def _dump(_level)
194
+ opts = options.dup
195
+ opts.delete(:logger)
196
+ opts.delete(:parser) if opts[:parser] && opts[:parser].is_a?(Proc)
197
+ Marshal.dump([http_method, path, opts, last_response, @last_uri, @raw_request])
198
+ end
199
+
178
200
  private
179
201
 
180
202
  def http
@@ -225,8 +247,17 @@ module HTTParty
225
247
  if body.multipart?
226
248
  content_type = "multipart/form-data; boundary=#{body.boundary}"
227
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] != false
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
228
260
  end
229
- @raw_request.body = body.call
230
261
  end
231
262
 
232
263
  @raw_request.instance_variable_set(:@decode_content, decompress_content?)
@@ -279,24 +310,7 @@ module HTTParty
279
310
 
280
311
  def handle_response(raw_body, &block)
281
312
  if response_redirects?
282
- options[:limit] -= 1
283
- if options[:logger]
284
- logger = HTTParty::Logger.build(options[:logger], options[:log_level], options[:log_format])
285
- logger.format(self, last_response)
286
- end
287
- self.path = last_response['location']
288
- self.redirect = true
289
- if last_response.class == Net::HTTPSeeOther
290
- unless options[:maintain_method_across_redirects] && options[:resend_on_redirect]
291
- self.http_method = Net::HTTP::Get
292
- end
293
- elsif last_response.code != '307' && last_response.code != '308'
294
- unless options[:maintain_method_across_redirects]
295
- self.http_method = Net::HTTP::Get
296
- end
297
- end
298
- capture_cookies(last_response)
299
- perform(&block)
313
+ handle_redirection(&block)
300
314
  else
301
315
  raw_body ||= last_response.body
302
316
 
@@ -315,10 +329,34 @@ module HTTParty
315
329
  end
316
330
  end
317
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
+
318
356
  def handle_host_redirection
319
357
  check_duplicate_location_header
320
358
  redirect_path = options[:uri_adapter].parse(last_response['location']).normalize
321
- 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
322
360
  @changed_hosts = true
323
361
  end
324
362
 
@@ -346,6 +384,14 @@ module HTTParty
346
384
  parser.call(body, format)
347
385
  end
348
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
+
349
395
  def capture_cookies(response)
350
396
  return unless response['Set-Cookie']
351
397
  cookies_hash = HTTParty::CookieHash.new
@@ -398,5 +444,23 @@ module HTTParty
398
444
  assume_utf16_is_big_endian: assume_utf16_is_big_endian
399
445
  ).call
400
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
401
465
  end
402
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.19.0'
4
+ VERSION = '0.24.0'
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'
@@ -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.19.0
4
+ version: 0.24.0
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-09 00:00:00.000000000 Z
12
+ date: 2025-12-28 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.0.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"