httparty 0.23.1 → 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 +4 -4
- data/docs/README.md +30 -0
- data/examples/README.md +1 -0
- data/examples/multipart.rb +13 -0
- data/httparty.gemspec +1 -0
- data/lib/httparty/exceptions.rb +4 -0
- data/lib/httparty/request/body.rb +30 -11
- data/lib/httparty/request/streaming_multipart_body.rb +190 -0
- data/lib/httparty/request.rb +30 -1
- data/lib/httparty/version.rb +1 -1
- data/lib/httparty.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5140fa45bdc5cad9c43dedc767d225a8d303df17e7684fb229e15b5b719c7581
|
|
4
|
+
data.tar.gz: df6926136fccc436033ebdec116d46d90c9d4cc41425e064f9caa8954d174ae2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0a9f6122cbddaf9929f6c0e2ecf790438b980b9c878ebb638a7ceb0f3404faf5ab722a0eeedc7d239513176a8806c5bd7963316228ab3cedc4e5a592fdea0f63
|
|
7
|
+
data.tar.gz: 140dd82771ccf6d1e97d427c7e60c00728e05389124a68b1dc9a6b24ef4fdbd2356dd2d1b3e7836afa17b70020fdff7f3e2a652c34eb6945cb40ef546cfa82a4
|
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.
|
data/examples/README.md
CHANGED
data/examples/multipart.rb
CHANGED
|
@@ -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
|
+
)
|
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
|
|
data/lib/httparty/exceptions.rb
CHANGED
|
@@ -59,4 +59,8 @@ module HTTParty
|
|
|
59
59
|
|
|
60
60
|
# Exception that is raised when common network errors occur.
|
|
61
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
|
|
62
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(''.
|
|
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
|
data/lib/httparty/request.rb
CHANGED
|
@@ -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)
|
|
@@ -245,8 +247,17 @@ module HTTParty
|
|
|
245
247
|
if body.multipart?
|
|
246
248
|
content_type = "multipart/form-data; boundary=#{body.boundary}"
|
|
247
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
|
|
248
260
|
end
|
|
249
|
-
@raw_request.body = body.call
|
|
250
261
|
end
|
|
251
262
|
|
|
252
263
|
@raw_request.instance_variable_set(:@decode_content, decompress_content?)
|
|
@@ -433,5 +444,23 @@ module HTTParty
|
|
|
433
444
|
assume_utf16_is_big_endian: assume_utf16_is_big_endian
|
|
434
445
|
).call
|
|
435
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
|
|
436
465
|
end
|
|
437
466
|
end
|
data/lib/httparty/version.rb
CHANGED
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+:]
|
|
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:
|
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.
|
|
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:
|
|
12
|
+
date: 2026-01-14 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: csv
|
|
@@ -118,6 +118,7 @@ files:
|
|
|
118
118
|
- lib/httparty/request.rb
|
|
119
119
|
- lib/httparty/request/body.rb
|
|
120
120
|
- lib/httparty/request/multipart_boundary.rb
|
|
121
|
+
- lib/httparty/request/streaming_multipart_body.rb
|
|
121
122
|
- lib/httparty/response.rb
|
|
122
123
|
- lib/httparty/response/headers.rb
|
|
123
124
|
- lib/httparty/response_fragment.rb
|
|
@@ -130,7 +131,8 @@ files:
|
|
|
130
131
|
homepage: https://github.com/jnunemaker/httparty
|
|
131
132
|
licenses:
|
|
132
133
|
- MIT
|
|
133
|
-
metadata:
|
|
134
|
+
metadata:
|
|
135
|
+
changelog_uri: https://github.com/jnunemaker/httparty/releases
|
|
134
136
|
post_install_message: When you HTTParty, you must party hard!
|
|
135
137
|
rdoc_options: []
|
|
136
138
|
require_paths:
|