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,1533 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class HTTPConnectionTest < Minitest::Test
|
|
6
|
+
cover "HTTP::Connection*"
|
|
7
|
+
|
|
8
|
+
def build_req(uri: "http://example.com/", verb: :get, headers: {}, **)
|
|
9
|
+
HTTP::Request.new(verb: verb, uri: uri, headers: headers, **)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def build_connection(socket: nil, **)
|
|
13
|
+
socket ||= fake(connect: nil, close: nil)
|
|
14
|
+
timeout_class = fake(new: socket)
|
|
15
|
+
req = build_req
|
|
16
|
+
opts_obj = HTTP::Options.new(timeout_class: timeout_class, **)
|
|
17
|
+
HTTP::Connection.new(req, opts_obj)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# #initialize
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
def test_initialize_initializes_state_from_options
|
|
24
|
+
connection = build_connection
|
|
25
|
+
|
|
26
|
+
refute_predicate connection, :failed_proxy_connect?
|
|
27
|
+
assert_predicate connection, :finished_request?
|
|
28
|
+
assert_predicate connection, :expired?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_initialize_raises_connection_error_on_io_error_during_connect
|
|
32
|
+
req = build_req
|
|
33
|
+
err_socket = fake(
|
|
34
|
+
connect: ->(*) { raise IOError, "connection refused" }
|
|
35
|
+
)
|
|
36
|
+
err_timeout_class = fake(new: err_socket)
|
|
37
|
+
err_opts = HTTP::Options.new(timeout_class: err_timeout_class)
|
|
38
|
+
|
|
39
|
+
err = assert_raises(HTTP::ConnectionError) do
|
|
40
|
+
HTTP::Connection.new(req, err_opts)
|
|
41
|
+
end
|
|
42
|
+
assert_includes err.message, "failed to connect"
|
|
43
|
+
assert_includes err.message, "connection refused"
|
|
44
|
+
refute_nil err.backtrace
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_initialize_raises_connection_error_on_socket_error_during_connect
|
|
48
|
+
req = build_req
|
|
49
|
+
err_socket = fake(
|
|
50
|
+
connect: ->(*) { raise SocketError, "dns failure" }
|
|
51
|
+
)
|
|
52
|
+
err_timeout_class = fake(new: err_socket)
|
|
53
|
+
err_opts = HTTP::Options.new(timeout_class: err_timeout_class)
|
|
54
|
+
|
|
55
|
+
err = assert_raises(HTTP::ConnectionError) do
|
|
56
|
+
HTTP::Connection.new(req, err_opts)
|
|
57
|
+
end
|
|
58
|
+
assert_includes err.message, "failed to connect"
|
|
59
|
+
assert_includes err.message, "dns failure"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_initialize_raises_connection_error_on_system_call_error_during_connect
|
|
63
|
+
req = build_req
|
|
64
|
+
err_socket = fake(
|
|
65
|
+
connect: ->(*) { raise Errno::ECONNREFUSED, "refused" }
|
|
66
|
+
)
|
|
67
|
+
err_timeout_class = fake(new: err_socket)
|
|
68
|
+
err_opts = HTTP::Options.new(timeout_class: err_timeout_class)
|
|
69
|
+
|
|
70
|
+
err = assert_raises(HTTP::ConnectionError) do
|
|
71
|
+
HTTP::Connection.new(req, err_opts)
|
|
72
|
+
end
|
|
73
|
+
assert_includes err.message, "failed to connect"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_initialize_timeout_error_closes_socket_and_re_raises
|
|
77
|
+
https_req = build_req(uri: "https://example.com/")
|
|
78
|
+
closed = false
|
|
79
|
+
tls_socket = fake(
|
|
80
|
+
connect: nil,
|
|
81
|
+
close: -> { closed = true },
|
|
82
|
+
start_tls: ->(*) { raise HTTP::TimeoutError },
|
|
83
|
+
closed?: false
|
|
84
|
+
)
|
|
85
|
+
tls_timeout_class = fake(new: tls_socket)
|
|
86
|
+
tls_opts = HTTP::Options.new(timeout_class: tls_timeout_class)
|
|
87
|
+
|
|
88
|
+
assert_raises(HTTP::TimeoutError) do
|
|
89
|
+
HTTP::Connection.new(https_req, tls_opts)
|
|
90
|
+
end
|
|
91
|
+
assert closed, "socket should have been closed"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_initialize_io_timeout_error_converts_to_connect_timeout_error
|
|
95
|
+
req = build_req
|
|
96
|
+
io_timeout_socket = fake(
|
|
97
|
+
connect: lambda { |*|
|
|
98
|
+
raise IO::TimeoutError, "Connect timed out!"
|
|
99
|
+
},
|
|
100
|
+
close: nil,
|
|
101
|
+
closed?: false
|
|
102
|
+
)
|
|
103
|
+
io_timeout_class = fake(new: io_timeout_socket)
|
|
104
|
+
io_opts = HTTP::Options.new(timeout_class: io_timeout_class)
|
|
105
|
+
|
|
106
|
+
err = assert_raises(HTTP::ConnectTimeoutError) do
|
|
107
|
+
HTTP::Connection.new(req, io_opts)
|
|
108
|
+
end
|
|
109
|
+
assert_equal "Connect timed out!", err.message
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
# #send_request
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
def test_send_request_streams_request_and_sets_pending_state
|
|
116
|
+
req = build_req
|
|
117
|
+
sr_req = build_req(uri: "http://example.com/path")
|
|
118
|
+
|
|
119
|
+
write_socket = fake(connect: nil, close: nil, write: proc(&:bytesize))
|
|
120
|
+
write_timeout_class = fake(new: write_socket)
|
|
121
|
+
write_opts = HTTP::Options.new(timeout_class: write_timeout_class)
|
|
122
|
+
conn = HTTP::Connection.new(req, write_opts)
|
|
123
|
+
|
|
124
|
+
assert_predicate conn, :finished_request?
|
|
125
|
+
|
|
126
|
+
conn.send_request(sr_req)
|
|
127
|
+
|
|
128
|
+
refute_predicate conn, :finished_request?
|
|
129
|
+
assert conn.instance_variable_get(:@pending_response)
|
|
130
|
+
refute conn.instance_variable_get(:@pending_request)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def test_send_request_raises_state_error_when_request_already_pending
|
|
134
|
+
connection = build_connection
|
|
135
|
+
connection.instance_variable_set(:@pending_request, true)
|
|
136
|
+
sr_req = build_req
|
|
137
|
+
err = assert_raises(HTTP::StateError) { connection.send_request(sr_req) }
|
|
138
|
+
assert_includes err.message, "response is pending"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def test_send_request_sets_pending_request_true_before_streaming_then_false_after
|
|
142
|
+
req = build_req
|
|
143
|
+
pending_during_stream = nil
|
|
144
|
+
conn = nil
|
|
145
|
+
stream_socket = fake(
|
|
146
|
+
connect: nil,
|
|
147
|
+
close: nil,
|
|
148
|
+
write: proc { |data|
|
|
149
|
+
pending_during_stream = conn.instance_variable_get(:@pending_request)
|
|
150
|
+
data.bytesize
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
stream_timeout_class = fake(new: stream_socket)
|
|
154
|
+
stream_opts = HTTP::Options.new(timeout_class: stream_timeout_class)
|
|
155
|
+
conn = HTTP::Connection.new(req, stream_opts)
|
|
156
|
+
|
|
157
|
+
sr_req = build_req
|
|
158
|
+
conn.send_request(sr_req)
|
|
159
|
+
|
|
160
|
+
assert pending_during_stream, "pending_request should be true during streaming"
|
|
161
|
+
assert conn.instance_variable_get(:@pending_response)
|
|
162
|
+
refute conn.instance_variable_get(:@pending_request)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def test_send_request_calls_req_stream_with_socket
|
|
166
|
+
req = build_req
|
|
167
|
+
stream_args = nil
|
|
168
|
+
sr_req = Minitest::Mock.new
|
|
169
|
+
sr_req.expect(:stream, nil) do |s|
|
|
170
|
+
stream_args = s
|
|
171
|
+
true
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
write_socket = fake(connect: nil, close: nil, write: lambda(&:bytesize))
|
|
175
|
+
write_timeout_class = fake(new: write_socket)
|
|
176
|
+
write_opts = HTTP::Options.new(timeout_class: write_timeout_class)
|
|
177
|
+
conn = HTTP::Connection.new(req, write_opts)
|
|
178
|
+
|
|
179
|
+
conn.send_request(sr_req)
|
|
180
|
+
|
|
181
|
+
assert_same conn.instance_variable_get(:@socket), stream_args
|
|
182
|
+
sr_req.verify
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
# #readpartial
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
def test_readpartial_raises_eof_error_when_no_response_pending
|
|
189
|
+
connection = build_connection
|
|
190
|
+
|
|
191
|
+
assert_raises(EOFError) { connection.readpartial }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def test_readpartial_reads_data_from_socket_and_returns_chunks
|
|
195
|
+
req = build_req
|
|
196
|
+
call_count = 0
|
|
197
|
+
responses = [
|
|
198
|
+
"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello",
|
|
199
|
+
:eof
|
|
200
|
+
]
|
|
201
|
+
rp_socket = fake(
|
|
202
|
+
connect: nil,
|
|
203
|
+
close: nil,
|
|
204
|
+
readpartial: proc {
|
|
205
|
+
idx = [call_count, responses.length - 1].min
|
|
206
|
+
responses[idx].tap { call_count += 1 }
|
|
207
|
+
},
|
|
208
|
+
closed?: proc { call_count >= responses.length }
|
|
209
|
+
)
|
|
210
|
+
rp_timeout_class = fake(new: rp_socket)
|
|
211
|
+
rp_opts = HTTP::Options.new(timeout_class: rp_timeout_class)
|
|
212
|
+
conn = HTTP::Connection.new(req, rp_opts)
|
|
213
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
214
|
+
|
|
215
|
+
conn.read_headers!
|
|
216
|
+
chunk = conn.readpartial
|
|
217
|
+
|
|
218
|
+
assert_equal "hello", chunk
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def test_readpartial_reads_data_in_parts_and_finishes
|
|
222
|
+
req = build_req
|
|
223
|
+
call_count = 0
|
|
224
|
+
responses = [
|
|
225
|
+
"HTTP/1.1 200 OK\r\nContent-Type: text\r\n\r\n",
|
|
226
|
+
"1", "23", "456", "78", "9", "0", :eof
|
|
227
|
+
]
|
|
228
|
+
rp_socket = fake(
|
|
229
|
+
connect: nil,
|
|
230
|
+
close: nil,
|
|
231
|
+
readpartial: proc {
|
|
232
|
+
idx = [call_count, responses.length - 1].min
|
|
233
|
+
responses[idx].tap { call_count += 1 }
|
|
234
|
+
},
|
|
235
|
+
closed?: proc { call_count >= responses.length }
|
|
236
|
+
)
|
|
237
|
+
rp_timeout_class = fake(new: rp_socket)
|
|
238
|
+
rp_opts = HTTP::Options.new(timeout_class: rp_timeout_class)
|
|
239
|
+
conn = HTTP::Connection.new(req, rp_opts)
|
|
240
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
241
|
+
|
|
242
|
+
conn.read_headers!
|
|
243
|
+
buffer = +""
|
|
244
|
+
begin
|
|
245
|
+
loop do
|
|
246
|
+
s = conn.readpartial(3)
|
|
247
|
+
refute_predicate conn, :finished_request? if s != ""
|
|
248
|
+
buffer << s
|
|
249
|
+
end
|
|
250
|
+
rescue EOFError
|
|
251
|
+
# Expected
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
assert_equal "1234567890", buffer
|
|
255
|
+
assert_predicate conn, :finished_request?
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def test_readpartial_fills_outbuf_when_provided
|
|
259
|
+
req = build_req
|
|
260
|
+
call_count = 0
|
|
261
|
+
responses = [
|
|
262
|
+
"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello",
|
|
263
|
+
:eof
|
|
264
|
+
]
|
|
265
|
+
ob_socket = fake(
|
|
266
|
+
connect: nil,
|
|
267
|
+
close: nil,
|
|
268
|
+
readpartial: proc {
|
|
269
|
+
idx = [call_count, responses.length - 1].min
|
|
270
|
+
responses[idx].tap { call_count += 1 }
|
|
271
|
+
},
|
|
272
|
+
closed?: proc { call_count >= responses.length }
|
|
273
|
+
)
|
|
274
|
+
ob_timeout_class = fake(new: ob_socket)
|
|
275
|
+
ob_opts = HTTP::Options.new(timeout_class: ob_timeout_class)
|
|
276
|
+
conn = HTTP::Connection.new(req, ob_opts)
|
|
277
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
278
|
+
|
|
279
|
+
conn.read_headers!
|
|
280
|
+
outbuf = +""
|
|
281
|
+
result = conn.readpartial(16_384, outbuf)
|
|
282
|
+
|
|
283
|
+
assert_equal "hello", outbuf
|
|
284
|
+
assert_same outbuf, result
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def test_readpartial_uses_size_parameter_when_reading_from_socket
|
|
288
|
+
req = build_req
|
|
289
|
+
call_count = 0
|
|
290
|
+
read_sizes = []
|
|
291
|
+
responses = [
|
|
292
|
+
"HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n",
|
|
293
|
+
"helloworld",
|
|
294
|
+
:eof
|
|
295
|
+
]
|
|
296
|
+
sz_socket = fake(
|
|
297
|
+
connect: nil,
|
|
298
|
+
close: nil,
|
|
299
|
+
readpartial: proc { |size, *|
|
|
300
|
+
read_sizes << size
|
|
301
|
+
idx = [call_count, responses.length - 1].min
|
|
302
|
+
responses[idx].tap { call_count += 1 }
|
|
303
|
+
},
|
|
304
|
+
closed?: proc { call_count >= responses.length }
|
|
305
|
+
)
|
|
306
|
+
sz_timeout_class = fake(new: sz_socket)
|
|
307
|
+
sz_opts = HTTP::Options.new(timeout_class: sz_timeout_class)
|
|
308
|
+
conn = HTTP::Connection.new(req, sz_opts)
|
|
309
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
310
|
+
|
|
311
|
+
conn.read_headers!
|
|
312
|
+
conn.readpartial(42)
|
|
313
|
+
|
|
314
|
+
assert_includes read_sizes, 42
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def test_readpartial_detects_premature_eof_on_framed_content_length_response
|
|
318
|
+
req = build_req
|
|
319
|
+
call_count = 0
|
|
320
|
+
responses = [
|
|
321
|
+
"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\nhello",
|
|
322
|
+
:eof
|
|
323
|
+
]
|
|
324
|
+
eof_socket = fake(
|
|
325
|
+
connect: nil,
|
|
326
|
+
close: nil,
|
|
327
|
+
readpartial: proc {
|
|
328
|
+
idx = [call_count, responses.length - 1].min
|
|
329
|
+
responses[idx].tap { call_count += 1 }
|
|
330
|
+
},
|
|
331
|
+
closed?: false
|
|
332
|
+
)
|
|
333
|
+
eof_timeout_class = fake(new: eof_socket)
|
|
334
|
+
eof_opts = HTTP::Options.new(timeout_class: eof_timeout_class)
|
|
335
|
+
conn = HTTP::Connection.new(req, eof_opts)
|
|
336
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
337
|
+
|
|
338
|
+
conn.read_headers!
|
|
339
|
+
chunk = conn.readpartial
|
|
340
|
+
|
|
341
|
+
assert_equal "hello", chunk
|
|
342
|
+
err = assert_raises(HTTP::ConnectionError) { conn.readpartial }
|
|
343
|
+
assert_includes err.message, "response body ended prematurely"
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def test_readpartial_detects_premature_eof_on_chunked_response
|
|
347
|
+
req = build_req
|
|
348
|
+
call_count = 0
|
|
349
|
+
responses = [
|
|
350
|
+
"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n",
|
|
351
|
+
:eof
|
|
352
|
+
]
|
|
353
|
+
eof_socket = fake(
|
|
354
|
+
connect: nil,
|
|
355
|
+
close: nil,
|
|
356
|
+
readpartial: proc {
|
|
357
|
+
idx = [call_count, responses.length - 1].min
|
|
358
|
+
responses[idx].tap { call_count += 1 }
|
|
359
|
+
},
|
|
360
|
+
closed?: false
|
|
361
|
+
)
|
|
362
|
+
eof_timeout_class = fake(new: eof_socket)
|
|
363
|
+
eof_opts = HTTP::Options.new(timeout_class: eof_timeout_class)
|
|
364
|
+
conn = HTTP::Connection.new(req, eof_opts)
|
|
365
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
366
|
+
|
|
367
|
+
conn.read_headers!
|
|
368
|
+
chunk = conn.readpartial
|
|
369
|
+
|
|
370
|
+
assert_equal "hello", chunk
|
|
371
|
+
err = assert_raises(HTTP::ConnectionError) { conn.readpartial }
|
|
372
|
+
assert_includes err.message, "response body ended prematurely"
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def test_readpartial_finishes_cleanly_when_not_framed
|
|
376
|
+
req = build_req
|
|
377
|
+
call_count = 0
|
|
378
|
+
responses = [
|
|
379
|
+
"HTTP/1.1 200 OK\r\n\r\nhello",
|
|
380
|
+
:eof
|
|
381
|
+
]
|
|
382
|
+
unframed_socket = fake(
|
|
383
|
+
connect: nil,
|
|
384
|
+
close: nil,
|
|
385
|
+
readpartial: proc {
|
|
386
|
+
idx = [call_count, responses.length - 1].min
|
|
387
|
+
responses[idx].tap { call_count += 1 }
|
|
388
|
+
},
|
|
389
|
+
closed?: false
|
|
390
|
+
)
|
|
391
|
+
unframed_timeout_class = fake(new: unframed_socket)
|
|
392
|
+
unframed_opts = HTTP::Options.new(timeout_class: unframed_timeout_class)
|
|
393
|
+
conn = HTTP::Connection.new(req, unframed_opts)
|
|
394
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
395
|
+
|
|
396
|
+
conn.read_headers!
|
|
397
|
+
chunk = conn.readpartial
|
|
398
|
+
|
|
399
|
+
assert_equal "hello", chunk
|
|
400
|
+
# Should not raise premature EOF because body is not framed
|
|
401
|
+
chunk2 = conn.readpartial
|
|
402
|
+
|
|
403
|
+
assert_equal "", chunk2
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def test_readpartial_finishes_response_when_parser_says_finished
|
|
407
|
+
req = build_req
|
|
408
|
+
call_count = 0
|
|
409
|
+
responses = [
|
|
410
|
+
"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n",
|
|
411
|
+
"hello",
|
|
412
|
+
# After reading "hello", parser is finished (Content-Length satisfied)
|
|
413
|
+
# even if we haven't seen :eof yet
|
|
414
|
+
"more data that shouldn't matter",
|
|
415
|
+
:eof
|
|
416
|
+
]
|
|
417
|
+
pf_socket = fake(
|
|
418
|
+
connect: nil,
|
|
419
|
+
close: nil,
|
|
420
|
+
readpartial: proc {
|
|
421
|
+
idx = [call_count, responses.length - 1].min
|
|
422
|
+
responses[idx].tap { call_count += 1 }
|
|
423
|
+
},
|
|
424
|
+
closed?: false
|
|
425
|
+
)
|
|
426
|
+
pf_timeout_class = fake(new: pf_socket)
|
|
427
|
+
pf_opts = HTTP::Options.new(timeout_class: pf_timeout_class)
|
|
428
|
+
conn = HTTP::Connection.new(req, pf_opts)
|
|
429
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
430
|
+
|
|
431
|
+
conn.read_headers!
|
|
432
|
+
chunk = conn.readpartial
|
|
433
|
+
|
|
434
|
+
assert_equal "hello", chunk
|
|
435
|
+
# After reading exactly Content-Length bytes, parser.finished? should be true
|
|
436
|
+
# and finish_response should be called
|
|
437
|
+
assert_predicate conn, :finished_request?
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def test_readpartial_returns_binary_empty_string_when_parser_has_no_data
|
|
441
|
+
req = build_req
|
|
442
|
+
call_count = 0
|
|
443
|
+
# Headers with no body data, then immediately EOF
|
|
444
|
+
responses = [
|
|
445
|
+
"HTTP/1.1 200 OK\r\n\r\n",
|
|
446
|
+
:eof
|
|
447
|
+
]
|
|
448
|
+
empty_socket = fake(
|
|
449
|
+
connect: nil,
|
|
450
|
+
close: nil,
|
|
451
|
+
readpartial: proc {
|
|
452
|
+
idx = [call_count, responses.length - 1].min
|
|
453
|
+
responses[idx].tap { call_count += 1 }
|
|
454
|
+
},
|
|
455
|
+
closed?: false
|
|
456
|
+
)
|
|
457
|
+
empty_timeout_class = fake(new: empty_socket)
|
|
458
|
+
empty_opts = HTTP::Options.new(timeout_class: empty_timeout_class)
|
|
459
|
+
conn = HTTP::Connection.new(req, empty_opts)
|
|
460
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
461
|
+
|
|
462
|
+
conn.read_headers!
|
|
463
|
+
chunk = conn.readpartial
|
|
464
|
+
# Should return binary empty string "".b
|
|
465
|
+
assert_equal Encoding::ASCII_8BIT, chunk.encoding
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# ---------------------------------------------------------------------------
|
|
469
|
+
# #read_headers!
|
|
470
|
+
# ---------------------------------------------------------------------------
|
|
471
|
+
def test_read_headers_populates_headers_preserving_casing
|
|
472
|
+
req = build_req
|
|
473
|
+
raw_response = "HTTP/1.1 200 OK\r\nContent-Type: text\r\nfoo_bar: 123\r\n\r\n"
|
|
474
|
+
read_socket = fake(connect: nil, close: nil, readpartial: raw_response)
|
|
475
|
+
read_timeout_class = fake(new: read_socket)
|
|
476
|
+
read_opts = HTTP::Options.new(timeout_class: read_timeout_class)
|
|
477
|
+
conn = HTTP::Connection.new(req, read_opts)
|
|
478
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
479
|
+
|
|
480
|
+
conn.read_headers!
|
|
481
|
+
|
|
482
|
+
assert_equal "text", conn.headers["Content-Type"]
|
|
483
|
+
assert_equal "123", conn.headers["Foo-Bar"]
|
|
484
|
+
assert_equal "123", conn.headers["foo_bar"]
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def test_read_headers_raises_response_header_error_on_eof_before_headers_complete
|
|
488
|
+
req = build_req
|
|
489
|
+
eof_socket = fake(connect: nil, close: nil, readpartial: :eof)
|
|
490
|
+
eof_timeout_class = fake(new: eof_socket)
|
|
491
|
+
eof_opts = HTTP::Options.new(timeout_class: eof_timeout_class)
|
|
492
|
+
conn = HTTP::Connection.new(req, eof_opts)
|
|
493
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
494
|
+
|
|
495
|
+
err = assert_raises(HTTP::ResponseHeaderError) { conn.read_headers! }
|
|
496
|
+
assert_includes err.message, "couldn't read response headers"
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def test_read_headers_calls_set_keep_alive_after_reading
|
|
500
|
+
req = build_req
|
|
501
|
+
raw_response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"
|
|
502
|
+
read_socket = fake(connect: nil, close: nil, readpartial: raw_response, closed?: false)
|
|
503
|
+
read_timeout_class = fake(new: read_socket)
|
|
504
|
+
read_opts = HTTP::Options.new(timeout_class: read_timeout_class)
|
|
505
|
+
conn = HTTP::Connection.new(req, read_opts)
|
|
506
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
507
|
+
conn.instance_variable_set(:@persistent, true)
|
|
508
|
+
|
|
509
|
+
conn.read_headers!
|
|
510
|
+
|
|
511
|
+
assert_predicate conn, :keep_alive?
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def test_read_headers_passes_buffer_size_to_read_more
|
|
515
|
+
req = build_req
|
|
516
|
+
read_sizes = []
|
|
517
|
+
call_count = 0
|
|
518
|
+
responses = [
|
|
519
|
+
"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
|
|
520
|
+
:eof
|
|
521
|
+
]
|
|
522
|
+
bs_socket = fake(
|
|
523
|
+
connect: nil,
|
|
524
|
+
close: nil,
|
|
525
|
+
readpartial: proc { |size, *|
|
|
526
|
+
read_sizes << size
|
|
527
|
+
idx = [call_count, responses.length - 1].min
|
|
528
|
+
responses[idx].tap { call_count += 1 }
|
|
529
|
+
}
|
|
530
|
+
)
|
|
531
|
+
bs_timeout_class = fake(new: bs_socket)
|
|
532
|
+
bs_opts = HTTP::Options.new(timeout_class: bs_timeout_class)
|
|
533
|
+
conn = HTTP::Connection.new(req, bs_opts)
|
|
534
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
535
|
+
|
|
536
|
+
conn.read_headers!
|
|
537
|
+
|
|
538
|
+
assert_includes read_sizes, HTTP::Connection::BUFFER_SIZE
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# ---------------------------------------------------------------------------
|
|
542
|
+
# #read_headers! with 1xx informational response
|
|
543
|
+
# ---------------------------------------------------------------------------
|
|
544
|
+
def test_read_headers_skips_100_continue_and_returns_final_response
|
|
545
|
+
req = build_req
|
|
546
|
+
call_count = 0
|
|
547
|
+
responses = [
|
|
548
|
+
"HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello",
|
|
549
|
+
:eof
|
|
550
|
+
]
|
|
551
|
+
info_socket = fake(
|
|
552
|
+
connect: nil,
|
|
553
|
+
close: nil,
|
|
554
|
+
readpartial: proc {
|
|
555
|
+
idx = [call_count, responses.length - 1].min
|
|
556
|
+
responses[idx].tap { call_count += 1 }
|
|
557
|
+
}
|
|
558
|
+
)
|
|
559
|
+
info_timeout_class = fake(new: info_socket)
|
|
560
|
+
info_opts = HTTP::Options.new(timeout_class: info_timeout_class)
|
|
561
|
+
conn = HTTP::Connection.new(req, info_opts)
|
|
562
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
563
|
+
|
|
564
|
+
conn.read_headers!
|
|
565
|
+
|
|
566
|
+
assert_equal 200, conn.status_code
|
|
567
|
+
assert_equal "5", conn.headers["Content-Length"]
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def test_read_headers_skips_100_continue_in_small_chunks
|
|
571
|
+
req = build_req
|
|
572
|
+
raw = "HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"
|
|
573
|
+
chunks = raw.chars + [:eof]
|
|
574
|
+
call_count = 0
|
|
575
|
+
chunked_socket = fake(
|
|
576
|
+
connect: nil,
|
|
577
|
+
close: nil,
|
|
578
|
+
readpartial: proc {
|
|
579
|
+
idx = [call_count, chunks.length - 1].min
|
|
580
|
+
chunks[idx].tap { call_count += 1 }
|
|
581
|
+
}
|
|
582
|
+
)
|
|
583
|
+
chunked_timeout_class = fake(new: chunked_socket)
|
|
584
|
+
chunked_opts = HTTP::Options.new(timeout_class: chunked_timeout_class)
|
|
585
|
+
conn = HTTP::Connection.new(req, chunked_opts)
|
|
586
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
587
|
+
|
|
588
|
+
conn.read_headers!
|
|
589
|
+
|
|
590
|
+
assert_equal 200, conn.status_code
|
|
591
|
+
assert_equal "5", conn.headers["Content-Length"]
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# ---------------------------------------------------------------------------
|
|
595
|
+
# #send_request when response already pending
|
|
596
|
+
# ---------------------------------------------------------------------------
|
|
597
|
+
def test_send_request_with_pending_response_boolean_closes_and_proceeds
|
|
598
|
+
req = build_req
|
|
599
|
+
socket = fake(connect: nil, close: nil, closed?: false, write: lambda(&:bytesize))
|
|
600
|
+
timeout_class = fake(new: socket)
|
|
601
|
+
opts = HTTP::Options.new(timeout_class: timeout_class)
|
|
602
|
+
connection = HTTP::Connection.new(req, opts)
|
|
603
|
+
connection.instance_variable_set(:@pending_response, true)
|
|
604
|
+
new_req = build_req
|
|
605
|
+
connection.send_request(new_req)
|
|
606
|
+
|
|
607
|
+
assert connection.instance_variable_get(:@pending_response)
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def test_send_request_with_large_content_length_pending_closes_instead_of_flushing
|
|
611
|
+
req = build_req
|
|
612
|
+
socket = fake(connect: nil, close: nil, closed?: false, write: lambda(&:bytesize))
|
|
613
|
+
timeout_class = fake(new: socket)
|
|
614
|
+
opts = HTTP::Options.new(timeout_class: timeout_class)
|
|
615
|
+
connection = HTTP::Connection.new(req, opts)
|
|
616
|
+
response = fake(content_length: HTTP::Connection::MAX_FLUSH_SIZE + 1, flush: nil)
|
|
617
|
+
connection.instance_variable_set(:@pending_response, response)
|
|
618
|
+
new_req = build_req
|
|
619
|
+
connection.send_request(new_req)
|
|
620
|
+
|
|
621
|
+
assert connection.instance_variable_get(:@pending_response)
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def test_send_request_when_flushing_raises_closes_and_proceeds
|
|
625
|
+
req = build_req
|
|
626
|
+
socket = fake(connect: nil, close: nil, closed?: false, write: lambda(&:bytesize))
|
|
627
|
+
timeout_class = fake(new: socket)
|
|
628
|
+
opts = HTTP::Options.new(timeout_class: timeout_class)
|
|
629
|
+
connection = HTTP::Connection.new(req, opts)
|
|
630
|
+
response = fake(content_length: nil, flush: -> { raise "boom" })
|
|
631
|
+
connection.instance_variable_set(:@pending_response, response)
|
|
632
|
+
new_req = build_req
|
|
633
|
+
connection.send_request(new_req)
|
|
634
|
+
|
|
635
|
+
assert connection.instance_variable_get(:@pending_response)
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def test_send_request_with_small_body_pending_flushes_response_body
|
|
639
|
+
req = build_req
|
|
640
|
+
socket = fake(connect: nil, close: nil, closed?: false, write: lambda(&:bytesize))
|
|
641
|
+
timeout_class = fake(new: socket)
|
|
642
|
+
opts = HTTP::Options.new(timeout_class: timeout_class)
|
|
643
|
+
connection = HTTP::Connection.new(req, opts)
|
|
644
|
+
flushed = false
|
|
645
|
+
response = fake(content_length: 100, flush: -> { flushed = true })
|
|
646
|
+
connection.instance_variable_set(:@pending_response, response)
|
|
647
|
+
new_req = build_req
|
|
648
|
+
connection.send_request(new_req)
|
|
649
|
+
|
|
650
|
+
assert flushed
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
# ---------------------------------------------------------------------------
|
|
654
|
+
# #finish_response
|
|
655
|
+
# ---------------------------------------------------------------------------
|
|
656
|
+
def test_finish_response_closes_socket_when_not_keeping_alive
|
|
657
|
+
req = build_req
|
|
658
|
+
call_count = 0
|
|
659
|
+
responses = [
|
|
660
|
+
"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello",
|
|
661
|
+
:eof
|
|
662
|
+
]
|
|
663
|
+
closed = false
|
|
664
|
+
fr_socket = fake(
|
|
665
|
+
connect: nil,
|
|
666
|
+
close: -> { closed = true },
|
|
667
|
+
readpartial: proc {
|
|
668
|
+
idx = [call_count, responses.length - 1].min
|
|
669
|
+
responses[idx].tap { call_count += 1 }
|
|
670
|
+
},
|
|
671
|
+
closed?: proc { closed }
|
|
672
|
+
)
|
|
673
|
+
fr_timeout_class = fake(new: fr_socket)
|
|
674
|
+
fr_opts = HTTP::Options.new(timeout_class: fr_timeout_class)
|
|
675
|
+
conn = HTTP::Connection.new(req, fr_opts)
|
|
676
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
677
|
+
|
|
678
|
+
conn.read_headers!
|
|
679
|
+
conn.finish_response
|
|
680
|
+
|
|
681
|
+
assert closed, "socket should be closed when not keep-alive"
|
|
682
|
+
assert_predicate conn, :finished_request?
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
def test_finish_response_does_not_close_socket_when_keeping_alive
|
|
686
|
+
req = build_req
|
|
687
|
+
call_count = 0
|
|
688
|
+
responses = [
|
|
689
|
+
"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello",
|
|
690
|
+
:eof
|
|
691
|
+
]
|
|
692
|
+
closed = false
|
|
693
|
+
ka_socket = fake(
|
|
694
|
+
connect: nil,
|
|
695
|
+
close: -> { closed = true },
|
|
696
|
+
readpartial: proc {
|
|
697
|
+
idx = [call_count, responses.length - 1].min
|
|
698
|
+
responses[idx].tap { call_count += 1 }
|
|
699
|
+
},
|
|
700
|
+
closed?: proc { closed }
|
|
701
|
+
)
|
|
702
|
+
ka_timeout_class = fake(new: ka_socket)
|
|
703
|
+
ka_opts = HTTP::Options.new(timeout_class: ka_timeout_class, keep_alive_timeout: 10)
|
|
704
|
+
conn = HTTP::Connection.new(req, ka_opts)
|
|
705
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
706
|
+
conn.instance_variable_set(:@persistent, true)
|
|
707
|
+
|
|
708
|
+
conn.read_headers!
|
|
709
|
+
|
|
710
|
+
assert_predicate conn, :keep_alive?
|
|
711
|
+
conn.finish_response
|
|
712
|
+
|
|
713
|
+
refute closed, "socket should NOT be closed when keep-alive"
|
|
714
|
+
assert_predicate conn, :finished_request?
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
def test_finish_response_resets_parser_for_reuse
|
|
718
|
+
req = build_req
|
|
719
|
+
call_count = 0
|
|
720
|
+
responses = [
|
|
721
|
+
"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
|
|
722
|
+
:eof
|
|
723
|
+
]
|
|
724
|
+
pr_socket = fake(
|
|
725
|
+
connect: nil,
|
|
726
|
+
close: nil,
|
|
727
|
+
readpartial: proc {
|
|
728
|
+
idx = [call_count, responses.length - 1].min
|
|
729
|
+
responses[idx].tap { call_count += 1 }
|
|
730
|
+
},
|
|
731
|
+
closed?: false
|
|
732
|
+
)
|
|
733
|
+
pr_timeout_class = fake(new: pr_socket)
|
|
734
|
+
pr_opts = HTTP::Options.new(timeout_class: pr_timeout_class)
|
|
735
|
+
conn = HTTP::Connection.new(req, pr_opts)
|
|
736
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
737
|
+
conn.instance_variable_set(:@persistent, true)
|
|
738
|
+
|
|
739
|
+
conn.read_headers!
|
|
740
|
+
|
|
741
|
+
assert_equal 200, conn.status_code
|
|
742
|
+
|
|
743
|
+
conn.finish_response
|
|
744
|
+
|
|
745
|
+
# Parser should be reset -- feeding new response should work
|
|
746
|
+
parser = conn.instance_variable_get(:@parser)
|
|
747
|
+
parser << "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n"
|
|
748
|
+
|
|
749
|
+
assert_equal 404, conn.status_code
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def test_finish_response_calls_reset_counter_when_socket_responds_to_it
|
|
753
|
+
req = build_req
|
|
754
|
+
counter_reset = false
|
|
755
|
+
call_count = 0
|
|
756
|
+
responses = [
|
|
757
|
+
"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
|
|
758
|
+
:eof
|
|
759
|
+
]
|
|
760
|
+
rc_socket = fake(
|
|
761
|
+
connect: nil,
|
|
762
|
+
close: nil,
|
|
763
|
+
readpartial: proc {
|
|
764
|
+
idx = [call_count, responses.length - 1].min
|
|
765
|
+
responses[idx].tap { call_count += 1 }
|
|
766
|
+
},
|
|
767
|
+
closed?: false,
|
|
768
|
+
reset_counter: -> { counter_reset = true }
|
|
769
|
+
)
|
|
770
|
+
rc_timeout_class = fake(new: rc_socket)
|
|
771
|
+
rc_opts = HTTP::Options.new(timeout_class: rc_timeout_class)
|
|
772
|
+
conn = HTTP::Connection.new(req, rc_opts)
|
|
773
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
774
|
+
conn.instance_variable_set(:@persistent, true)
|
|
775
|
+
|
|
776
|
+
conn.read_headers!
|
|
777
|
+
conn.finish_response
|
|
778
|
+
|
|
779
|
+
assert counter_reset, "reset_counter should have been called"
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
def test_finish_response_does_not_call_reset_counter_when_socket_does_not_respond
|
|
783
|
+
req = build_req
|
|
784
|
+
call_count = 0
|
|
785
|
+
responses = [
|
|
786
|
+
"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
|
|
787
|
+
:eof
|
|
788
|
+
]
|
|
789
|
+
# Socket without reset_counter method
|
|
790
|
+
no_rc_socket = fake(
|
|
791
|
+
connect: nil,
|
|
792
|
+
close: nil,
|
|
793
|
+
readpartial: proc {
|
|
794
|
+
idx = [call_count, responses.length - 1].min
|
|
795
|
+
responses[idx].tap { call_count += 1 }
|
|
796
|
+
},
|
|
797
|
+
closed?: false
|
|
798
|
+
)
|
|
799
|
+
no_rc_timeout_class = fake(new: no_rc_socket)
|
|
800
|
+
no_rc_opts = HTTP::Options.new(timeout_class: no_rc_timeout_class)
|
|
801
|
+
conn = HTTP::Connection.new(req, no_rc_opts)
|
|
802
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
803
|
+
conn.instance_variable_set(:@persistent, true)
|
|
804
|
+
|
|
805
|
+
conn.read_headers!
|
|
806
|
+
# Should not raise even though socket lacks reset_counter
|
|
807
|
+
conn.finish_response
|
|
808
|
+
|
|
809
|
+
assert_predicate conn, :finished_request?
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
def test_finish_response_resets_timer_for_persistent_connections
|
|
813
|
+
req = build_req
|
|
814
|
+
call_count = 0
|
|
815
|
+
responses = [
|
|
816
|
+
"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
|
|
817
|
+
:eof
|
|
818
|
+
]
|
|
819
|
+
rt_socket = fake(
|
|
820
|
+
connect: nil,
|
|
821
|
+
close: nil,
|
|
822
|
+
readpartial: proc {
|
|
823
|
+
idx = [call_count, responses.length - 1].min
|
|
824
|
+
responses[idx].tap { call_count += 1 }
|
|
825
|
+
},
|
|
826
|
+
closed?: false
|
|
827
|
+
)
|
|
828
|
+
rt_timeout_class = fake(new: rt_socket)
|
|
829
|
+
rt_opts = HTTP::Options.new(timeout_class: rt_timeout_class, keep_alive_timeout: 30)
|
|
830
|
+
conn = HTTP::Connection.new(req, rt_opts)
|
|
831
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
832
|
+
conn.instance_variable_set(:@persistent, true)
|
|
833
|
+
|
|
834
|
+
conn.read_headers!
|
|
835
|
+
|
|
836
|
+
before = Time.now
|
|
837
|
+
conn.finish_response
|
|
838
|
+
after = Time.now
|
|
839
|
+
|
|
840
|
+
expires = conn.instance_variable_get(:@conn_expires_at)
|
|
841
|
+
|
|
842
|
+
assert_operator expires, :>=, before + 30
|
|
843
|
+
assert_operator expires, :<=, after + 30
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
# ---------------------------------------------------------------------------
|
|
847
|
+
# #close
|
|
848
|
+
# ---------------------------------------------------------------------------
|
|
849
|
+
def test_close_when_socket_is_nil_raises_no_method_error
|
|
850
|
+
conn = HTTP::Connection.allocate
|
|
851
|
+
conn.instance_variable_set(:@socket, nil)
|
|
852
|
+
conn.instance_variable_set(:@pending_response, false)
|
|
853
|
+
conn.instance_variable_set(:@pending_request, false)
|
|
854
|
+
assert_raises(NoMethodError) { conn.close }
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
def test_close_closes_socket_and_clears_pending_state
|
|
858
|
+
req = build_req
|
|
859
|
+
closed = false
|
|
860
|
+
close_socket = fake(
|
|
861
|
+
connect: nil,
|
|
862
|
+
close: -> { closed = true },
|
|
863
|
+
closed?: proc { closed }
|
|
864
|
+
)
|
|
865
|
+
close_timeout_class = fake(new: close_socket)
|
|
866
|
+
close_opts = HTTP::Options.new(timeout_class: close_timeout_class)
|
|
867
|
+
conn = HTTP::Connection.new(req, close_opts)
|
|
868
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
869
|
+
conn.instance_variable_set(:@pending_request, true)
|
|
870
|
+
|
|
871
|
+
conn.close
|
|
872
|
+
|
|
873
|
+
assert closed, "socket should be closed"
|
|
874
|
+
refute conn.instance_variable_get(:@pending_response)
|
|
875
|
+
refute conn.instance_variable_get(:@pending_request)
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
def test_close_does_not_close_already_closed_socket
|
|
879
|
+
req = build_req
|
|
880
|
+
close_count = 0
|
|
881
|
+
already_closed_socket = fake(
|
|
882
|
+
connect: nil,
|
|
883
|
+
close: -> { close_count += 1 },
|
|
884
|
+
closed?: true
|
|
885
|
+
)
|
|
886
|
+
ac_timeout_class = fake(new: already_closed_socket)
|
|
887
|
+
ac_opts = HTTP::Options.new(timeout_class: ac_timeout_class)
|
|
888
|
+
conn = HTTP::Connection.new(req, ac_opts)
|
|
889
|
+
|
|
890
|
+
conn.close
|
|
891
|
+
|
|
892
|
+
assert_equal 0, close_count
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
# ---------------------------------------------------------------------------
|
|
896
|
+
# #finished_request?
|
|
897
|
+
# ---------------------------------------------------------------------------
|
|
898
|
+
def test_finished_request_returns_true_when_neither_pending
|
|
899
|
+
connection = build_connection
|
|
900
|
+
|
|
901
|
+
assert_predicate connection, :finished_request?
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
def test_finished_request_returns_false_when_pending_response
|
|
905
|
+
connection = build_connection
|
|
906
|
+
connection.instance_variable_set(:@pending_response, true)
|
|
907
|
+
|
|
908
|
+
refute_predicate connection, :finished_request?
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
def test_finished_request_returns_false_when_pending_request
|
|
912
|
+
connection = build_connection
|
|
913
|
+
connection.instance_variable_set(:@pending_request, true)
|
|
914
|
+
|
|
915
|
+
refute_predicate connection, :finished_request?
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
def test_finished_request_returns_false_when_both_pending
|
|
919
|
+
connection = build_connection
|
|
920
|
+
connection.instance_variable_set(:@pending_request, true)
|
|
921
|
+
connection.instance_variable_set(:@pending_response, true)
|
|
922
|
+
|
|
923
|
+
refute_predicate connection, :finished_request?
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
# ---------------------------------------------------------------------------
|
|
927
|
+
# #keep_alive?
|
|
928
|
+
# ---------------------------------------------------------------------------
|
|
929
|
+
def test_keep_alive_returns_false_when_keep_alive_is_false
|
|
930
|
+
connection = build_connection
|
|
931
|
+
connection.instance_variable_set(:@keep_alive, false)
|
|
932
|
+
|
|
933
|
+
refute_predicate connection, :keep_alive?
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
def test_keep_alive_returns_false_when_socket_is_closed
|
|
937
|
+
req = build_req
|
|
938
|
+
closed_socket = fake(connect: nil, close: nil, closed?: true)
|
|
939
|
+
closed_timeout_class = fake(new: closed_socket)
|
|
940
|
+
closed_opts = HTTP::Options.new(timeout_class: closed_timeout_class)
|
|
941
|
+
conn = HTTP::Connection.new(req, closed_opts)
|
|
942
|
+
conn.instance_variable_set(:@keep_alive, true)
|
|
943
|
+
|
|
944
|
+
refute_predicate conn, :keep_alive?
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
def test_keep_alive_returns_true_when_keep_alive_and_socket_open
|
|
948
|
+
req = build_req
|
|
949
|
+
open_socket = fake(connect: nil, close: nil, closed?: false)
|
|
950
|
+
open_timeout_class = fake(new: open_socket)
|
|
951
|
+
open_opts = HTTP::Options.new(timeout_class: open_timeout_class)
|
|
952
|
+
conn = HTTP::Connection.new(req, open_opts)
|
|
953
|
+
conn.instance_variable_set(:@keep_alive, true)
|
|
954
|
+
|
|
955
|
+
assert_predicate conn, :keep_alive?
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
# ---------------------------------------------------------------------------
|
|
959
|
+
# #expired?
|
|
960
|
+
# ---------------------------------------------------------------------------
|
|
961
|
+
def test_expired_returns_true_when_conn_expires_at_is_nil
|
|
962
|
+
connection = build_connection
|
|
963
|
+
|
|
964
|
+
assert_predicate connection, :expired?
|
|
965
|
+
end
|
|
966
|
+
|
|
967
|
+
def test_expired_returns_true_when_connection_has_expired
|
|
968
|
+
connection = build_connection
|
|
969
|
+
connection.instance_variable_set(:@conn_expires_at, Time.now - 1)
|
|
970
|
+
|
|
971
|
+
assert_predicate connection, :expired?
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
def test_expired_returns_false_when_connection_has_not_expired
|
|
975
|
+
connection = build_connection
|
|
976
|
+
connection.instance_variable_set(:@conn_expires_at, Time.now + 60)
|
|
977
|
+
|
|
978
|
+
refute_predicate connection, :expired?
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
# ---------------------------------------------------------------------------
|
|
982
|
+
# keep_alive behavior (set_keep_alive)
|
|
983
|
+
# ---------------------------------------------------------------------------
|
|
984
|
+
def test_keep_alive_with_http10_and_keep_alive_header
|
|
985
|
+
req = build_req
|
|
986
|
+
response = "HTTP/1.0 200 OK\r\nConnection: Keep-Alive\r\nContent-Length: 2\r\n\r\nOK"
|
|
987
|
+
ka_socket = fake(connect: nil, close: nil, readpartial: response, closed?: false)
|
|
988
|
+
ka_timeout_class = fake(new: ka_socket)
|
|
989
|
+
ka_opts = HTTP::Options.new(timeout_class: ka_timeout_class)
|
|
990
|
+
conn = HTTP::Connection.new(req, ka_opts)
|
|
991
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
992
|
+
conn.instance_variable_set(:@persistent, true)
|
|
993
|
+
|
|
994
|
+
conn.read_headers!
|
|
995
|
+
|
|
996
|
+
assert_predicate conn, :keep_alive?
|
|
997
|
+
end
|
|
998
|
+
|
|
999
|
+
def test_keep_alive_with_http10_without_keep_alive_header
|
|
1000
|
+
req = build_req
|
|
1001
|
+
response = "HTTP/1.0 200 OK\r\nContent-Length: 2\r\n\r\nOK"
|
|
1002
|
+
ka_socket = fake(connect: nil, close: nil, readpartial: response, closed?: false)
|
|
1003
|
+
ka_timeout_class = fake(new: ka_socket)
|
|
1004
|
+
ka_opts = HTTP::Options.new(timeout_class: ka_timeout_class)
|
|
1005
|
+
conn = HTTP::Connection.new(req, ka_opts)
|
|
1006
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
1007
|
+
conn.instance_variable_set(:@persistent, true)
|
|
1008
|
+
|
|
1009
|
+
conn.read_headers!
|
|
1010
|
+
|
|
1011
|
+
refute_predicate conn, :keep_alive?
|
|
1012
|
+
end
|
|
1013
|
+
|
|
1014
|
+
def test_keep_alive_with_http11_and_no_connection_header
|
|
1015
|
+
req = build_req
|
|
1016
|
+
response = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK"
|
|
1017
|
+
ka_socket = fake(connect: nil, close: nil, readpartial: response, closed?: false)
|
|
1018
|
+
ka_timeout_class = fake(new: ka_socket)
|
|
1019
|
+
ka_opts = HTTP::Options.new(timeout_class: ka_timeout_class)
|
|
1020
|
+
conn = HTTP::Connection.new(req, ka_opts)
|
|
1021
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
1022
|
+
conn.instance_variable_set(:@persistent, true)
|
|
1023
|
+
|
|
1024
|
+
conn.read_headers!
|
|
1025
|
+
|
|
1026
|
+
assert_predicate conn, :keep_alive?
|
|
1027
|
+
end
|
|
1028
|
+
|
|
1029
|
+
def test_keep_alive_with_http11_and_connection_close
|
|
1030
|
+
req = build_req
|
|
1031
|
+
response = "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 2\r\n\r\nOK"
|
|
1032
|
+
ka_socket = fake(connect: nil, close: nil, readpartial: response, closed?: false)
|
|
1033
|
+
ka_timeout_class = fake(new: ka_socket)
|
|
1034
|
+
ka_opts = HTTP::Options.new(timeout_class: ka_timeout_class)
|
|
1035
|
+
conn = HTTP::Connection.new(req, ka_opts)
|
|
1036
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
1037
|
+
conn.instance_variable_set(:@persistent, true)
|
|
1038
|
+
|
|
1039
|
+
conn.read_headers!
|
|
1040
|
+
|
|
1041
|
+
refute_predicate conn, :keep_alive?
|
|
1042
|
+
end
|
|
1043
|
+
|
|
1044
|
+
def test_keep_alive_with_unknown_http_version
|
|
1045
|
+
req = build_req
|
|
1046
|
+
response = "HTTP/2.0 200 OK\r\nContent-Length: 2\r\n\r\nOK"
|
|
1047
|
+
ka_socket = fake(connect: nil, close: nil, readpartial: response, closed?: false)
|
|
1048
|
+
ka_timeout_class = fake(new: ka_socket)
|
|
1049
|
+
ka_opts = HTTP::Options.new(timeout_class: ka_timeout_class)
|
|
1050
|
+
conn = HTTP::Connection.new(req, ka_opts)
|
|
1051
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
1052
|
+
conn.instance_variable_set(:@persistent, true)
|
|
1053
|
+
|
|
1054
|
+
conn.read_headers!
|
|
1055
|
+
|
|
1056
|
+
refute_predicate conn, :keep_alive?
|
|
1057
|
+
end
|
|
1058
|
+
|
|
1059
|
+
def test_keep_alive_when_not_persistent_sets_keep_alive_false
|
|
1060
|
+
req = build_req
|
|
1061
|
+
response = "HTTP/1.1 200 OK\r\nConnection: Keep-Alive\r\nContent-Length: 2\r\n\r\nOK"
|
|
1062
|
+
ka_socket = fake(connect: nil, close: nil, readpartial: response, closed?: false)
|
|
1063
|
+
ka_timeout_class = fake(new: ka_socket)
|
|
1064
|
+
ka_opts = HTTP::Options.new(timeout_class: ka_timeout_class)
|
|
1065
|
+
conn = HTTP::Connection.new(req, ka_opts)
|
|
1066
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
1067
|
+
|
|
1068
|
+
conn.read_headers!
|
|
1069
|
+
|
|
1070
|
+
refute_predicate conn, :keep_alive?
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
# ---------------------------------------------------------------------------
|
|
1074
|
+
# proxy connect
|
|
1075
|
+
# ---------------------------------------------------------------------------
|
|
1076
|
+
def test_proxy_connect_non_200_marks_failed_and_stores_headers
|
|
1077
|
+
proxy_req = build_req(
|
|
1078
|
+
uri: "https://example.com/",
|
|
1079
|
+
proxy: { proxy_address: "proxy.example.com", proxy_port: 8080 }
|
|
1080
|
+
)
|
|
1081
|
+
proxy_response = "HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic\r\n\r\n"
|
|
1082
|
+
call_count = 0
|
|
1083
|
+
proxy_socket = fake(
|
|
1084
|
+
connect: nil,
|
|
1085
|
+
close: nil,
|
|
1086
|
+
write: lambda(&:bytesize),
|
|
1087
|
+
readpartial: proc {
|
|
1088
|
+
call_count += 1
|
|
1089
|
+
call_count == 1 ? proxy_response : :eof
|
|
1090
|
+
},
|
|
1091
|
+
start_tls: ->(*) {}
|
|
1092
|
+
)
|
|
1093
|
+
proxy_timeout_class = fake(new: proxy_socket)
|
|
1094
|
+
proxy_opts = HTTP::Options.new(timeout_class: proxy_timeout_class)
|
|
1095
|
+
conn = HTTP::Connection.new(proxy_req, proxy_opts)
|
|
1096
|
+
|
|
1097
|
+
assert_predicate conn, :failed_proxy_connect?
|
|
1098
|
+
assert_instance_of HTTP::Headers, conn.proxy_response_headers
|
|
1099
|
+
assert_equal "Basic", conn.proxy_response_headers["Proxy-Authenticate"]
|
|
1100
|
+
# pending_response should still be true (not reset on failure)
|
|
1101
|
+
assert conn.instance_variable_get(:@pending_response)
|
|
1102
|
+
end
|
|
1103
|
+
|
|
1104
|
+
def test_proxy_connect_200_completes_successfully_and_resets_parser
|
|
1105
|
+
proxy_req = build_req(
|
|
1106
|
+
uri: "https://example.com/",
|
|
1107
|
+
proxy: { proxy_address: "proxy.example.com", proxy_port: 8080 }
|
|
1108
|
+
)
|
|
1109
|
+
proxy_response = "HTTP/1.1 200 Connection established\r\n\r\n"
|
|
1110
|
+
proxy_socket = fake(
|
|
1111
|
+
connect: nil,
|
|
1112
|
+
close: nil,
|
|
1113
|
+
write: lambda(&:bytesize),
|
|
1114
|
+
readpartial: proxy_response,
|
|
1115
|
+
start_tls: ->(*) {},
|
|
1116
|
+
"hostname=": ->(*) {},
|
|
1117
|
+
"sync_close=": ->(*) {},
|
|
1118
|
+
post_connection_check: ->(*) {}
|
|
1119
|
+
)
|
|
1120
|
+
proxy_timeout_class = fake(new: proxy_socket)
|
|
1121
|
+
proxy_opts = HTTP::Options.new(timeout_class: proxy_timeout_class)
|
|
1122
|
+
conn = HTTP::Connection.new(proxy_req, proxy_opts)
|
|
1123
|
+
|
|
1124
|
+
refute_predicate conn, :failed_proxy_connect?
|
|
1125
|
+
assert_instance_of HTTP::Headers, conn.proxy_response_headers
|
|
1126
|
+
# Parser should have been reset, pending_response should be false
|
|
1127
|
+
assert_predicate conn, :finished_request?
|
|
1128
|
+
end
|
|
1129
|
+
|
|
1130
|
+
def test_proxy_connect_skips_for_http_request
|
|
1131
|
+
http_proxy_req = build_req(
|
|
1132
|
+
uri: "http://example.com/",
|
|
1133
|
+
proxy: { proxy_address: "proxy.example.com", proxy_port: 8080 }
|
|
1134
|
+
)
|
|
1135
|
+
connect_called = false
|
|
1136
|
+
plain_socket = fake(
|
|
1137
|
+
connect: nil,
|
|
1138
|
+
close: nil,
|
|
1139
|
+
connect_using_proxy: ->(*) { connect_called = true }
|
|
1140
|
+
)
|
|
1141
|
+
plain_timeout_class = fake(new: plain_socket)
|
|
1142
|
+
plain_opts = HTTP::Options.new(timeout_class: plain_timeout_class)
|
|
1143
|
+
conn = HTTP::Connection.new(http_proxy_req, plain_opts)
|
|
1144
|
+
|
|
1145
|
+
refute connect_called
|
|
1146
|
+
refute_predicate conn, :failed_proxy_connect?
|
|
1147
|
+
end
|
|
1148
|
+
|
|
1149
|
+
# ---------------------------------------------------------------------------
|
|
1150
|
+
# start_tls
|
|
1151
|
+
# ---------------------------------------------------------------------------
|
|
1152
|
+
def test_start_tls_uses_provided_ssl_context
|
|
1153
|
+
ssl_ctx = OpenSSL::SSL::SSLContext.new
|
|
1154
|
+
https_req = build_req(uri: "https://example.com/")
|
|
1155
|
+
|
|
1156
|
+
start_tls_args = nil
|
|
1157
|
+
tls_socket = fake(
|
|
1158
|
+
connect: nil,
|
|
1159
|
+
close: nil,
|
|
1160
|
+
start_tls: ->(*args) { start_tls_args = args }
|
|
1161
|
+
)
|
|
1162
|
+
tls_timeout_class = fake(new: tls_socket)
|
|
1163
|
+
tls_opts = HTTP::Options.new(timeout_class: tls_timeout_class, ssl_context: ssl_ctx)
|
|
1164
|
+
|
|
1165
|
+
HTTP::Connection.new(https_req, tls_opts)
|
|
1166
|
+
|
|
1167
|
+
assert_equal "example.com", start_tls_args[0]
|
|
1168
|
+
assert_equal ssl_ctx, start_tls_args[2]
|
|
1169
|
+
end
|
|
1170
|
+
|
|
1171
|
+
def test_start_tls_creates_ssl_context_when_not_provided
|
|
1172
|
+
https_req = build_req(uri: "https://example.com/")
|
|
1173
|
+
|
|
1174
|
+
start_tls_args = nil
|
|
1175
|
+
tls_socket = fake(
|
|
1176
|
+
connect: nil,
|
|
1177
|
+
close: nil,
|
|
1178
|
+
start_tls: ->(*args) { start_tls_args = args }
|
|
1179
|
+
)
|
|
1180
|
+
tls_timeout_class = fake(new: tls_socket)
|
|
1181
|
+
tls_opts = HTTP::Options.new(timeout_class: tls_timeout_class)
|
|
1182
|
+
|
|
1183
|
+
HTTP::Connection.new(https_req, tls_opts)
|
|
1184
|
+
|
|
1185
|
+
assert_equal "example.com", start_tls_args[0]
|
|
1186
|
+
assert_instance_of OpenSSL::SSL::SSLContext, start_tls_args[2]
|
|
1187
|
+
end
|
|
1188
|
+
|
|
1189
|
+
def test_start_tls_passes_ssl_socket_class
|
|
1190
|
+
https_req = build_req(uri: "https://example.com/")
|
|
1191
|
+
|
|
1192
|
+
start_tls_args = nil
|
|
1193
|
+
tls_socket = fake(
|
|
1194
|
+
connect: nil,
|
|
1195
|
+
close: nil,
|
|
1196
|
+
start_tls: ->(*args) { start_tls_args = args }
|
|
1197
|
+
)
|
|
1198
|
+
tls_timeout_class = fake(new: tls_socket)
|
|
1199
|
+
custom_ssl_class = Class.new
|
|
1200
|
+
tls_opts = HTTP::Options.new(timeout_class: tls_timeout_class, ssl_socket_class: custom_ssl_class)
|
|
1201
|
+
|
|
1202
|
+
HTTP::Connection.new(https_req, tls_opts)
|
|
1203
|
+
|
|
1204
|
+
assert_equal custom_ssl_class, start_tls_args[1]
|
|
1205
|
+
end
|
|
1206
|
+
|
|
1207
|
+
def test_start_tls_passes_host_from_req_uri_host
|
|
1208
|
+
https_req = build_req(uri: "https://subdomain.example.com/")
|
|
1209
|
+
|
|
1210
|
+
start_tls_args = nil
|
|
1211
|
+
tls_socket = fake(
|
|
1212
|
+
connect: nil,
|
|
1213
|
+
close: nil,
|
|
1214
|
+
start_tls: ->(*args) { start_tls_args = args }
|
|
1215
|
+
)
|
|
1216
|
+
tls_timeout_class = fake(new: tls_socket)
|
|
1217
|
+
tls_opts = HTTP::Options.new(timeout_class: tls_timeout_class)
|
|
1218
|
+
|
|
1219
|
+
HTTP::Connection.new(https_req, tls_opts)
|
|
1220
|
+
|
|
1221
|
+
assert_equal "subdomain.example.com", start_tls_args[0]
|
|
1222
|
+
end
|
|
1223
|
+
|
|
1224
|
+
def test_start_tls_skips_tls_for_http_requests
|
|
1225
|
+
http_req = build_req(uri: "http://example.com/")
|
|
1226
|
+
start_tls_called = false
|
|
1227
|
+
no_tls_socket = fake(
|
|
1228
|
+
connect: nil,
|
|
1229
|
+
close: nil,
|
|
1230
|
+
start_tls: ->(*) { start_tls_called = true }
|
|
1231
|
+
)
|
|
1232
|
+
no_tls_timeout_class = fake(new: no_tls_socket)
|
|
1233
|
+
no_tls_opts = HTTP::Options.new(timeout_class: no_tls_timeout_class)
|
|
1234
|
+
|
|
1235
|
+
HTTP::Connection.new(http_req, no_tls_opts)
|
|
1236
|
+
|
|
1237
|
+
refute start_tls_called
|
|
1238
|
+
end
|
|
1239
|
+
|
|
1240
|
+
def test_start_tls_skips_tls_when_proxy_connect_failed
|
|
1241
|
+
proxy_req = build_req(
|
|
1242
|
+
uri: "https://example.com/",
|
|
1243
|
+
proxy: { proxy_address: "proxy.example.com", proxy_port: 8080 }
|
|
1244
|
+
)
|
|
1245
|
+
proxy_response = "HTTP/1.1 407 Auth Required\r\nContent-Length: 0\r\n\r\n"
|
|
1246
|
+
call_count = 0
|
|
1247
|
+
start_tls_called = false
|
|
1248
|
+
proxy_socket = fake(
|
|
1249
|
+
connect: nil,
|
|
1250
|
+
close: nil,
|
|
1251
|
+
write: lambda(&:bytesize),
|
|
1252
|
+
readpartial: proc {
|
|
1253
|
+
call_count += 1
|
|
1254
|
+
call_count == 1 ? proxy_response : :eof
|
|
1255
|
+
},
|
|
1256
|
+
start_tls: ->(*) { start_tls_called = true }
|
|
1257
|
+
)
|
|
1258
|
+
proxy_timeout_class = fake(new: proxy_socket)
|
|
1259
|
+
proxy_opts = HTTP::Options.new(timeout_class: proxy_timeout_class)
|
|
1260
|
+
|
|
1261
|
+
conn = HTTP::Connection.new(proxy_req, proxy_opts)
|
|
1262
|
+
|
|
1263
|
+
assert_predicate conn, :failed_proxy_connect?
|
|
1264
|
+
refute start_tls_called
|
|
1265
|
+
end
|
|
1266
|
+
|
|
1267
|
+
def test_start_tls_applies_ssl_options_via_set_params_when_no_ssl_context
|
|
1268
|
+
https_req = build_req(uri: "https://example.com/")
|
|
1269
|
+
|
|
1270
|
+
start_tls_args = nil
|
|
1271
|
+
tls_socket = fake(
|
|
1272
|
+
connect: nil,
|
|
1273
|
+
close: nil,
|
|
1274
|
+
start_tls: ->(*args) { start_tls_args = args }
|
|
1275
|
+
)
|
|
1276
|
+
tls_timeout_class = fake(new: tls_socket)
|
|
1277
|
+
ssl_options = { verify_mode: OpenSSL::SSL::VERIFY_NONE }
|
|
1278
|
+
tls_opts = HTTP::Options.new(timeout_class: tls_timeout_class, ssl: ssl_options)
|
|
1279
|
+
|
|
1280
|
+
HTTP::Connection.new(https_req, tls_opts)
|
|
1281
|
+
|
|
1282
|
+
ctx = start_tls_args[2]
|
|
1283
|
+
|
|
1284
|
+
assert_instance_of OpenSSL::SSL::SSLContext, ctx
|
|
1285
|
+
assert_equal OpenSSL::SSL::VERIFY_NONE, ctx.verify_mode
|
|
1286
|
+
end
|
|
1287
|
+
|
|
1288
|
+
# ---------------------------------------------------------------------------
|
|
1289
|
+
# read_more and error handling
|
|
1290
|
+
# ---------------------------------------------------------------------------
|
|
1291
|
+
def test_read_more_handles_nil_from_readpartial
|
|
1292
|
+
req = build_req
|
|
1293
|
+
call_count = 0
|
|
1294
|
+
responses = ["HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n", nil, :eof]
|
|
1295
|
+
rm_socket = fake(
|
|
1296
|
+
connect: nil,
|
|
1297
|
+
close: nil,
|
|
1298
|
+
readpartial: proc { responses[[call_count, responses.length - 1].min].tap { call_count += 1 } }
|
|
1299
|
+
)
|
|
1300
|
+
rm_timeout_class = fake(new: rm_socket)
|
|
1301
|
+
rm_opts = HTTP::Options.new(timeout_class: rm_timeout_class)
|
|
1302
|
+
conn = HTTP::Connection.new(req, rm_opts)
|
|
1303
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
1304
|
+
|
|
1305
|
+
conn.read_headers!
|
|
1306
|
+
result = conn.readpartial
|
|
1307
|
+
|
|
1308
|
+
assert_equal "", result
|
|
1309
|
+
end
|
|
1310
|
+
|
|
1311
|
+
def test_read_more_raises_socket_read_error_on_io_errors
|
|
1312
|
+
req = build_req
|
|
1313
|
+
call_count = 0
|
|
1314
|
+
rm_socket = fake(
|
|
1315
|
+
connect: nil,
|
|
1316
|
+
close: nil,
|
|
1317
|
+
readpartial: proc {
|
|
1318
|
+
call_count += 1
|
|
1319
|
+
raise IOError, "broken" unless call_count == 1
|
|
1320
|
+
|
|
1321
|
+
"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n"
|
|
1322
|
+
}
|
|
1323
|
+
)
|
|
1324
|
+
rm_timeout_class = fake(new: rm_socket)
|
|
1325
|
+
rm_opts = HTTP::Options.new(timeout_class: rm_timeout_class)
|
|
1326
|
+
conn = HTTP::Connection.new(req, rm_opts)
|
|
1327
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
1328
|
+
|
|
1329
|
+
conn.read_headers!
|
|
1330
|
+
err = assert_raises(HTTP::ConnectionError) { conn.readpartial }
|
|
1331
|
+
assert_includes err.message, "error reading from socket"
|
|
1332
|
+
assert_includes err.message, "broken"
|
|
1333
|
+
end
|
|
1334
|
+
|
|
1335
|
+
def test_read_more_raises_socket_read_error_on_socket_error
|
|
1336
|
+
req = build_req
|
|
1337
|
+
call_count = 0
|
|
1338
|
+
rm_socket = fake(
|
|
1339
|
+
connect: nil,
|
|
1340
|
+
close: nil,
|
|
1341
|
+
readpartial: proc {
|
|
1342
|
+
call_count += 1
|
|
1343
|
+
raise SocketError, "socket error" unless call_count == 1
|
|
1344
|
+
|
|
1345
|
+
"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n"
|
|
1346
|
+
}
|
|
1347
|
+
)
|
|
1348
|
+
rm_timeout_class = fake(new: rm_socket)
|
|
1349
|
+
rm_opts = HTTP::Options.new(timeout_class: rm_timeout_class)
|
|
1350
|
+
conn = HTTP::Connection.new(req, rm_opts)
|
|
1351
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
1352
|
+
|
|
1353
|
+
conn.read_headers!
|
|
1354
|
+
err = assert_raises(HTTP::ConnectionError) { conn.readpartial }
|
|
1355
|
+
assert_includes err.message, "error reading from socket"
|
|
1356
|
+
assert_includes err.message, "socket error"
|
|
1357
|
+
end
|
|
1358
|
+
|
|
1359
|
+
def test_read_more_passes_buffer_to_socket_readpartial
|
|
1360
|
+
req = build_req
|
|
1361
|
+
read_args = []
|
|
1362
|
+
call_count = 0
|
|
1363
|
+
responses = [
|
|
1364
|
+
"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n",
|
|
1365
|
+
"hello",
|
|
1366
|
+
:eof
|
|
1367
|
+
]
|
|
1368
|
+
buf_socket = fake(
|
|
1369
|
+
connect: nil,
|
|
1370
|
+
close: nil,
|
|
1371
|
+
readpartial: proc { |*args|
|
|
1372
|
+
read_args << args
|
|
1373
|
+
idx = [call_count, responses.length - 1].min
|
|
1374
|
+
responses[idx].tap { call_count += 1 }
|
|
1375
|
+
},
|
|
1376
|
+
closed?: false
|
|
1377
|
+
)
|
|
1378
|
+
buf_timeout_class = fake(new: buf_socket)
|
|
1379
|
+
buf_opts = HTTP::Options.new(timeout_class: buf_timeout_class)
|
|
1380
|
+
conn = HTTP::Connection.new(req, buf_opts)
|
|
1381
|
+
conn.instance_variable_set(:@pending_response, true)
|
|
1382
|
+
|
|
1383
|
+
conn.read_headers!
|
|
1384
|
+
conn.readpartial
|
|
1385
|
+
|
|
1386
|
+
# Verify that readpartial was called with 2 arguments (size + buffer)
|
|
1387
|
+
assert(read_args.all? { |a| a.length == 2 }, "readpartial should receive size and buffer")
|
|
1388
|
+
# Verify the buffer is a string (not nil)
|
|
1389
|
+
assert(read_args.all? { |a| a[1].is_a?(String) }, "buffer should be a String")
|
|
1390
|
+
end
|
|
1391
|
+
|
|
1392
|
+
# ---------------------------------------------------------------------------
|
|
1393
|
+
# connect_socket
|
|
1394
|
+
# ---------------------------------------------------------------------------
|
|
1395
|
+
def test_connect_socket_passes_correct_arguments
|
|
1396
|
+
req = build_req
|
|
1397
|
+
connect_args = nil
|
|
1398
|
+
connect_kwargs = nil
|
|
1399
|
+
cs_socket = fake(
|
|
1400
|
+
connect: lambda { |*args, **kwargs|
|
|
1401
|
+
connect_args = args
|
|
1402
|
+
connect_kwargs = kwargs
|
|
1403
|
+
},
|
|
1404
|
+
close: nil
|
|
1405
|
+
)
|
|
1406
|
+
cs_timeout_class = fake(new: cs_socket)
|
|
1407
|
+
cs_opts = HTTP::Options.new(timeout_class: cs_timeout_class)
|
|
1408
|
+
|
|
1409
|
+
HTTP::Connection.new(req, cs_opts)
|
|
1410
|
+
|
|
1411
|
+
assert_equal HTTP::Options.default_socket_class, connect_args[0]
|
|
1412
|
+
assert_equal "example.com", connect_args[1]
|
|
1413
|
+
assert_equal 80, connect_args[2]
|
|
1414
|
+
refute connect_kwargs.fetch(:nodelay)
|
|
1415
|
+
end
|
|
1416
|
+
|
|
1417
|
+
def test_connect_socket_passes_timeout_options_to_timeout_class_new
|
|
1418
|
+
req = build_req
|
|
1419
|
+
new_kwargs = nil
|
|
1420
|
+
cs_socket = fake(connect: nil, close: nil)
|
|
1421
|
+
cs_timeout_class = fake(
|
|
1422
|
+
new: lambda { |**kwargs|
|
|
1423
|
+
new_kwargs = kwargs
|
|
1424
|
+
cs_socket
|
|
1425
|
+
}
|
|
1426
|
+
)
|
|
1427
|
+
cs_opts = HTTP::Options.new(timeout_class: cs_timeout_class)
|
|
1428
|
+
|
|
1429
|
+
HTTP::Connection.new(req, cs_opts)
|
|
1430
|
+
|
|
1431
|
+
assert_instance_of Hash, new_kwargs
|
|
1432
|
+
end
|
|
1433
|
+
|
|
1434
|
+
def test_connect_socket_calls_reset_timer_during_initialization
|
|
1435
|
+
req = build_req
|
|
1436
|
+
persist_socket = fake(connect: nil, close: nil)
|
|
1437
|
+
persist_timeout_class = fake(new: persist_socket)
|
|
1438
|
+
persist_opts = HTTP::Options.new(timeout_class: persist_timeout_class, keep_alive_timeout: 5)
|
|
1439
|
+
conn = HTTP::Connection.new(req, persist_opts)
|
|
1440
|
+
conn.instance_variable_set(:@persistent, true)
|
|
1441
|
+
|
|
1442
|
+
# For persistent connections, reset_timer should set @conn_expires_at
|
|
1443
|
+
before = Time.now
|
|
1444
|
+
conn.send(:reset_timer)
|
|
1445
|
+
after = Time.now
|
|
1446
|
+
|
|
1447
|
+
expires = conn.instance_variable_get(:@conn_expires_at)
|
|
1448
|
+
|
|
1449
|
+
assert_operator expires, :>=, before + 5
|
|
1450
|
+
assert_operator expires, :<=, after + 5
|
|
1451
|
+
end
|
|
1452
|
+
|
|
1453
|
+
# ---------------------------------------------------------------------------
|
|
1454
|
+
# reset_timer
|
|
1455
|
+
# ---------------------------------------------------------------------------
|
|
1456
|
+
def test_reset_timer_sets_conn_expires_at_for_persistent_connections
|
|
1457
|
+
req = build_req
|
|
1458
|
+
persist_socket = fake(connect: nil, close: nil)
|
|
1459
|
+
persist_timeout_class = fake(new: persist_socket)
|
|
1460
|
+
persist_opts = HTTP::Options.new(timeout_class: persist_timeout_class, keep_alive_timeout: 5)
|
|
1461
|
+
conn = HTTP::Connection.new(req, persist_opts)
|
|
1462
|
+
conn.instance_variable_set(:@persistent, true)
|
|
1463
|
+
|
|
1464
|
+
before = Time.now
|
|
1465
|
+
conn.send(:reset_timer)
|
|
1466
|
+
after = Time.now
|
|
1467
|
+
|
|
1468
|
+
expires = conn.instance_variable_get(:@conn_expires_at)
|
|
1469
|
+
|
|
1470
|
+
assert_operator expires, :>=, before + 5
|
|
1471
|
+
assert_operator expires, :<=, after + 5
|
|
1472
|
+
end
|
|
1473
|
+
|
|
1474
|
+
def test_reset_timer_does_not_set_conn_expires_at_for_non_persistent
|
|
1475
|
+
connection = build_connection
|
|
1476
|
+
connection.instance_variable_set(:@conn_expires_at, nil)
|
|
1477
|
+
connection.send(:reset_timer)
|
|
1478
|
+
|
|
1479
|
+
assert_nil connection.instance_variable_get(:@conn_expires_at)
|
|
1480
|
+
end
|
|
1481
|
+
|
|
1482
|
+
def test_reset_timer_uses_keep_alive_timeout_from_options
|
|
1483
|
+
req = build_req
|
|
1484
|
+
persist_socket = fake(connect: nil, close: nil)
|
|
1485
|
+
persist_timeout_class = fake(new: persist_socket)
|
|
1486
|
+
persist_opts = HTTP::Options.new(timeout_class: persist_timeout_class, keep_alive_timeout: 42)
|
|
1487
|
+
conn = HTTP::Connection.new(req, persist_opts)
|
|
1488
|
+
conn.instance_variable_set(:@persistent, true)
|
|
1489
|
+
|
|
1490
|
+
before = Time.now
|
|
1491
|
+
conn.send(:reset_timer)
|
|
1492
|
+
|
|
1493
|
+
expires = conn.instance_variable_get(:@conn_expires_at)
|
|
1494
|
+
# Should be approximately now + 42
|
|
1495
|
+
assert_operator expires, :>=, before + 42
|
|
1496
|
+
assert_operator expires, :<, before + 43
|
|
1497
|
+
end
|
|
1498
|
+
|
|
1499
|
+
# ---------------------------------------------------------------------------
|
|
1500
|
+
# init_state
|
|
1501
|
+
# ---------------------------------------------------------------------------
|
|
1502
|
+
def test_init_state_stores_persistent_flag_from_options
|
|
1503
|
+
req = build_req
|
|
1504
|
+
persist_socket = fake(connect: nil, close: nil)
|
|
1505
|
+
persist_timeout_class = fake(new: persist_socket)
|
|
1506
|
+
persist_opts = HTTP::Options.new(timeout_class: persist_timeout_class, persistent: "http://example.com")
|
|
1507
|
+
conn = HTTP::Connection.new(req, persist_opts)
|
|
1508
|
+
|
|
1509
|
+
assert conn.instance_variable_get(:@persistent)
|
|
1510
|
+
end
|
|
1511
|
+
|
|
1512
|
+
def test_init_state_stores_keep_alive_timeout_as_float
|
|
1513
|
+
connection = build_connection
|
|
1514
|
+
kat = connection.instance_variable_get(:@keep_alive_timeout)
|
|
1515
|
+
|
|
1516
|
+
assert_instance_of Float, kat
|
|
1517
|
+
end
|
|
1518
|
+
|
|
1519
|
+
def test_init_state_initializes_buffer_as_binary_empty_string
|
|
1520
|
+
connection = build_connection
|
|
1521
|
+
buf = connection.instance_variable_get(:@buffer)
|
|
1522
|
+
|
|
1523
|
+
assert_equal "".b, buf
|
|
1524
|
+
assert_equal Encoding::ASCII_8BIT, buf.encoding
|
|
1525
|
+
end
|
|
1526
|
+
|
|
1527
|
+
def test_init_state_initializes_parser
|
|
1528
|
+
connection = build_connection
|
|
1529
|
+
parser = connection.instance_variable_get(:@parser)
|
|
1530
|
+
|
|
1531
|
+
assert_instance_of HTTP::Response::Parser, parser
|
|
1532
|
+
end
|
|
1533
|
+
end
|