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,424 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
require "support/dummy_server"
|
|
6
|
+
|
|
7
|
+
class HTTPSessionTest < Minitest::Test
|
|
8
|
+
cover "HTTP::Session*"
|
|
9
|
+
run_server(:dummy) { DummyServer.new }
|
|
10
|
+
run_server(:dummy2) { DummyServer.new }
|
|
11
|
+
|
|
12
|
+
def session
|
|
13
|
+
@session ||= HTTP::Session.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# #initialize
|
|
17
|
+
|
|
18
|
+
def test_initialize_creates_a_session_with_default_options
|
|
19
|
+
assert_kind_of HTTP::Options, session.default_options
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_initialize_creates_a_session_with_given_options
|
|
23
|
+
session = HTTP::Session.new(headers: { "Accept" => "text/html" })
|
|
24
|
+
|
|
25
|
+
assert_equal "text/html", session.default_options.headers[:accept]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# #request
|
|
29
|
+
|
|
30
|
+
def test_request_returns_an_http_response
|
|
31
|
+
response = session.request(:get, dummy.endpoint)
|
|
32
|
+
|
|
33
|
+
assert_kind_of HTTP::Response, response
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_request_creates_a_new_client_for_each_request
|
|
37
|
+
client_ids = []
|
|
38
|
+
original_new = HTTP::Client.method(:new)
|
|
39
|
+
|
|
40
|
+
HTTP::Client.stub(:new, lambda { |*args|
|
|
41
|
+
c = original_new.call(*args)
|
|
42
|
+
client_ids << c.object_id
|
|
43
|
+
c
|
|
44
|
+
}) do
|
|
45
|
+
session.get(dummy.endpoint)
|
|
46
|
+
session.get(dummy.endpoint)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
assert_equal 2, client_ids.uniq.size
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# #request with block
|
|
53
|
+
|
|
54
|
+
def test_request_with_block_yields_the_response_and_returns_block_value
|
|
55
|
+
result = session.get(dummy.endpoint) { |res| res.status.code }
|
|
56
|
+
|
|
57
|
+
assert_equal 200, result
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_request_with_block_closes_the_client_after_the_block
|
|
61
|
+
closed = false
|
|
62
|
+
original_make = session.method(:make_client) # steep:ignore
|
|
63
|
+
session.define_singleton_method(:make_client) do |opts|
|
|
64
|
+
client = original_make.call(opts)
|
|
65
|
+
original_close = client.method(:close)
|
|
66
|
+
client.define_singleton_method(:close) do
|
|
67
|
+
closed = true
|
|
68
|
+
original_close.call
|
|
69
|
+
end
|
|
70
|
+
client
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
session.get(dummy.endpoint, &:status)
|
|
74
|
+
|
|
75
|
+
assert closed, "expected close to have been called"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_request_with_block_closes_the_client_even_when_the_block_raises
|
|
79
|
+
closed = false
|
|
80
|
+
original_make = session.method(:make_client) # steep:ignore
|
|
81
|
+
session.define_singleton_method(:make_client) do |opts|
|
|
82
|
+
client = original_make.call(opts)
|
|
83
|
+
original_close = client.method(:close)
|
|
84
|
+
client.define_singleton_method(:close) do
|
|
85
|
+
closed = true
|
|
86
|
+
original_close.call
|
|
87
|
+
end
|
|
88
|
+
client
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
assert_raises(RuntimeError) { session.get(dummy.endpoint) { raise "boom" } }
|
|
92
|
+
|
|
93
|
+
assert closed, "expected close to have been called on error"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_request_with_block_handles_nil_client_when_make_client_raises
|
|
97
|
+
session.define_singleton_method(:make_client) { |*| raise "boom" }
|
|
98
|
+
|
|
99
|
+
assert_raises(RuntimeError) { session.get(dummy.endpoint) { nil } }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Request::Builder
|
|
103
|
+
|
|
104
|
+
def test_request_builder_builds_an_http_request_from_session_options
|
|
105
|
+
builder = HTTP::Request::Builder.new(session.default_options)
|
|
106
|
+
req = builder.build(:get, "http://example.com/")
|
|
107
|
+
|
|
108
|
+
assert_kind_of HTTP::Request, req
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# #persistent?
|
|
112
|
+
|
|
113
|
+
def test_persistent_returns_false_by_default
|
|
114
|
+
refute_predicate session, :persistent?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# chaining
|
|
118
|
+
|
|
119
|
+
def test_chaining_returns_a_session_from_headers
|
|
120
|
+
chained = session.headers("Accept" => "text/html")
|
|
121
|
+
|
|
122
|
+
assert_kind_of HTTP::Session, chained
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def test_chaining_returns_a_session_from_timeout
|
|
126
|
+
chained = session.timeout(10)
|
|
127
|
+
|
|
128
|
+
assert_kind_of HTTP::Session, chained
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def test_chaining_returns_a_session_from_cookies
|
|
132
|
+
chained = session.cookies(session_id: "abc")
|
|
133
|
+
|
|
134
|
+
assert_kind_of HTTP::Session, chained
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def test_chaining_returns_a_session_from_follow
|
|
138
|
+
chained = session.follow
|
|
139
|
+
|
|
140
|
+
assert_kind_of HTTP::Session, chained
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def test_chaining_returns_a_session_from_use
|
|
144
|
+
chained = session.use(:auto_deflate)
|
|
145
|
+
|
|
146
|
+
assert_kind_of HTTP::Session, chained
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def test_chaining_returns_a_session_from_nodelay
|
|
150
|
+
chained = session.nodelay
|
|
151
|
+
|
|
152
|
+
assert_kind_of HTTP::Session, chained
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def test_chaining_returns_a_session_from_encoding
|
|
156
|
+
chained = session.encoding("UTF-8")
|
|
157
|
+
|
|
158
|
+
assert_kind_of HTTP::Session, chained
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def test_chaining_returns_a_session_from_via
|
|
162
|
+
chained = session.via("proxy.example.com", 8080)
|
|
163
|
+
|
|
164
|
+
assert_kind_of HTTP::Session, chained
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def test_chaining_returns_a_session_from_retriable
|
|
168
|
+
chained = session.retriable
|
|
169
|
+
|
|
170
|
+
assert_kind_of HTTP::Session, chained
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def test_chaining_returns_a_session_from_digest_auth
|
|
174
|
+
chained = session.digest_auth(user: "admin", pass: "secret")
|
|
175
|
+
|
|
176
|
+
assert_kind_of HTTP::Session, chained
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def test_chaining_preserves_options_through_chaining
|
|
180
|
+
chained = session.headers("Accept" => "text/html")
|
|
181
|
+
.timeout(10)
|
|
182
|
+
.cookies(session_id: "abc")
|
|
183
|
+
|
|
184
|
+
assert_equal "text/html", chained.default_options.headers[:accept]
|
|
185
|
+
assert_equal HTTP::Timeout::Global, chained.default_options.timeout_class
|
|
186
|
+
assert_equal "session_id=abc", chained.default_options.headers["Cookie"]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# thread safety
|
|
190
|
+
|
|
191
|
+
def test_thread_safety_can_be_shared_across_threads_without_errors
|
|
192
|
+
shared_session = HTTP.headers("Accept" => "text/html").timeout(15)
|
|
193
|
+
errors = []
|
|
194
|
+
mutex = Mutex.new
|
|
195
|
+
|
|
196
|
+
threads = Array.new(5) do
|
|
197
|
+
Thread.new do
|
|
198
|
+
shared_session.get(dummy.endpoint)
|
|
199
|
+
rescue => e
|
|
200
|
+
mutex.synchronize { errors << e }
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
threads.each(&:join)
|
|
204
|
+
|
|
205
|
+
assert_empty errors, "Expected no errors but got: #{errors.map(&:message).join(', ')}"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# cookies during redirects
|
|
209
|
+
|
|
210
|
+
def test_cookies_during_redirects_forwards_response_cookies_through_redirect_chain
|
|
211
|
+
response = HTTP.follow.get("#{dummy.endpoint}/redirect-with-cookie")
|
|
212
|
+
|
|
213
|
+
assert_includes response.to_s, "from_redirect=yes"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def test_cookies_during_redirects_accumulates_cookies_across_redirect_hops
|
|
217
|
+
response = HTTP.follow.get("#{dummy.endpoint}/redirect-cookie-chain/1")
|
|
218
|
+
body = response.to_s
|
|
219
|
+
|
|
220
|
+
assert_includes body, "first=1"
|
|
221
|
+
assert_includes body, "second=2"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def test_cookies_during_redirects_forwards_initial_request_cookies_through_redirects
|
|
225
|
+
response = HTTP.cookies(original: "value").follow.get("#{dummy.endpoint}/redirect-no-cookies")
|
|
226
|
+
|
|
227
|
+
assert_includes response.to_s, "original=value"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def test_cookies_during_redirects_deletes_cookies_with_empty_value_during_redirect
|
|
231
|
+
response = HTTP.follow.get("#{dummy.endpoint}/redirect-set-then-delete/1")
|
|
232
|
+
|
|
233
|
+
refute_includes response.to_s, "temp="
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def test_cookies_during_redirects_breaks_redirect_loop_when_cookie_changes_the_server_response
|
|
237
|
+
response = HTTP.follow.get("#{dummy.endpoint}/cookie-loop")
|
|
238
|
+
|
|
239
|
+
assert_equal "authenticated", response.to_s
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def test_cookies_during_redirects_does_not_set_cookie_header_when_no_cookies_present
|
|
243
|
+
response = HTTP.follow.get("#{dummy.endpoint}/redirect-no-cookies")
|
|
244
|
+
|
|
245
|
+
assert_equal "", response.to_s
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def test_cookies_during_redirects_applies_features_to_redirect_requests
|
|
249
|
+
response = HTTP.use(:auto_deflate).follow.get("#{dummy.endpoint}/redirect-301")
|
|
250
|
+
|
|
251
|
+
assert_equal "<!doctype html>", response.to_s
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# persistent
|
|
255
|
+
|
|
256
|
+
def test_persistent_returns_an_http_session
|
|
257
|
+
session = HTTP::Session.new.persistent(dummy.endpoint)
|
|
258
|
+
|
|
259
|
+
assert_kind_of HTTP::Session, session
|
|
260
|
+
ensure
|
|
261
|
+
session&.close
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# #close
|
|
265
|
+
|
|
266
|
+
def test_close_closes_all_pooled_clients
|
|
267
|
+
session = HTTP.persistent(dummy.endpoint)
|
|
268
|
+
session.get("/")
|
|
269
|
+
|
|
270
|
+
clients = session.instance_variable_get(:@clients)
|
|
271
|
+
|
|
272
|
+
refute_empty clients
|
|
273
|
+
|
|
274
|
+
session.close
|
|
275
|
+
|
|
276
|
+
assert_empty clients
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def test_close_is_safe_to_call_on_non_persistent_sessions
|
|
280
|
+
session.close
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# persistent connection reuse with chaining
|
|
284
|
+
|
|
285
|
+
def test_persistent_chaining_reuses_connections_when_chaining_headers
|
|
286
|
+
session = HTTP.persistent(dummy.endpoint)
|
|
287
|
+
|
|
288
|
+
sock1 = session.headers("Accept" => "application/json").get("#{dummy.endpoint}/socket/1").to_s
|
|
289
|
+
sock2 = session.headers("Accept" => "text/html").get("#{dummy.endpoint}/socket/2").to_s
|
|
290
|
+
|
|
291
|
+
refute_equal "", sock1
|
|
292
|
+
assert_equal sock1, sock2
|
|
293
|
+
ensure
|
|
294
|
+
session&.close
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def test_persistent_chaining_reuses_connections_when_chaining_auth
|
|
298
|
+
session = HTTP.persistent(dummy.endpoint)
|
|
299
|
+
|
|
300
|
+
sock1 = session.auth("Bearer token").get("#{dummy.endpoint}/socket/1").to_s
|
|
301
|
+
sock2 = session.auth("Bearer token").get("#{dummy.endpoint}/socket/2").to_s
|
|
302
|
+
|
|
303
|
+
refute_equal "", sock1
|
|
304
|
+
assert_equal sock1, sock2
|
|
305
|
+
ensure
|
|
306
|
+
session&.close
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def test_persistent_chaining_shares_the_connection_pool_across_chained_sessions
|
|
310
|
+
session = HTTP.persistent(dummy.endpoint)
|
|
311
|
+
chained = session.headers("Accept" => "application/json")
|
|
312
|
+
|
|
313
|
+
assert_same session.instance_variable_get(:@clients),
|
|
314
|
+
chained.instance_variable_get(:@clients)
|
|
315
|
+
ensure
|
|
316
|
+
session&.close
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def test_persistent_chaining_does_not_share_pool_for_non_persistent_sessions
|
|
320
|
+
chained = session.headers("Accept" => "application/json")
|
|
321
|
+
|
|
322
|
+
refute_same session.instance_variable_get(:@clients),
|
|
323
|
+
chained.instance_variable_get(:@clients)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# base_uri
|
|
327
|
+
|
|
328
|
+
def test_base_uri_returns_a_session_from_base_uri
|
|
329
|
+
chained = session.base_uri(dummy.endpoint)
|
|
330
|
+
|
|
331
|
+
assert_kind_of HTTP::Session, chained
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def test_base_uri_preserves_base_uri_through_chaining
|
|
335
|
+
chained = session.base_uri("https://example.com/api")
|
|
336
|
+
.headers("Accept" => "application/json")
|
|
337
|
+
|
|
338
|
+
assert_equal "https://example.com/api", chained.default_options.base_uri.to_s
|
|
339
|
+
assert_equal "application/json", chained.default_options.headers[:accept]
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def test_base_uri_resolves_relative_request_paths_against_base_uri
|
|
343
|
+
response = HTTP.base_uri(dummy.endpoint).get("/")
|
|
344
|
+
|
|
345
|
+
assert_kind_of HTTP::Response, response
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# persistent cross-origin redirects
|
|
349
|
+
|
|
350
|
+
def test_cross_origin_follows_redirects_to_a_different_origin
|
|
351
|
+
target = "#{dummy2.endpoint}/"
|
|
352
|
+
response = HTTP.persistent(dummy.endpoint).follow
|
|
353
|
+
.get("#{dummy.endpoint}/cross-origin-redirect?target=#{target}")
|
|
354
|
+
|
|
355
|
+
assert_equal 200, response.status.code
|
|
356
|
+
assert_equal "<!doctype html>", response.to_s
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def test_cross_origin_follows_redirects_back_to_the_original_origin
|
|
360
|
+
bounce_back = "#{dummy.endpoint}/"
|
|
361
|
+
target = "#{dummy2.endpoint}/cross-origin-redirect?target=#{bounce_back}"
|
|
362
|
+
response = HTTP.persistent(dummy.endpoint).follow
|
|
363
|
+
.get("#{dummy.endpoint}/cross-origin-redirect?target=#{target}")
|
|
364
|
+
|
|
365
|
+
assert_equal 200, response.status.code
|
|
366
|
+
assert_equal "<!doctype html>", response.to_s
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def test_cross_origin_pools_clients_per_origin
|
|
370
|
+
target = "#{dummy2.endpoint}/"
|
|
371
|
+
|
|
372
|
+
HTTP.persistent(dummy.endpoint) do |http|
|
|
373
|
+
session = http.follow
|
|
374
|
+
session.get("#{dummy.endpoint}/cross-origin-redirect?target=#{target}")
|
|
375
|
+
clients = session.instance_variable_get(:@clients)
|
|
376
|
+
|
|
377
|
+
assert_equal 2, clients.size
|
|
378
|
+
assert_includes clients.keys, URI.parse(dummy.endpoint).origin
|
|
379
|
+
assert_includes clients.keys, URI.parse(dummy2.endpoint).origin
|
|
380
|
+
|
|
381
|
+
session.close
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def test_cross_origin_manages_cookies_across_cross_origin_redirect_hops
|
|
386
|
+
target = "#{dummy2.endpoint}/echo-cookies"
|
|
387
|
+
session = HTTP.persistent(dummy.endpoint).follow
|
|
388
|
+
response = session.get("#{dummy.endpoint}/cross-origin-redirect-with-cookie?target=#{target}")
|
|
389
|
+
|
|
390
|
+
assert_equal 200, response.status.code
|
|
391
|
+
assert_equal "from_origin=yes", response.to_s
|
|
392
|
+
ensure
|
|
393
|
+
session&.close
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def test_cross_origin_reuses_pooled_connections_within_the_same_origin
|
|
397
|
+
HTTP.persistent(dummy.endpoint) do |http|
|
|
398
|
+
http.get(dummy.endpoint)
|
|
399
|
+
http.get(dummy.endpoint)
|
|
400
|
+
|
|
401
|
+
clients = http.instance_variable_get(:@clients)
|
|
402
|
+
|
|
403
|
+
assert_equal 1, clients.size
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def test_cross_origin_closes_all_pooled_connections_with_block_form_of_get
|
|
408
|
+
closed_origins = []
|
|
409
|
+
session = HTTP.persistent(dummy.endpoint).follow
|
|
410
|
+
|
|
411
|
+
target = "#{dummy2.endpoint}/"
|
|
412
|
+
session.get("#{dummy.endpoint}/cross-origin-redirect?target=#{target}") do |_res|
|
|
413
|
+
session.instance_variable_get(:@clients).each_value do |client|
|
|
414
|
+
original_close = client.method(:close)
|
|
415
|
+
client.define_singleton_method(:close) do
|
|
416
|
+
closed_origins << default_options.persistent
|
|
417
|
+
original_close.call
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
assert_equal 2, closed_origins.size
|
|
423
|
+
end
|
|
424
|
+
end
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class HTTPTimeoutGlobalTest < Minitest::Test
|
|
6
|
+
cover "HTTP::Timeout::Global*"
|
|
7
|
+
|
|
8
|
+
def setup
|
|
9
|
+
super
|
|
10
|
+
@io = fake(wait_readable: true, wait_writable: true)
|
|
11
|
+
@socket = fake(to_io: @io, closed?: false)
|
|
12
|
+
@timeout = HTTP::Timeout::Global.new(global_timeout: 5)
|
|
13
|
+
@timeout.instance_variable_set(:@socket, @socket)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# -- #connect --
|
|
17
|
+
|
|
18
|
+
def test_connect_sets_tcp_nodelay_when_nodelay_is_true
|
|
19
|
+
setsockopt_args = nil
|
|
20
|
+
tcp_socket = fake(
|
|
21
|
+
setsockopt: ->(*args) { setsockopt_args = args }
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
socket_class = fake(open: tcp_socket)
|
|
25
|
+
@timeout.connect(socket_class, "example.com", 80, nodelay: true)
|
|
26
|
+
|
|
27
|
+
assert_equal [Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1], setsockopt_args
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# -- #connect_ssl --
|
|
31
|
+
|
|
32
|
+
def test_connect_ssl_completes_without_error
|
|
33
|
+
connected = Object.new
|
|
34
|
+
socket = fake(
|
|
35
|
+
to_io: @io,
|
|
36
|
+
closed?: false,
|
|
37
|
+
connect_nonblock: ->(*) { connected }
|
|
38
|
+
)
|
|
39
|
+
@timeout.instance_variable_set(:@socket, socket)
|
|
40
|
+
@timeout.connect_ssl
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_connect_ssl_when_wait_readable_raised_waits_and_retries
|
|
44
|
+
call_count = 0
|
|
45
|
+
connected = Object.new
|
|
46
|
+
socket = fake(
|
|
47
|
+
to_io: @io,
|
|
48
|
+
closed?: false,
|
|
49
|
+
connect_nonblock: proc { |*|
|
|
50
|
+
call_count += 1
|
|
51
|
+
raise IO::EAGAINWaitReadable if call_count == 1
|
|
52
|
+
|
|
53
|
+
connected
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
@timeout.instance_variable_set(:@socket, socket)
|
|
57
|
+
@timeout.connect_ssl
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_connect_ssl_when_wait_writable_raised_waits_and_retries
|
|
61
|
+
call_count = 0
|
|
62
|
+
connected = Object.new
|
|
63
|
+
socket = fake(
|
|
64
|
+
to_io: @io,
|
|
65
|
+
closed?: false,
|
|
66
|
+
connect_nonblock: proc { |*|
|
|
67
|
+
call_count += 1
|
|
68
|
+
raise IO::EAGAINWaitWritable if call_count == 1
|
|
69
|
+
|
|
70
|
+
connected
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
@timeout.instance_variable_set(:@socket, socket)
|
|
74
|
+
@timeout.connect_ssl
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# -- #perform_io (via readpartial) --
|
|
78
|
+
|
|
79
|
+
def test_readpartial_when_wait_readable_waits_and_retries
|
|
80
|
+
call_count = 0
|
|
81
|
+
socket = fake(
|
|
82
|
+
to_io: @io,
|
|
83
|
+
closed?: false,
|
|
84
|
+
read_nonblock: proc { |*|
|
|
85
|
+
call_count += 1
|
|
86
|
+
call_count == 1 ? :wait_readable : "data"
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
@timeout.instance_variable_set(:@socket, socket)
|
|
90
|
+
|
|
91
|
+
assert_equal "data", @timeout.readpartial(10)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_write_when_wait_writable_waits_and_retries
|
|
95
|
+
call_count = 0
|
|
96
|
+
socket = fake(
|
|
97
|
+
to_io: @io,
|
|
98
|
+
closed?: false,
|
|
99
|
+
write_nonblock: proc { |*|
|
|
100
|
+
call_count += 1
|
|
101
|
+
call_count == 1 ? :wait_writable : 4
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
@timeout.instance_variable_set(:@socket, socket)
|
|
105
|
+
|
|
106
|
+
assert_equal 4, @timeout.write("data")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_readpartial_when_io_wait_readable_raised_waits_and_retries
|
|
110
|
+
call_count = 0
|
|
111
|
+
socket = fake(
|
|
112
|
+
to_io: @io,
|
|
113
|
+
closed?: false,
|
|
114
|
+
read_nonblock: proc { |*|
|
|
115
|
+
call_count += 1
|
|
116
|
+
raise IO::EAGAINWaitReadable if call_count == 1
|
|
117
|
+
|
|
118
|
+
"data"
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
@timeout.instance_variable_set(:@socket, socket)
|
|
122
|
+
|
|
123
|
+
assert_equal "data", @timeout.readpartial(10)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_write_when_io_wait_writable_raised_waits_and_retries
|
|
127
|
+
call_count = 0
|
|
128
|
+
socket = fake(
|
|
129
|
+
to_io: @io,
|
|
130
|
+
closed?: false,
|
|
131
|
+
write_nonblock: proc { |*|
|
|
132
|
+
call_count += 1
|
|
133
|
+
raise IO::EAGAINWaitWritable if call_count == 1
|
|
134
|
+
|
|
135
|
+
4
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
@timeout.instance_variable_set(:@socket, socket)
|
|
139
|
+
|
|
140
|
+
assert_equal 4, @timeout.write("data")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def test_readpartial_when_nil_eof_returns_eof
|
|
144
|
+
socket = fake(
|
|
145
|
+
to_io: @io,
|
|
146
|
+
closed?: false,
|
|
147
|
+
read_nonblock: nil
|
|
148
|
+
)
|
|
149
|
+
@timeout.instance_variable_set(:@socket, socket)
|
|
150
|
+
|
|
151
|
+
assert_equal :eof, @timeout.readpartial(10)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def test_readpartial_when_eof_error_raised_returns_eof
|
|
155
|
+
socket = fake(
|
|
156
|
+
to_io: @io,
|
|
157
|
+
closed?: false,
|
|
158
|
+
read_nonblock: ->(*) { raise EOFError }
|
|
159
|
+
)
|
|
160
|
+
@timeout.instance_variable_set(:@socket, socket)
|
|
161
|
+
|
|
162
|
+
assert_equal :eof, @timeout.readpartial(10)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# -- with per-operation timeouts --
|
|
166
|
+
|
|
167
|
+
def test_readpartial_with_per_op_timeouts_uses_global_time_left_as_effective_timeout
|
|
168
|
+
timeout = HTTP::Timeout::Global.new(global_timeout: 100, read_timeout: 100, write_timeout: 100,
|
|
169
|
+
connect_timeout: 100)
|
|
170
|
+
call_count = 0
|
|
171
|
+
socket = fake(
|
|
172
|
+
to_io: @io,
|
|
173
|
+
closed?: false,
|
|
174
|
+
read_nonblock: proc { |*|
|
|
175
|
+
call_count += 1
|
|
176
|
+
call_count == 1 ? :wait_readable : "data"
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
timeout.instance_variable_set(:@socket, socket)
|
|
180
|
+
|
|
181
|
+
assert_equal "data", timeout.readpartial(10)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def test_readpartial_with_tight_per_op_raises_when_read_timeout_fires
|
|
185
|
+
timeout = HTTP::Timeout::Global.new(global_timeout: 100, read_timeout: 0.01, write_timeout: 0.01,
|
|
186
|
+
connect_timeout: 0.01)
|
|
187
|
+
io_nil = fake(wait_readable: nil, wait_writable: true)
|
|
188
|
+
socket = fake(
|
|
189
|
+
to_io: io_nil,
|
|
190
|
+
closed?: false,
|
|
191
|
+
read_nonblock: :wait_readable
|
|
192
|
+
)
|
|
193
|
+
timeout.instance_variable_set(:@socket, socket)
|
|
194
|
+
|
|
195
|
+
err = assert_raises(HTTP::TimeoutError) { timeout.readpartial(10) }
|
|
196
|
+
assert_match(/Read timed out/, err.message)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def test_write_with_tight_per_op_raises_when_write_timeout_fires
|
|
200
|
+
timeout = HTTP::Timeout::Global.new(global_timeout: 100, read_timeout: 0.01, write_timeout: 0.01,
|
|
201
|
+
connect_timeout: 0.01)
|
|
202
|
+
io_nil = fake(wait_readable: true, wait_writable: nil)
|
|
203
|
+
socket = fake(
|
|
204
|
+
to_io: io_nil,
|
|
205
|
+
closed?: false,
|
|
206
|
+
write_nonblock: :wait_writable
|
|
207
|
+
)
|
|
208
|
+
timeout.instance_variable_set(:@socket, socket)
|
|
209
|
+
|
|
210
|
+
err = assert_raises(HTTP::TimeoutError) { timeout.write("data") }
|
|
211
|
+
assert_match(/Write timed out/, err.message)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def test_connect_ssl_with_tight_per_op_uses_connect_timeout_for_wait_readable
|
|
215
|
+
timeout = HTTP::Timeout::Global.new(global_timeout: 100, read_timeout: 0.01, write_timeout: 0.01,
|
|
216
|
+
connect_timeout: 0.01)
|
|
217
|
+
io_nil = fake(wait_readable: nil, wait_writable: true)
|
|
218
|
+
socket = fake(
|
|
219
|
+
to_io: io_nil,
|
|
220
|
+
closed?: false,
|
|
221
|
+
connect_nonblock: ->(*) { raise IO::EAGAINWaitReadable }
|
|
222
|
+
)
|
|
223
|
+
timeout.instance_variable_set(:@socket, socket)
|
|
224
|
+
assert_raises(HTTP::TimeoutError) { timeout.connect_ssl }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def test_connect_ssl_with_tight_per_op_uses_connect_timeout_for_wait_writable
|
|
228
|
+
timeout = HTTP::Timeout::Global.new(global_timeout: 100, read_timeout: 0.01, write_timeout: 0.01,
|
|
229
|
+
connect_timeout: 0.01)
|
|
230
|
+
io_nil = fake(wait_readable: true, wait_writable: nil)
|
|
231
|
+
socket = fake(
|
|
232
|
+
to_io: io_nil,
|
|
233
|
+
closed?: false,
|
|
234
|
+
connect_nonblock: ->(*) { raise IO::EAGAINWaitWritable }
|
|
235
|
+
)
|
|
236
|
+
timeout.instance_variable_set(:@socket, socket)
|
|
237
|
+
assert_raises(HTTP::TimeoutError) { timeout.connect_ssl }
|
|
238
|
+
end
|
|
239
|
+
end
|