http 5.3.1 → 6.0.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/CHANGELOG.md +241 -41
- data/LICENSE.txt +1 -1
- data/README.md +110 -13
- data/UPGRADING.md +491 -0
- data/http.gemspec +32 -29
- data/lib/http/base64.rb +11 -1
- data/lib/http/chainable/helpers.rb +62 -0
- data/lib/http/chainable/verbs.rb +136 -0
- data/lib/http/chainable.rb +232 -136
- data/lib/http/client.rb +158 -127
- data/lib/http/connection/internals.rb +141 -0
- data/lib/http/connection.rb +126 -97
- data/lib/http/content_type.rb +61 -6
- data/lib/http/errors.rb +25 -1
- data/lib/http/feature.rb +65 -5
- data/lib/http/features/auto_deflate.rb +124 -17
- data/lib/http/features/auto_inflate.rb +38 -15
- data/lib/http/features/caching/entry.rb +178 -0
- data/lib/http/features/caching/in_memory_store.rb +63 -0
- data/lib/http/features/caching.rb +216 -0
- data/lib/http/features/digest_auth.rb +234 -0
- data/lib/http/features/instrumentation.rb +97 -17
- data/lib/http/features/logging.rb +183 -5
- data/lib/http/features/normalize_uri.rb +17 -0
- data/lib/http/features/raise_error.rb +18 -3
- data/lib/http/form_data/composite_io.rb +106 -0
- data/lib/http/form_data/file.rb +95 -0
- data/lib/http/form_data/multipart/param.rb +62 -0
- data/lib/http/form_data/multipart.rb +106 -0
- data/lib/http/form_data/part.rb +52 -0
- data/lib/http/form_data/readable.rb +58 -0
- data/lib/http/form_data/urlencoded.rb +175 -0
- data/lib/http/form_data/version.rb +8 -0
- data/lib/http/form_data.rb +102 -0
- data/lib/http/headers/known.rb +3 -0
- data/lib/http/headers/normalizer.rb +17 -36
- data/lib/http/headers.rb +172 -65
- data/lib/http/mime_type/adapter.rb +24 -9
- data/lib/http/mime_type/json.rb +19 -4
- data/lib/http/mime_type.rb +21 -3
- data/lib/http/options/definitions.rb +189 -0
- data/lib/http/options.rb +172 -125
- data/lib/http/redirector.rb +80 -75
- data/lib/http/request/body.rb +87 -6
- data/lib/http/request/builder.rb +184 -0
- data/lib/http/request/proxy.rb +83 -0
- data/lib/http/request/writer.rb +76 -16
- data/lib/http/request.rb +214 -98
- data/lib/http/response/body.rb +103 -18
- data/lib/http/response/inflater.rb +35 -7
- data/lib/http/response/parser.rb +98 -4
- data/lib/http/response/status/reasons.rb +2 -4
- data/lib/http/response/status.rb +141 -31
- data/lib/http/response.rb +219 -61
- data/lib/http/retriable/delay_calculator.rb +38 -11
- data/lib/http/retriable/errors.rb +21 -0
- data/lib/http/retriable/performer.rb +82 -38
- data/lib/http/session.rb +280 -0
- data/lib/http/timeout/global.rb +147 -34
- data/lib/http/timeout/null.rb +155 -9
- data/lib/http/timeout/per_operation.rb +139 -18
- data/lib/http/uri/normalizer.rb +82 -0
- data/lib/http/uri/parsing.rb +182 -0
- data/lib/http/uri.rb +289 -124
- data/lib/http/version.rb +2 -1
- data/lib/http.rb +11 -2
- data/sig/deps.rbs +122 -0
- data/sig/http.rbs +1619 -0
- data/test/http/base64_test.rb +28 -0
- data/test/http/client_test.rb +739 -0
- data/test/http/connection_test.rb +1533 -0
- data/test/http/content_type_test.rb +190 -0
- data/test/http/errors_test.rb +28 -0
- data/test/http/feature_test.rb +49 -0
- data/test/http/features/auto_deflate_test.rb +317 -0
- data/test/http/features/auto_inflate_test.rb +213 -0
- data/test/http/features/caching_test.rb +942 -0
- data/test/http/features/digest_auth_test.rb +996 -0
- data/test/http/features/instrumentation_test.rb +246 -0
- data/test/http/features/logging_test.rb +654 -0
- data/test/http/features/normalize_uri_test.rb +41 -0
- data/test/http/features/raise_error_test.rb +77 -0
- data/test/http/form_data/composite_io_test.rb +215 -0
- data/test/http/form_data/file_test.rb +255 -0
- data/test/http/form_data/fixtures/the-http-gem.info +1 -0
- data/test/http/form_data/multipart_test.rb +303 -0
- data/test/http/form_data/part_test.rb +90 -0
- data/test/http/form_data/urlencoded_test.rb +164 -0
- data/test/http/form_data_test.rb +232 -0
- data/test/http/headers/normalizer_test.rb +93 -0
- data/test/http/headers_test.rb +888 -0
- data/test/http/mime_type/json_test.rb +39 -0
- data/test/http/mime_type_test.rb +150 -0
- data/test/http/options/base_uri_test.rb +148 -0
- data/test/http/options/body_test.rb +21 -0
- data/test/http/options/features_test.rb +38 -0
- data/test/http/options/form_test.rb +21 -0
- data/test/http/options/headers_test.rb +32 -0
- data/test/http/options/json_test.rb +21 -0
- data/test/http/options/merge_test.rb +78 -0
- data/test/http/options/new_test.rb +37 -0
- data/test/http/options/proxy_test.rb +32 -0
- data/test/http/options_test.rb +575 -0
- data/test/http/redirector_test.rb +639 -0
- data/test/http/request/body_test.rb +318 -0
- data/test/http/request/builder_test.rb +623 -0
- data/test/http/request/writer_test.rb +391 -0
- data/test/http/request_test.rb +1733 -0
- data/test/http/response/body_test.rb +292 -0
- data/test/http/response/parser_test.rb +105 -0
- data/test/http/response/status_test.rb +322 -0
- data/test/http/response_test.rb +502 -0
- data/test/http/retriable/delay_calculator_test.rb +194 -0
- data/test/http/retriable/errors_test.rb +71 -0
- data/test/http/retriable/performer_test.rb +551 -0
- data/test/http/session_test.rb +424 -0
- data/test/http/timeout/global_test.rb +239 -0
- data/test/http/timeout/null_test.rb +218 -0
- data/test/http/timeout/per_operation_test.rb +220 -0
- data/test/http/uri/normalizer_test.rb +89 -0
- data/test/http/uri_test.rb +1140 -0
- data/test/http/version_test.rb +15 -0
- data/test/http_test.rb +818 -0
- data/test/regression_tests.rb +27 -0
- data/test/support/dummy_server/encoding_routes.rb +47 -0
- data/test/support/dummy_server/routes.rb +201 -0
- data/test/support/dummy_server/servlet.rb +81 -0
- data/test/support/dummy_server.rb +200 -0
- data/{spec → test}/support/fakeio.rb +2 -2
- data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
- data/test/support/http_handling_shared/timeout_tests.rb +134 -0
- data/test/support/http_handling_shared.rb +11 -0
- data/test/support/proxy_server.rb +207 -0
- data/test/support/servers/runner.rb +67 -0
- data/{spec → test}/support/simplecov.rb +11 -2
- data/test/support/ssl_helper.rb +108 -0
- data/test/test_helper.rb +38 -0
- metadata +108 -168
- data/.github/workflows/ci.yml +0 -67
- data/.gitignore +0 -15
- data/.rspec +0 -1
- data/.rubocop/layout.yml +0 -8
- data/.rubocop/metrics.yml +0 -4
- data/.rubocop/rspec.yml +0 -9
- data/.rubocop/style.yml +0 -32
- data/.rubocop.yml +0 -11
- data/.rubocop_todo.yml +0 -219
- data/.yardopts +0 -2
- data/CHANGES_OLD.md +0 -1002
- data/Gemfile +0 -51
- data/Guardfile +0 -18
- data/Rakefile +0 -64
- data/lib/http/headers/mixin.rb +0 -34
- data/lib/http/retriable/client.rb +0 -37
- data/logo.png +0 -0
- data/spec/lib/http/client_spec.rb +0 -556
- data/spec/lib/http/connection_spec.rb +0 -88
- data/spec/lib/http/content_type_spec.rb +0 -47
- data/spec/lib/http/features/auto_deflate_spec.rb +0 -77
- data/spec/lib/http/features/auto_inflate_spec.rb +0 -86
- data/spec/lib/http/features/instrumentation_spec.rb +0 -81
- data/spec/lib/http/features/logging_spec.rb +0 -65
- data/spec/lib/http/features/raise_error_spec.rb +0 -62
- data/spec/lib/http/headers/mixin_spec.rb +0 -36
- data/spec/lib/http/headers/normalizer_spec.rb +0 -52
- data/spec/lib/http/headers_spec.rb +0 -527
- data/spec/lib/http/options/body_spec.rb +0 -15
- data/spec/lib/http/options/features_spec.rb +0 -33
- data/spec/lib/http/options/form_spec.rb +0 -15
- data/spec/lib/http/options/headers_spec.rb +0 -24
- data/spec/lib/http/options/json_spec.rb +0 -15
- data/spec/lib/http/options/merge_spec.rb +0 -68
- data/spec/lib/http/options/new_spec.rb +0 -30
- data/spec/lib/http/options/proxy_spec.rb +0 -20
- data/spec/lib/http/options_spec.rb +0 -13
- data/spec/lib/http/redirector_spec.rb +0 -530
- data/spec/lib/http/request/body_spec.rb +0 -211
- data/spec/lib/http/request/writer_spec.rb +0 -121
- data/spec/lib/http/request_spec.rb +0 -234
- data/spec/lib/http/response/body_spec.rb +0 -85
- data/spec/lib/http/response/parser_spec.rb +0 -74
- data/spec/lib/http/response/status_spec.rb +0 -253
- data/spec/lib/http/response_spec.rb +0 -262
- data/spec/lib/http/retriable/delay_calculator_spec.rb +0 -69
- data/spec/lib/http/retriable/performer_spec.rb +0 -302
- data/spec/lib/http/uri/normalizer_spec.rb +0 -95
- data/spec/lib/http/uri_spec.rb +0 -71
- data/spec/lib/http_spec.rb +0 -535
- data/spec/regression_specs.rb +0 -24
- data/spec/spec_helper.rb +0 -89
- data/spec/support/black_hole.rb +0 -13
- data/spec/support/dummy_server/servlet.rb +0 -203
- data/spec/support/dummy_server.rb +0 -44
- data/spec/support/fuubar.rb +0 -21
- data/spec/support/http_handling_shared.rb +0 -190
- data/spec/support/proxy_server.rb +0 -39
- data/spec/support/servers/config.rb +0 -11
- data/spec/support/servers/runner.rb +0 -19
- data/spec/support/ssl_helper.rb +0 -104
- /data/{spec → test}/support/capture_warning.rb +0 -0
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
require "uri"
|
|
6
|
+
require "logger"
|
|
7
|
+
|
|
8
|
+
require "support/http_handling_shared"
|
|
9
|
+
require "support/dummy_server"
|
|
10
|
+
require "support/ssl_helper"
|
|
11
|
+
|
|
12
|
+
StubbedClient = Class.new(HTTP::Client) do
|
|
13
|
+
def perform(request, options)
|
|
14
|
+
stubbed = stubs[HTTP::URI::NORMALIZER.call(request.uri).to_s]
|
|
15
|
+
stubbed ? stubbed.call(request) : super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def stubs
|
|
19
|
+
@stubs ||= {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def stub(stubs)
|
|
23
|
+
@stubs = stubs.transform_keys do |k|
|
|
24
|
+
HTTP::URI::NORMALIZER.call(k).to_s
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class HTTPClientTest < Minitest::Test
|
|
32
|
+
cover "HTTP::Client*"
|
|
33
|
+
run_server(:dummy) { DummyServer.new }
|
|
34
|
+
|
|
35
|
+
def capture_request(client, &)
|
|
36
|
+
captured_req = nil
|
|
37
|
+
client.stub(:perform, lambda { |req, _opts|
|
|
38
|
+
captured_req = req
|
|
39
|
+
nil
|
|
40
|
+
}, &)
|
|
41
|
+
captured_req
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def redirect_response(location, status = 302)
|
|
45
|
+
lambda do |request|
|
|
46
|
+
HTTP::Response.new(
|
|
47
|
+
status: status,
|
|
48
|
+
version: "1.1",
|
|
49
|
+
headers: { "Location" => location },
|
|
50
|
+
body: "",
|
|
51
|
+
request: request
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def simple_response(body, status = 200)
|
|
57
|
+
lambda do |request|
|
|
58
|
+
HTTP::Response.new(
|
|
59
|
+
status: status,
|
|
60
|
+
version: "1.1",
|
|
61
|
+
body: body,
|
|
62
|
+
request: request
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def client
|
|
68
|
+
@client ||= HTTP::Client.new
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def parse_query(str)
|
|
72
|
+
URI.decode_www_form(str).group_by(&:first).transform_values { |v| v.map(&:last) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# following redirects
|
|
76
|
+
|
|
77
|
+
def test_following_redirects_returns_response_of_new_location
|
|
78
|
+
client = StubbedClient.new(follow: true).stub(
|
|
79
|
+
"http://example.com/" => redirect_response("http://example.com/blog"),
|
|
80
|
+
"http://example.com/blog" => simple_response("OK")
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
assert_equal "OK", client.get("http://example.com/").to_s
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_following_redirects_prepends_previous_request_uri_scheme_and_host_if_needed
|
|
87
|
+
client = StubbedClient.new(follow: true).stub(
|
|
88
|
+
"http://example.com/" => redirect_response("/index"),
|
|
89
|
+
"http://example.com/index" => redirect_response("/index.html"),
|
|
90
|
+
"http://example.com/index.html" => simple_response("OK")
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
assert_equal "OK", client.get("http://example.com/").to_s
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_following_redirects_fails_upon_endless_redirects
|
|
97
|
+
client = StubbedClient.new(follow: true).stub(
|
|
98
|
+
"http://example.com/" => redirect_response("/")
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
assert_raises(HTTP::Redirector::EndlessRedirectError) { client.get("http://example.com/") }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def test_following_redirects_fails_if_max_amount_of_hops_reached
|
|
105
|
+
client = StubbedClient.new(follow: { max_hops: 5 }).stub(
|
|
106
|
+
"http://example.com/" => redirect_response("/1"),
|
|
107
|
+
"http://example.com/1" => redirect_response("/2"),
|
|
108
|
+
"http://example.com/2" => redirect_response("/3"),
|
|
109
|
+
"http://example.com/3" => redirect_response("/4"),
|
|
110
|
+
"http://example.com/4" => redirect_response("/5"),
|
|
111
|
+
"http://example.com/5" => redirect_response("/6"),
|
|
112
|
+
"http://example.com/6" => simple_response("OK")
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
assert_raises(HTTP::Redirector::TooManyRedirectsError) { client.get("http://example.com/") }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def test_following_redirects_with_non_ascii_urls_theoretically_works_like_a_charm
|
|
119
|
+
client = StubbedClient.new(follow: true).stub(
|
|
120
|
+
"http://example.com/" => redirect_response("/könig"),
|
|
121
|
+
"http://example.com/könig" => simple_response("OK")
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
client.get "http://example.com/könig"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def test_following_redirects_with_non_ascii_urls_follows_redirects
|
|
128
|
+
client = StubbedClient.new(follow: true).stub(
|
|
129
|
+
"http://example.com/" => redirect_response("/könig"),
|
|
130
|
+
"http://example.com/könig" => simple_response("OK")
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
assert_equal "OK", client.get("http://example.com/").to_s
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# following redirects with logging
|
|
137
|
+
|
|
138
|
+
def test_following_redirects_with_logging_logs_all_requests
|
|
139
|
+
logdev = StringIO.new
|
|
140
|
+
logger = Logger.new(logdev)
|
|
141
|
+
logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
|
|
142
|
+
logger.level = Logger::INFO
|
|
143
|
+
|
|
144
|
+
client = StubbedClient.new(follow: true, features: { logging: { logger: logger } }).stub(
|
|
145
|
+
"http://example.com/" => redirect_response("/1"),
|
|
146
|
+
"http://example.com/1" => redirect_response("/2"),
|
|
147
|
+
"http://example.com/2" => redirect_response("/3"),
|
|
148
|
+
"http://example.com/3" => simple_response("OK")
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
client.get("http://example.com/")
|
|
152
|
+
|
|
153
|
+
assert_equal <<~OUTPUT, logdev.string
|
|
154
|
+
** INFO **
|
|
155
|
+
> GET http://example.com/
|
|
156
|
+
** INFO **
|
|
157
|
+
> GET http://example.com/1
|
|
158
|
+
** INFO **
|
|
159
|
+
> GET http://example.com/2
|
|
160
|
+
** INFO **
|
|
161
|
+
> GET http://example.com/3
|
|
162
|
+
OUTPUT
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# base_uri
|
|
166
|
+
|
|
167
|
+
def test_base_uri_resolves_relative_paths_against_base_uri
|
|
168
|
+
client = StubbedClient.new(base_uri: "https://example.com/api").stub(
|
|
169
|
+
"https://example.com/api/users" => simple_response("OK")
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
assert_equal "OK", client.get("users").to_s
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def test_base_uri_resolves_absolute_paths_from_host_root
|
|
176
|
+
client = StubbedClient.new(base_uri: "https://example.com/api").stub(
|
|
177
|
+
"https://example.com/users" => simple_response("OK")
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
assert_equal "OK", client.get("/users").to_s
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def test_base_uri_ignores_base_uri_for_absolute_urls
|
|
184
|
+
client = StubbedClient.new(base_uri: "https://example.com/api").stub(
|
|
185
|
+
"https://other.com/path" => simple_response("OK")
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
assert_equal "OK", client.get("https://other.com/path").to_s
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def test_base_uri_handles_parent_path_traversal
|
|
192
|
+
client = StubbedClient.new(base_uri: "https://example.com/api/v1").stub(
|
|
193
|
+
"https://example.com/api/v2" => simple_response("OK")
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
assert_equal "OK", client.get("../v2").to_s
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def test_base_uri_handles_base_uri_without_trailing_slash
|
|
200
|
+
client = StubbedClient.new(base_uri: "https://example.com/api").stub(
|
|
201
|
+
"https://example.com/api/users" => simple_response("OK")
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
assert_equal "OK", client.get("users").to_s
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def test_base_uri_handles_base_uri_with_trailing_slash
|
|
208
|
+
client = StubbedClient.new(base_uri: "https://example.com/api/").stub(
|
|
209
|
+
"https://example.com/api/users" => simple_response("OK")
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
assert_equal "OK", client.get("users").to_s
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# parsing params
|
|
216
|
+
|
|
217
|
+
def test_parsing_params_accepts_params_within_the_provided_url
|
|
218
|
+
req = capture_request(client) { client.get("http://example.com/?foo=bar") }
|
|
219
|
+
|
|
220
|
+
assert_equal({ "foo" => %w[bar] }, parse_query(req.uri.query))
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def test_parsing_params_combines_get_params_from_the_uri_with_the_passed_in_params
|
|
224
|
+
req = capture_request(client) { client.get("http://example.com/?foo=bar", params: { baz: "quux" }) }
|
|
225
|
+
|
|
226
|
+
assert_equal({ "foo" => %w[bar], "baz" => %w[quux] }, parse_query(req.uri.query))
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def test_parsing_params_merges_duplicate_values
|
|
230
|
+
req = capture_request(client) { client.get("http://example.com/?a=1", params: { a: 2 }) }
|
|
231
|
+
|
|
232
|
+
assert_match(/^(a=1&a=2|a=2&a=1)$/, req.uri.query)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def test_parsing_params_does_not_modify_query_part_if_no_params_were_given
|
|
236
|
+
req = capture_request(client) { client.get("http://example.com/?deadbeef") }
|
|
237
|
+
|
|
238
|
+
assert_equal "deadbeef", req.uri.query
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def test_parsing_params_does_not_corrupt_index_less_arrays
|
|
242
|
+
req = capture_request(client) { client.get("http://example.com/?a[]=b&a[]=c", params: { d: "e" }) }
|
|
243
|
+
|
|
244
|
+
assert_equal({ "a[]" => %w[b c], "d" => %w[e] }, parse_query(req.uri.query))
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def test_parsing_params_properly_encodes_colons
|
|
248
|
+
req = capture_request(client) { client.get("http://example.com/", params: { t: "1970-01-01T00:00:00Z" }) }
|
|
249
|
+
|
|
250
|
+
assert_equal "t=1970-01-01T00%3A00%3A00Z", req.uri.query
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def test_parsing_params_does_not_convert_newlines_into_crlf_before_encoding_string_values
|
|
254
|
+
req = capture_request(client) { client.get("http://example.com/", params: { foo: "bar\nbaz" }) }
|
|
255
|
+
|
|
256
|
+
assert_equal "foo=bar%0Abaz", req.uri.query
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# passing multipart form data
|
|
260
|
+
|
|
261
|
+
def test_passing_multipart_form_data_creates_url_encoded_form_data_object
|
|
262
|
+
req = capture_request(client) { client.get("http://example.com/", form: { foo: "bar" }) }
|
|
263
|
+
|
|
264
|
+
assert_kind_of HTTP::FormData::Urlencoded, req.body.source
|
|
265
|
+
assert_equal "foo=bar", req.body.source.to_s
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def test_passing_multipart_form_data_creates_multipart_form_data_object
|
|
269
|
+
req = capture_request(client) { client.get("http://example.com/", form: { foo: HTTP::FormData::Part.new("content") }) }
|
|
270
|
+
|
|
271
|
+
assert_kind_of HTTP::FormData::Multipart, req.body.source
|
|
272
|
+
assert_includes req.body.source.to_s, "content"
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def test_passing_multipart_form_data_with_multipart_object_passes_it_through_unchanged
|
|
276
|
+
form_data = HTTP::FormData::Multipart.new({ foo: "bar" })
|
|
277
|
+
req = capture_request(client) { client.get("http://example.com/", form: form_data) }
|
|
278
|
+
|
|
279
|
+
assert_same form_data, req.body.source
|
|
280
|
+
assert_match(/^Content-Disposition: form-data; name="foo"\r\n\r\nbar\r\n/m, req.body.source.to_s)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def test_passing_multipart_form_data_with_urlencoded_object_passes_it_through_unchanged
|
|
284
|
+
form_data = HTTP::FormData::Urlencoded.new({ foo: "bar" })
|
|
285
|
+
req = capture_request(client) { client.get("http://example.com/", form: form_data) }
|
|
286
|
+
|
|
287
|
+
assert_same form_data, req.body.source
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# passing json
|
|
291
|
+
|
|
292
|
+
def test_passing_json_encodes_given_object
|
|
293
|
+
req = capture_request(client) { client.get("http://example.com/", json: { foo: :bar }) }
|
|
294
|
+
|
|
295
|
+
assert_equal '{"foo":"bar"}', req.body.source
|
|
296
|
+
assert_equal "application/json; charset=utf-8", req.headers["Content-Type"]
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# #request with non-ASCII URLs
|
|
300
|
+
|
|
301
|
+
def test_request_with_non_ascii_urls_theoretically_works_like_a_charm
|
|
302
|
+
client.get "#{dummy.endpoint}/könig"
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def test_request_with_non_ascii_urls_handles_multi_byte_characters
|
|
306
|
+
client.get "#{dummy.endpoint}/héllö-wörld"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# #request with explicitly given Host header
|
|
310
|
+
|
|
311
|
+
def test_request_with_explicitly_given_host_header_keeps_host_header_as_is
|
|
312
|
+
headers = { "Host" => "another.example.com" }
|
|
313
|
+
host_client = HTTP::Client.new(headers: headers)
|
|
314
|
+
req = capture_request(host_client) { host_client.request(:get, "http://example.com/") }
|
|
315
|
+
|
|
316
|
+
assert_equal "another.example.com", req.headers["Host"]
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# #request when :auto_deflate was specified
|
|
320
|
+
|
|
321
|
+
def test_request_when_auto_deflate_deletes_content_length_header
|
|
322
|
+
headers = { "Content-Length" => "12" }
|
|
323
|
+
deflate_client = HTTP::Client.new(headers: headers, features: { auto_deflate: {} }, body: "foo")
|
|
324
|
+
req = capture_request(deflate_client) { deflate_client.request(:get, "http://example.com/") }
|
|
325
|
+
|
|
326
|
+
assert_nil req.headers["Content-Length"]
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def test_request_when_auto_deflate_sets_content_encoding_header
|
|
330
|
+
headers = { "Content-Length" => "12" }
|
|
331
|
+
deflate_client = HTTP::Client.new(headers: headers, features: { auto_deflate: {} }, body: "foo")
|
|
332
|
+
req = capture_request(deflate_client) { deflate_client.request(:get, "http://example.com/") }
|
|
333
|
+
|
|
334
|
+
assert_equal "gzip", req.headers["Content-Encoding"]
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def test_request_when_auto_deflate_and_no_body_does_not_set_content_encoding_header
|
|
338
|
+
headers = { "Content-Length" => "12" }
|
|
339
|
+
deflate_client = HTTP::Client.new(headers: headers, features: { auto_deflate: {} })
|
|
340
|
+
req = capture_request(deflate_client) { deflate_client.request(:get, "http://example.com/") }
|
|
341
|
+
|
|
342
|
+
refute_includes req.headers, "Content-Encoding"
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# #request Feature
|
|
346
|
+
|
|
347
|
+
def feature_class
|
|
348
|
+
@feature_class ||= Class.new(HTTP::Feature) do
|
|
349
|
+
attr_reader :captured_request, :captured_response, :captured_error
|
|
350
|
+
|
|
351
|
+
def wrap_request(request)
|
|
352
|
+
@captured_request = request
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def wrap_response(response)
|
|
356
|
+
@captured_response = response
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def on_error(request, error)
|
|
360
|
+
@captured_request = request
|
|
361
|
+
@captured_error = error
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def test_feature_is_given_a_chance_to_wrap_the_request
|
|
367
|
+
feature_instance = feature_class.new
|
|
368
|
+
|
|
369
|
+
response = client.use(test_feature: feature_instance)
|
|
370
|
+
.request(:get, dummy.endpoint)
|
|
371
|
+
|
|
372
|
+
assert_equal 200, response.code
|
|
373
|
+
assert_equal :get, feature_instance.captured_request.verb
|
|
374
|
+
assert_equal "#{dummy.endpoint}/", feature_instance.captured_request.uri.to_s
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def test_feature_is_given_a_chance_to_wrap_the_response
|
|
378
|
+
feature_instance = feature_class.new
|
|
379
|
+
|
|
380
|
+
response = client.use(test_feature: feature_instance)
|
|
381
|
+
.request(:get, dummy.endpoint)
|
|
382
|
+
|
|
383
|
+
assert_equal response, feature_instance.captured_response
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def test_feature_is_given_a_chance_to_handle_an_error
|
|
387
|
+
sleep_url = "#{dummy.endpoint}/sleep"
|
|
388
|
+
feature_instance = feature_class.new
|
|
389
|
+
|
|
390
|
+
assert_raises(HTTP::TimeoutError) do
|
|
391
|
+
client.use(test_feature: feature_instance)
|
|
392
|
+
.timeout(0.01)
|
|
393
|
+
.request(:post, sleep_url)
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
assert_kind_of HTTP::TimeoutError, feature_instance.captured_error
|
|
397
|
+
assert_equal :post, feature_instance.captured_request.verb
|
|
398
|
+
assert_equal sleep_url, feature_instance.captured_request.uri.to_s
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def test_feature_is_given_a_chance_to_handle_a_connection_timeout_error
|
|
402
|
+
sleep_url = "#{dummy.endpoint}/sleep"
|
|
403
|
+
feature_instance = feature_class.new
|
|
404
|
+
|
|
405
|
+
TCPSocket.stub(:open, ->(*) { sleep 0.1 }) do
|
|
406
|
+
assert_raises(HTTP::ConnectTimeoutError) do
|
|
407
|
+
client.use(test_feature: feature_instance)
|
|
408
|
+
.timeout(0.001)
|
|
409
|
+
.request(:post, sleep_url)
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
assert_kind_of HTTP::ConnectTimeoutError, feature_instance.captured_error
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def test_feature_handles_responses_in_the_reverse_order_from_the_requests
|
|
416
|
+
feature_class_order =
|
|
417
|
+
Class.new(HTTP::Feature) do
|
|
418
|
+
@order = []
|
|
419
|
+
|
|
420
|
+
class << self
|
|
421
|
+
attr_reader :order
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def initialize(id:)
|
|
425
|
+
super()
|
|
426
|
+
@id = id
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def wrap_request(req)
|
|
430
|
+
self.class.order << "request.#{@id}"
|
|
431
|
+
req
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def wrap_response(res)
|
|
435
|
+
self.class.order << "response.#{@id}"
|
|
436
|
+
res
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
feature_instance_a = feature_class_order.new(id: "a")
|
|
440
|
+
feature_instance_b = feature_class_order.new(id: "b")
|
|
441
|
+
feature_instance_c = feature_class_order.new(id: "c")
|
|
442
|
+
|
|
443
|
+
client.use(
|
|
444
|
+
test_feature_a: feature_instance_a,
|
|
445
|
+
test_feature_b: feature_instance_b,
|
|
446
|
+
test_feature_c: feature_instance_c
|
|
447
|
+
).request(:get, dummy.endpoint)
|
|
448
|
+
|
|
449
|
+
assert_equal(
|
|
450
|
+
["request.a", "request.b", "request.c", "response.c", "response.b", "response.a"],
|
|
451
|
+
feature_class_order.order
|
|
452
|
+
)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def test_feature_calls_on_request_once_per_attempt
|
|
456
|
+
feature_class_on_request =
|
|
457
|
+
Class.new(HTTP::Feature) do
|
|
458
|
+
attr_reader :call_count
|
|
459
|
+
|
|
460
|
+
def initialize
|
|
461
|
+
super
|
|
462
|
+
@call_count = 0
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def on_request(_request)
|
|
466
|
+
@call_count += 1
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
feature_instance = feature_class_on_request.new
|
|
470
|
+
|
|
471
|
+
client.use(test_feature: feature_instance)
|
|
472
|
+
.request(:get, dummy.endpoint)
|
|
473
|
+
|
|
474
|
+
assert_equal 1, feature_instance.call_count
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def test_feature_calls_on_request_once_per_retry_attempt
|
|
478
|
+
feature_class_on_request =
|
|
479
|
+
Class.new(HTTP::Feature) do
|
|
480
|
+
attr_reader :call_count
|
|
481
|
+
|
|
482
|
+
def initialize
|
|
483
|
+
super
|
|
484
|
+
@call_count = 0
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def on_request(_request)
|
|
488
|
+
@call_count += 1
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
feature_instance = feature_class_on_request.new
|
|
492
|
+
|
|
493
|
+
client.use(test_feature: feature_instance)
|
|
494
|
+
.retriable(delay: 0, retry_statuses: [500])
|
|
495
|
+
.request(:get, "#{dummy.endpoint}/retry-2")
|
|
496
|
+
|
|
497
|
+
assert_equal 2, feature_instance.call_count
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def test_feature_wraps_each_retry_attempt_with_around_request
|
|
501
|
+
feature_class_around =
|
|
502
|
+
Class.new(HTTP::Feature) do
|
|
503
|
+
attr_reader :events
|
|
504
|
+
|
|
505
|
+
def initialize
|
|
506
|
+
super
|
|
507
|
+
@events = []
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def around_request(request)
|
|
511
|
+
@events << :before
|
|
512
|
+
yield(request).tap do
|
|
513
|
+
@events << :after
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
feature_instance = feature_class_around.new
|
|
518
|
+
|
|
519
|
+
client.use(test_feature: feature_instance)
|
|
520
|
+
.retriable(delay: 0, retry_statuses: [500])
|
|
521
|
+
.request(:get, "#{dummy.endpoint}/retry-2")
|
|
522
|
+
|
|
523
|
+
assert_equal %i[before after before after], feature_instance.events
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def test_feature_wraps_the_exchange_with_around_request_in_feature_order
|
|
527
|
+
feature_class_around =
|
|
528
|
+
Class.new(HTTP::Feature) do
|
|
529
|
+
@order = []
|
|
530
|
+
|
|
531
|
+
class << self
|
|
532
|
+
attr_reader :order
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def initialize(id:)
|
|
536
|
+
super()
|
|
537
|
+
@id = id
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def around_request(request)
|
|
541
|
+
self.class.order << "before.#{@id}"
|
|
542
|
+
yield(request).tap do
|
|
543
|
+
self.class.order << "after.#{@id}"
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
feature_instance_a = feature_class_around.new(id: "a")
|
|
548
|
+
feature_instance_b = feature_class_around.new(id: "b")
|
|
549
|
+
feature_instance_c = feature_class_around.new(id: "c")
|
|
550
|
+
|
|
551
|
+
client.use(
|
|
552
|
+
test_feature_a: feature_instance_a,
|
|
553
|
+
test_feature_b: feature_instance_b,
|
|
554
|
+
test_feature_c: feature_instance_c
|
|
555
|
+
).request(:get, dummy.endpoint)
|
|
556
|
+
|
|
557
|
+
assert_equal(
|
|
558
|
+
["before.a", "before.b", "before.c", "after.c", "after.b", "after.a"],
|
|
559
|
+
feature_class_around.order
|
|
560
|
+
)
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# #perform
|
|
564
|
+
|
|
565
|
+
def test_perform_calls_finish_response_once_body_was_fully_flushed
|
|
566
|
+
body = client.get(dummy.endpoint).to_s
|
|
567
|
+
|
|
568
|
+
assert_equal "<!doctype html>", body
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def test_perform_provides_access_to_the_request_from_the_response
|
|
572
|
+
unique_value = "20190424"
|
|
573
|
+
response = client.headers("X-Value" => unique_value).get(dummy.endpoint)
|
|
574
|
+
|
|
575
|
+
assert_kind_of HTTP::Request, response.request
|
|
576
|
+
assert_equal unique_value, response.request.headers["X-Value"]
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def test_perform_with_head_request_does_not_iterate_through_body
|
|
580
|
+
response = client.head(dummy.endpoint)
|
|
581
|
+
|
|
582
|
+
assert_equal 200, response.status.to_i
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
def test_perform_with_head_request_finishes_response_after_headers_were_received
|
|
586
|
+
response = client.head(dummy.endpoint)
|
|
587
|
+
|
|
588
|
+
assert_equal 200, response.status.to_i
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def test_perform_when_server_fully_flushes_response_in_one_chunk_properly_reads_body
|
|
592
|
+
response_data = [
|
|
593
|
+
"HTTP/1.1 200 OK\r\n" \
|
|
594
|
+
"Content-Type: text/html\r\n" \
|
|
595
|
+
"Server: WEBrick/1.3.1 (Ruby/1.9.3/2013-11-22)\r\n" \
|
|
596
|
+
"Date: Mon, 24 Mar 2014 00:32:22 GMT\r\n" \
|
|
597
|
+
"Content-Length: 15\r\n" \
|
|
598
|
+
"Connection: Keep-Alive\r\n" \
|
|
599
|
+
"\r\n" \
|
|
600
|
+
"<!doctype html>"
|
|
601
|
+
]
|
|
602
|
+
|
|
603
|
+
socket_spy = fake(
|
|
604
|
+
close: nil,
|
|
605
|
+
closed?: true,
|
|
606
|
+
readpartial: proc { response_data.shift || :eof },
|
|
607
|
+
write: proc(&:bytesize)
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
TCPSocket.stub(:open, socket_spy) do
|
|
611
|
+
body = client.get(dummy.endpoint).to_s
|
|
612
|
+
|
|
613
|
+
assert_equal "<!doctype html>", body
|
|
614
|
+
end
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
def test_perform_when_uses_chunked_transfer_encoding_properly_reads_body
|
|
618
|
+
response_data = [
|
|
619
|
+
"HTTP/1.1 200 OK\r\n" \
|
|
620
|
+
"Content-Type: application/json\r\n" \
|
|
621
|
+
"Transfer-Encoding: chunked\r\n" \
|
|
622
|
+
"Connection: close\r\n" \
|
|
623
|
+
"\r\n" \
|
|
624
|
+
"9\r\n" \
|
|
625
|
+
"{\"state\":\r\n" \
|
|
626
|
+
"5\r\n" \
|
|
627
|
+
"\"ok\"}\r\n" \
|
|
628
|
+
"0\r\n" \
|
|
629
|
+
"\r\n"
|
|
630
|
+
]
|
|
631
|
+
|
|
632
|
+
socket_spy = fake(
|
|
633
|
+
close: nil,
|
|
634
|
+
closed?: true,
|
|
635
|
+
readpartial: proc { response_data.shift || :eof },
|
|
636
|
+
write: proc(&:bytesize)
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
TCPSocket.stub(:open, socket_spy) do
|
|
640
|
+
body = client.get(dummy.endpoint).to_s
|
|
641
|
+
|
|
642
|
+
assert_equal '{"state":"ok"}', body
|
|
643
|
+
end
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def test_perform_when_uses_chunked_transfer_encoding_with_broken_body_raises_connection_error
|
|
647
|
+
response_data = [
|
|
648
|
+
"HTTP/1.1 200 OK\r\n" \
|
|
649
|
+
"Content-Type: application/json\r\n" \
|
|
650
|
+
"Transfer-Encoding: chunked\r\n" \
|
|
651
|
+
"Connection: close\r\n" \
|
|
652
|
+
"\r\n" \
|
|
653
|
+
"9\r\n" \
|
|
654
|
+
"{\"state\":\r\n"
|
|
655
|
+
]
|
|
656
|
+
|
|
657
|
+
socket_spy = fake(
|
|
658
|
+
close: nil,
|
|
659
|
+
closed?: true,
|
|
660
|
+
readpartial: proc { response_data.shift || :eof },
|
|
661
|
+
write: proc(&:bytesize)
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
TCPSocket.stub(:open, socket_spy) do
|
|
665
|
+
assert_raises(HTTP::ConnectionError) { client.get(dummy.endpoint).to_s }
|
|
666
|
+
end
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
# #perform with failed proxy connect
|
|
670
|
+
|
|
671
|
+
def test_perform_with_failed_proxy_connect_skips_sending_request
|
|
672
|
+
proxy_client = HTTP::Client.new
|
|
673
|
+
conn = fake(
|
|
674
|
+
failed_proxy_connect?: true,
|
|
675
|
+
proxy_response_headers: {},
|
|
676
|
+
status_code: 407,
|
|
677
|
+
http_version: "1.1",
|
|
678
|
+
headers: HTTP::Headers.new,
|
|
679
|
+
finish_response: nil,
|
|
680
|
+
keep_alive?: true,
|
|
681
|
+
expired?: false,
|
|
682
|
+
close: nil,
|
|
683
|
+
"pending_response=": ->(*) {}
|
|
684
|
+
)
|
|
685
|
+
proxy_client.instance_variable_set(:@connection, conn)
|
|
686
|
+
proxy_client.instance_variable_set(:@state, :clean)
|
|
687
|
+
req = HTTP::Request.new(verb: :get, uri: "http://example.com/", headers: {})
|
|
688
|
+
response = proxy_client.perform(req, HTTP::Options.new)
|
|
689
|
+
|
|
690
|
+
assert_equal 407, response.status.to_i
|
|
691
|
+
end
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
class HTTPClientHTTPHandlingTest < Minitest::Test
|
|
695
|
+
run_server(:dummy) { DummyServer.new }
|
|
696
|
+
|
|
697
|
+
def server
|
|
698
|
+
dummy
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def build_client(**)
|
|
702
|
+
HTTP::Client.new(**)
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
include HTTPHandlingTests
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
class HTTPClientSSLTest < Minitest::Test
|
|
709
|
+
run_server(:dummy_ssl) { DummyServer.new(ssl: true) }
|
|
710
|
+
|
|
711
|
+
def server
|
|
712
|
+
dummy_ssl
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
def build_client(**)
|
|
716
|
+
HTTP::Client.new(**, ssl_context: SSLHelper.client_context)
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
include HTTPHandlingTests
|
|
720
|
+
|
|
721
|
+
def test_ssl_just_works
|
|
722
|
+
response = build_client.get(dummy_ssl.endpoint)
|
|
723
|
+
|
|
724
|
+
assert_equal "<!doctype html>", response.body.to_s
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
def test_ssl_fails_with_ssl_error_if_host_mismatch
|
|
728
|
+
assert_raises(OpenSSL::SSL::SSLError) do
|
|
729
|
+
build_client.get(dummy_ssl.endpoint.gsub("127.0.0.1", "localhost"))
|
|
730
|
+
end
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
def test_ssl_with_ssl_options_instead_of_a_context_just_works
|
|
734
|
+
ssl_client = HTTP::Client.new(ssl: SSLHelper.client_params)
|
|
735
|
+
response = ssl_client.get(dummy_ssl.endpoint)
|
|
736
|
+
|
|
737
|
+
assert_equal "<!doctype html>", response.body.to_s
|
|
738
|
+
end
|
|
739
|
+
end
|