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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +10 -14
- data/.gitignore +2 -1
- data/Changelog.md +399 -311
- data/Gemfile +3 -0
- data/Guardfile +3 -2
- data/README.md +17 -17
- data/docs/README.md +27 -5
- data/examples/party_foul_mode.rb +90 -0
- data/httparty.gemspec +4 -2
- data/lib/httparty/connection_adapter.rb +6 -25
- data/lib/httparty/decompressor.rb +11 -1
- data/lib/httparty/exceptions.rb +34 -3
- data/lib/httparty/hash_conversions.rb +1 -1
- data/lib/httparty/logger/curl_formatter.rb +1 -1
- data/lib/httparty/logger/logstash_formatter.rb +1 -0
- data/lib/httparty/module_inheritable_attributes.rb +3 -5
- data/lib/httparty/parser.rb +3 -0
- data/lib/httparty/request/body.rb +40 -13
- data/lib/httparty/request/streaming_multipart_body.rb +188 -0
- data/lib/httparty/request.rb +98 -34
- data/lib/httparty/response.rb +3 -3
- data/lib/httparty/text_encoder.rb +1 -1
- data/lib/httparty/version.rb +1 -1
- data/lib/httparty.rb +23 -11
- data/script/release +4 -4
- metadata +30 -13
- data/.simplecov +0 -1
data/Gemfile
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
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'
|
|
7
|
+
gem 'json'
|
|
6
8
|
|
|
7
9
|
group :development do
|
|
8
10
|
gem 'guard'
|
|
@@ -11,6 +13,7 @@ group :development do
|
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
group :test do
|
|
16
|
+
gem 'rexml'
|
|
14
17
|
gem 'rspec', '~> 3.4'
|
|
15
18
|
gem 'simplecov', require: false
|
|
16
19
|
gem 'aruba'
|
data/Guardfile
CHANGED
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# httparty
|
|
2
2
|
|
|
3
|
-
[](https://github.com/jnunemaker/httparty/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
Makes http fun again!
|
|
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
|
-
|
|
16
|
-
|
|
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/
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
@@ -14,6 +14,20 @@ response = HTTParty.get('http://example.com', format: :plain)
|
|
|
14
14
|
JSON.parse response, symbolize_names: true
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
## Posting JSON
|
|
18
|
+
When using Content Type `application/json` with `POST`, `PUT` or `PATCH` requests, the body should be a string of valid JSON:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# With written JSON
|
|
22
|
+
HTTParty.post('http://example.com', body: "{\"foo\":\"bar\"}", headers: { 'Content-Type' => 'application/json' })
|
|
23
|
+
|
|
24
|
+
# Using JSON.generate
|
|
25
|
+
HTTParty.post('http://example.com', body: JSON.generate({ foo: 'bar' }), headers: { 'Content-Type' => 'application/json' })
|
|
26
|
+
|
|
27
|
+
# Using object.to_json
|
|
28
|
+
HTTParty.post('http://example.com', body: { foo: 'bar' }.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
29
|
+
```
|
|
30
|
+
|
|
17
31
|
## Working with SSL
|
|
18
32
|
|
|
19
33
|
You can use this guide to work with SSL certificates.
|
|
@@ -22,6 +36,8 @@ You can use this guide to work with SSL certificates.
|
|
|
22
36
|
|
|
23
37
|
```ruby
|
|
24
38
|
# Use this example if you are using a pem file
|
|
39
|
+
# - cert.pem must contain the content of a PEM file having the private key appended (separated from the cert by a newline \n)
|
|
40
|
+
# - Use an empty string for the password if the cert is not password protected
|
|
25
41
|
|
|
26
42
|
class Client
|
|
27
43
|
include HTTParty
|
|
@@ -123,11 +139,16 @@ If you explicitly set `Accept-Encoding`, there be dragons:
|
|
|
123
139
|
`Net::HTTP` will automatically decompress it, and will omit `Content-Encoding`
|
|
124
140
|
from your `HTTParty::Response` headers.
|
|
125
141
|
|
|
126
|
-
* For encodings
|
|
127
|
-
|
|
142
|
+
* For the following encodings, HTTParty will automatically decompress them if you include
|
|
143
|
+
the required gem into your project. Similar to above, if decompression succeeds,
|
|
144
|
+
`Content-Encoding` will be omitted from your `HTTParty::Response` headers.
|
|
128
145
|
**Warning:** Support for these encodings is experimental and not fully battle-tested.
|
|
129
|
-
|
|
130
|
-
|
|
146
|
+
|
|
147
|
+
| Content-Encoding | Required Gem |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| `br` (Brotli) | [brotli](https://rubygems.org/gems/brotli) |
|
|
150
|
+
| `compress` (LZW) | [ruby-lzws](https://rubygems.org/gems/ruby-lzws) |
|
|
151
|
+
| `zstd` (Zstandard) | [zstd-ruby](https://rubygems.org/gems/zstd-ruby) |
|
|
131
152
|
|
|
132
153
|
* For other encodings, `HTTParty::Response#body` will return the raw uncompressed byte string,
|
|
133
154
|
and you'll need to inspect the `Content-Encoding` response header and decompress it yourself.
|
|
@@ -147,7 +168,8 @@ JSON.parse(res.body) # safe
|
|
|
147
168
|
|
|
148
169
|
require 'brotli'
|
|
149
170
|
require 'lzws'
|
|
150
|
-
|
|
171
|
+
require 'zstd-ruby'
|
|
172
|
+
res = HTTParty.get('https://example.com/test.json', headers: { 'Accept-Encoding' => 'br,compress,zstd' })
|
|
151
173
|
JSON.parse(res.body)
|
|
152
174
|
|
|
153
175
|
|
|
@@ -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,11 +12,13 @@ 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
|
-
s.required_ruby_version = '>= 2.
|
|
17
|
+
s.required_ruby_version = '>= 2.7.0'
|
|
17
18
|
|
|
19
|
+
s.add_dependency 'csv'
|
|
18
20
|
s.add_dependency 'multi_xml', ">= 0.5.2"
|
|
19
|
-
s.add_dependency
|
|
21
|
+
s.add_dependency 'mini_mime', ">= 1.0.0"
|
|
20
22
|
|
|
21
23
|
# If this line is removed, all hard partying will cease.
|
|
22
24
|
s.post_install_message = "When you HTTParty, you must party hard!"
|
|
@@ -119,10 +119,7 @@ module HTTParty
|
|
|
119
119
|
if add_timeout?(options[:timeout])
|
|
120
120
|
http.open_timeout = options[:timeout]
|
|
121
121
|
http.read_timeout = options[:timeout]
|
|
122
|
-
|
|
123
|
-
from_ruby_version('2.6.0', option: :write_timeout, warn: false) do
|
|
124
|
-
http.write_timeout = options[:timeout]
|
|
125
|
-
end
|
|
122
|
+
http.write_timeout = options[:timeout]
|
|
126
123
|
end
|
|
127
124
|
|
|
128
125
|
if add_timeout?(options[:read_timeout])
|
|
@@ -134,15 +131,11 @@ module HTTParty
|
|
|
134
131
|
end
|
|
135
132
|
|
|
136
133
|
if add_timeout?(options[:write_timeout])
|
|
137
|
-
|
|
138
|
-
http.write_timeout = options[:write_timeout]
|
|
139
|
-
end
|
|
134
|
+
http.write_timeout = options[:write_timeout]
|
|
140
135
|
end
|
|
141
136
|
|
|
142
137
|
if add_max_retries?(options[:max_retries])
|
|
143
|
-
|
|
144
|
-
http.max_retries = options[:max_retries]
|
|
145
|
-
end
|
|
138
|
+
http.max_retries = options[:max_retries]
|
|
146
139
|
end
|
|
147
140
|
|
|
148
141
|
if options[:debug_output]
|
|
@@ -157,15 +150,11 @@ module HTTParty
|
|
|
157
150
|
#
|
|
158
151
|
# @see https://bugs.ruby-lang.org/issues/6617
|
|
159
152
|
if options[:local_host]
|
|
160
|
-
|
|
161
|
-
http.local_host = options[:local_host]
|
|
162
|
-
end
|
|
153
|
+
http.local_host = options[:local_host]
|
|
163
154
|
end
|
|
164
155
|
|
|
165
156
|
if options[:local_port]
|
|
166
|
-
|
|
167
|
-
http.local_port = options[:local_port]
|
|
168
|
-
end
|
|
157
|
+
http.local_port = options[:local_port]
|
|
169
158
|
end
|
|
170
159
|
|
|
171
160
|
http
|
|
@@ -173,14 +162,6 @@ module HTTParty
|
|
|
173
162
|
|
|
174
163
|
private
|
|
175
164
|
|
|
176
|
-
def from_ruby_version(ruby_version, option: nil, warn: true)
|
|
177
|
-
if RUBY_VERSION >= ruby_version
|
|
178
|
-
yield
|
|
179
|
-
elsif warn
|
|
180
|
-
Kernel.warn("Warning: option #{ option } requires Ruby version #{ ruby_version } or later")
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
|
|
184
165
|
def add_timeout?(timeout)
|
|
185
166
|
timeout && (timeout.is_a?(Integer) || timeout.is_a?(Float))
|
|
186
167
|
end
|
|
@@ -223,7 +204,7 @@ module HTTParty
|
|
|
223
204
|
# Note: options[:pem] must contain the content of a PEM file having the private key appended
|
|
224
205
|
if options[:pem]
|
|
225
206
|
http.cert = OpenSSL::X509::Certificate.new(options[:pem])
|
|
226
|
-
http.key = OpenSSL::PKey
|
|
207
|
+
http.key = OpenSSL::PKey.read(options[:pem], options[:pem_password])
|
|
227
208
|
http.verify_mode = verify_ssl_certificate? ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
|
228
209
|
end
|
|
229
210
|
|
|
@@ -17,7 +17,8 @@ module HTTParty
|
|
|
17
17
|
'none' => :none,
|
|
18
18
|
'identity' => :none,
|
|
19
19
|
'br' => :brotli,
|
|
20
|
-
'compress' => :lzw
|
|
20
|
+
'compress' => :lzw,
|
|
21
|
+
'zstd' => :zstd
|
|
21
22
|
}.freeze
|
|
22
23
|
|
|
23
24
|
# The response body of the request
|
|
@@ -88,5 +89,14 @@ module HTTParty
|
|
|
88
89
|
nil
|
|
89
90
|
end
|
|
90
91
|
end
|
|
92
|
+
|
|
93
|
+
def zstd
|
|
94
|
+
return nil unless defined?(::Zstd)
|
|
95
|
+
begin
|
|
96
|
+
::Zstd.decompress(body)
|
|
97
|
+
rescue StandardError
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
end
|
|
91
101
|
end
|
|
92
102
|
end
|
data/lib/httparty/exceptions.rb
CHANGED
|
@@ -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 <
|
|
32
|
+
class UnsupportedFormat < Foul; end
|
|
9
33
|
|
|
10
34
|
# Exception raised when using a URI scheme other than HTTP or HTTPS
|
|
11
|
-
class UnsupportedURIScheme <
|
|
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 <
|
|
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
|
|
@@ -28,7 +28,7 @@ module HTTParty
|
|
|
28
28
|
def self.normalize_param(key, value)
|
|
29
29
|
normalized_keys = normalize_keys(key, value)
|
|
30
30
|
|
|
31
|
-
normalized_keys.flatten.each_slice(2).inject(
|
|
31
|
+
normalized_keys.flatten.each_slice(2).inject(''.dup) do |string, (k, v)|
|
|
32
32
|
string << "#{ERB::Util.url_encode(k)}=#{ERB::Util.url_encode(v.to_s)}&"
|
|
33
33
|
end
|
|
34
34
|
end
|
|
@@ -29,7 +29,7 @@ module HTTParty
|
|
|
29
29
|
@mattr_inheritable_attrs += args
|
|
30
30
|
|
|
31
31
|
args.each do |arg|
|
|
32
|
-
|
|
32
|
+
singleton_class.attr_accessor(arg)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
@mattr_inheritable_attrs
|
|
@@ -42,14 +42,12 @@ module HTTParty
|
|
|
42
42
|
subclass.instance_variable_set(ivar, instance_variable_get(ivar).clone)
|
|
43
43
|
|
|
44
44
|
if instance_variable_get(ivar).respond_to?(:merge)
|
|
45
|
-
|
|
45
|
+
subclass.class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
46
46
|
def self.#{inheritable_attribute}
|
|
47
47
|
duplicate = ModuleInheritableAttributes.hash_deep_dup(#{ivar})
|
|
48
48
|
#{ivar} = superclass.#{inheritable_attribute}.merge(duplicate)
|
|
49
49
|
end
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
subclass.class_eval method
|
|
50
|
+
RUBY
|
|
53
51
|
end
|
|
54
52
|
end
|
|
55
53
|
end
|
data/lib/httparty/parser.rb
CHANGED
|
@@ -118,16 +118,19 @@ module HTTParty
|
|
|
118
118
|
protected
|
|
119
119
|
|
|
120
120
|
def xml
|
|
121
|
+
require 'multi_xml'
|
|
121
122
|
MultiXml.parse(body)
|
|
122
123
|
end
|
|
123
124
|
|
|
124
125
|
UTF8_BOM = "\xEF\xBB\xBF"
|
|
125
126
|
|
|
126
127
|
def json
|
|
128
|
+
require 'json'
|
|
127
129
|
JSON.parse(body, :quirks_mode => true, :allow_nan => true)
|
|
128
130
|
end
|
|
129
131
|
|
|
130
132
|
def csv
|
|
133
|
+
require 'csv'
|
|
131
134
|
CSV.parse(body)
|
|
132
135
|
end
|
|
133
136
|
|
|
@@ -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,25 +31,48 @@ 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
|
|
|
52
|
+
# https://html.spec.whatwg.org/#multipart-form-data
|
|
53
|
+
MULTIPART_FORM_DATA_REPLACEMENT_TABLE = {
|
|
54
|
+
'"' => '%22',
|
|
55
|
+
"\r" => '%0D',
|
|
56
|
+
"\n" => '%0A'
|
|
57
|
+
}.freeze
|
|
58
|
+
|
|
35
59
|
def generate_multipart
|
|
36
60
|
normalized_params = params.flat_map { |key, value| HashConversions.normalize_keys(key, value) }
|
|
37
61
|
|
|
38
|
-
multipart = normalized_params.inject(
|
|
39
|
-
memo << "--#{boundary}#{NEWLINE}"
|
|
40
|
-
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
|
|
41
65
|
# value.path is used to support ActionDispatch::Http::UploadedFile
|
|
42
66
|
# https://github.com/jnunemaker/httparty/pull/585
|
|
43
|
-
memo << %(; filename="#{file_name(value)}") if file?(value)
|
|
44
|
-
memo << NEWLINE
|
|
45
|
-
memo << "Content-Type: #{content_type(value)}#{NEWLINE}" if file?(value)
|
|
46
|
-
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
|
|
47
71
|
memo << content_body(value)
|
|
48
|
-
memo << NEWLINE
|
|
72
|
+
memo << NEWLINE.b
|
|
49
73
|
end
|
|
50
74
|
|
|
51
|
-
multipart << "--#{boundary}--#{NEWLINE}"
|
|
75
|
+
multipart << "--#{boundary}--#{NEWLINE}".b
|
|
52
76
|
end
|
|
53
77
|
|
|
54
78
|
def has_file?(value)
|
|
@@ -76,16 +100,19 @@ module HTTParty
|
|
|
76
100
|
def content_body(object)
|
|
77
101
|
if file?(object)
|
|
78
102
|
object = (file = object).read
|
|
103
|
+
object.force_encoding(Encoding::BINARY) if object.respond_to?(:force_encoding)
|
|
79
104
|
file.rewind if file.respond_to?(:rewind)
|
|
105
|
+
object.to_s
|
|
106
|
+
else
|
|
107
|
+
object.to_s.b
|
|
80
108
|
end
|
|
81
|
-
|
|
82
|
-
object.to_s
|
|
83
109
|
end
|
|
84
110
|
|
|
85
111
|
def content_type(object)
|
|
86
112
|
return object.content_type if object.respond_to?(:content_type)
|
|
87
|
-
|
|
88
|
-
mime
|
|
113
|
+
require 'mini_mime'
|
|
114
|
+
mime = MiniMime.lookup_by_filename(object.path)
|
|
115
|
+
mime ? mime.content_type : 'application/octet-stream'
|
|
89
116
|
end
|
|
90
117
|
|
|
91
118
|
def file_name(object)
|