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,391 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class HTTPRequestWriterTest < Minitest::Test
|
|
6
|
+
cover "HTTP::Request::Writer*"
|
|
7
|
+
|
|
8
|
+
def build_writer(io: StringIO.new, body: HTTP::Request::Body.new(""), headers: HTTP::Headers.new,
|
|
9
|
+
headerstart: "GET /test HTTP/1.1")
|
|
10
|
+
HTTP::Request::Writer.new(io, body, headers, headerstart)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# #stream
|
|
14
|
+
|
|
15
|
+
def test_stream_with_multiple_headers_separates_with_crlf
|
|
16
|
+
io = StringIO.new
|
|
17
|
+
headers = HTTP::Headers.coerce "Host" => "example.org"
|
|
18
|
+
headerstart = "GET /test HTTP/1.1"
|
|
19
|
+
writer = build_writer(io: io, headers: headers, headerstart: headerstart)
|
|
20
|
+
writer.stream
|
|
21
|
+
|
|
22
|
+
assert_equal [
|
|
23
|
+
"#{headerstart}\r\n",
|
|
24
|
+
"Host: example.org\r\nContent-Length: 0\r\n\r\n"
|
|
25
|
+
].join, io.string
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_stream_with_mixed_case_headers_writes_with_same_casing
|
|
29
|
+
io = StringIO.new
|
|
30
|
+
headers = HTTP::Headers.coerce "content-Type" => "text", "X_MAX" => "200"
|
|
31
|
+
headerstart = "GET /test HTTP/1.1"
|
|
32
|
+
writer = build_writer(io: io, headers: headers, headerstart: headerstart)
|
|
33
|
+
writer.stream
|
|
34
|
+
|
|
35
|
+
assert_equal [
|
|
36
|
+
"#{headerstart}\r\n",
|
|
37
|
+
"content-Type: text\r\nX_MAX: 200\r\nContent-Length: 0\r\n\r\n"
|
|
38
|
+
].join, io.string
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_stream_with_nonempty_body_writes_body_and_sets_content_length
|
|
42
|
+
io = StringIO.new
|
|
43
|
+
body = HTTP::Request::Body.new("content")
|
|
44
|
+
headerstart = "GET /test HTTP/1.1"
|
|
45
|
+
writer = build_writer(io: io, body: body, headerstart: headerstart)
|
|
46
|
+
writer.stream
|
|
47
|
+
|
|
48
|
+
assert_equal [
|
|
49
|
+
"#{headerstart}\r\n",
|
|
50
|
+
"Content-Length: 7\r\n\r\n",
|
|
51
|
+
"content"
|
|
52
|
+
].join, io.string
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_stream_when_body_is_not_set_does_not_write_body_or_content_length
|
|
56
|
+
io = StringIO.new
|
|
57
|
+
body = HTTP::Request::Body.new(nil)
|
|
58
|
+
headerstart = "GET /test HTTP/1.1"
|
|
59
|
+
writer = build_writer(io: io, body: body, headerstart: headerstart)
|
|
60
|
+
writer.stream
|
|
61
|
+
|
|
62
|
+
assert_equal "#{headerstart}\r\n\r\n", io.string
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_stream_when_body_is_empty_sets_content_length_zero
|
|
66
|
+
io = StringIO.new
|
|
67
|
+
body = HTTP::Request::Body.new("")
|
|
68
|
+
headerstart = "GET /test HTTP/1.1"
|
|
69
|
+
writer = build_writer(io: io, body: body, headerstart: headerstart)
|
|
70
|
+
writer.stream
|
|
71
|
+
|
|
72
|
+
assert_equal [
|
|
73
|
+
"#{headerstart}\r\n",
|
|
74
|
+
"Content-Length: 0\r\n\r\n"
|
|
75
|
+
].join, io.string
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_stream_when_content_length_header_is_set_keeps_given_value
|
|
79
|
+
io = StringIO.new
|
|
80
|
+
headers = HTTP::Headers.coerce "Content-Length" => "12"
|
|
81
|
+
body = HTTP::Request::Body.new("content")
|
|
82
|
+
headerstart = "GET /test HTTP/1.1"
|
|
83
|
+
writer = build_writer(io: io, body: body, headers: headers, headerstart: headerstart)
|
|
84
|
+
writer.stream
|
|
85
|
+
|
|
86
|
+
assert_equal [
|
|
87
|
+
"#{headerstart}\r\n",
|
|
88
|
+
"Content-Length: 12\r\n\r\n",
|
|
89
|
+
"content"
|
|
90
|
+
].join, io.string
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def test_stream_when_transfer_encoding_is_chunked_writes_encoded_content
|
|
94
|
+
io = StringIO.new
|
|
95
|
+
headers = HTTP::Headers.coerce "Transfer-Encoding" => "chunked"
|
|
96
|
+
body = HTTP::Request::Body.new(%w[request body])
|
|
97
|
+
headerstart = "GET /test HTTP/1.1"
|
|
98
|
+
writer = build_writer(io: io, body: body, headers: headers, headerstart: headerstart)
|
|
99
|
+
writer.stream
|
|
100
|
+
|
|
101
|
+
assert_equal [
|
|
102
|
+
"#{headerstart}\r\n",
|
|
103
|
+
"Transfer-Encoding: chunked\r\n\r\n",
|
|
104
|
+
"7\r\nrequest\r\n4\r\nbody\r\n0\r\n\r\n"
|
|
105
|
+
].join, io.string
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def test_stream_when_transfer_encoding_chunked_with_large_body_encodes_hex
|
|
109
|
+
io = StringIO.new
|
|
110
|
+
headers = HTTP::Headers.coerce "Transfer-Encoding" => "chunked"
|
|
111
|
+
body = HTTP::Request::Body.new(["a" * 255])
|
|
112
|
+
headerstart = "GET /test HTTP/1.1"
|
|
113
|
+
writer = build_writer(io: io, body: body, headers: headers, headerstart: headerstart)
|
|
114
|
+
writer.stream
|
|
115
|
+
|
|
116
|
+
assert_includes io.string, "ff\r\n#{'a' * 255}\r\n"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def test_stream_when_transfer_encoding_is_not_chunked_does_not_treat_as_chunked
|
|
120
|
+
io = StringIO.new
|
|
121
|
+
headers = HTTP::Headers.coerce "Transfer-Encoding" => "gzip"
|
|
122
|
+
body = HTTP::Request::Body.new("content")
|
|
123
|
+
headerstart = "GET /test HTTP/1.1"
|
|
124
|
+
writer = build_writer(io: io, body: body, headers: headers, headerstart: headerstart)
|
|
125
|
+
writer.stream
|
|
126
|
+
|
|
127
|
+
refute_includes io.string, "0\r\n\r\n"
|
|
128
|
+
assert_includes io.string, "content"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def test_stream_when_transfer_encoding_is_not_chunked_returns_false_from_chunked
|
|
132
|
+
headers = HTTP::Headers.coerce "Transfer-Encoding" => "gzip"
|
|
133
|
+
body = HTTP::Request::Body.new("content")
|
|
134
|
+
writer = build_writer(body: body, headers: headers)
|
|
135
|
+
|
|
136
|
+
refute_predicate writer, :chunked?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def test_stream_when_server_wont_accept_data_aborts_silently
|
|
140
|
+
mock_io = Object.new
|
|
141
|
+
mock_io.define_singleton_method(:write) { |*| raise Errno::EPIPE }
|
|
142
|
+
body = HTTP::Request::Body.new("")
|
|
143
|
+
headers = HTTP::Headers.new
|
|
144
|
+
w = HTTP::Request::Writer.new(mock_io, body, headers, "GET /test HTTP/1.1")
|
|
145
|
+
w.stream
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def test_stream_when_body_is_nil_on_post_request_sets_content_length_to_zero
|
|
149
|
+
io = StringIO.new
|
|
150
|
+
body = HTTP::Request::Body.new(nil)
|
|
151
|
+
writer = build_writer(io: io, body: body, headerstart: "POST /test HTTP/1.1")
|
|
152
|
+
writer.stream
|
|
153
|
+
|
|
154
|
+
assert_equal "POST /test HTTP/1.1\r\nContent-Length: 0\r\n\r\n", io.string
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def test_stream_when_body_is_nil_on_head_request_omits_content_length
|
|
158
|
+
io = StringIO.new
|
|
159
|
+
headers = HTTP::Headers.coerce "Host" => "example.org"
|
|
160
|
+
body = HTTP::Request::Body.new(nil)
|
|
161
|
+
writer = build_writer(io: io, body: body, headers: headers, headerstart: "HEAD /test HTTP/1.1")
|
|
162
|
+
writer.stream
|
|
163
|
+
|
|
164
|
+
refute_includes io.string, "Content-Length"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def test_stream_when_body_is_nil_on_delete_request_omits_content_length
|
|
168
|
+
io = StringIO.new
|
|
169
|
+
headers = HTTP::Headers.coerce "Host" => "example.org"
|
|
170
|
+
body = HTTP::Request::Body.new(nil)
|
|
171
|
+
writer = build_writer(io: io, body: body, headers: headers, headerstart: "DELETE /test HTTP/1.1")
|
|
172
|
+
writer.stream
|
|
173
|
+
|
|
174
|
+
refute_includes io.string, "Content-Length"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def test_stream_when_body_is_nil_on_connect_request_omits_content_length
|
|
178
|
+
io = StringIO.new
|
|
179
|
+
headers = HTTP::Headers.coerce "Host" => "example.com:443"
|
|
180
|
+
body = HTTP::Request::Body.new(nil)
|
|
181
|
+
writer = build_writer(io: io, body: body, headers: headers, headerstart: "CONNECT example.com:443 HTTP/1.1")
|
|
182
|
+
writer.stream
|
|
183
|
+
|
|
184
|
+
refute_includes io.string, "Content-Length"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def test_stream_when_socket_raises_exception_raises_connection_error
|
|
188
|
+
mock_io = Object.new
|
|
189
|
+
mock_io.define_singleton_method(:write) { |*| raise Errno::ECONNRESET }
|
|
190
|
+
body = HTTP::Request::Body.new("")
|
|
191
|
+
headers = HTTP::Headers.new
|
|
192
|
+
w = HTTP::Request::Writer.new(mock_io, body, headers, "GET /test HTTP/1.1")
|
|
193
|
+
|
|
194
|
+
assert_raises(HTTP::ConnectionError) { w.stream }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def test_stream_when_socket_raises_exception_includes_original_error_message
|
|
198
|
+
mock_io = Object.new
|
|
199
|
+
mock_io.define_singleton_method(:write) { |*| raise Errno::ECONNRESET }
|
|
200
|
+
body = HTTP::Request::Body.new("")
|
|
201
|
+
headers = HTTP::Headers.new
|
|
202
|
+
w = HTTP::Request::Writer.new(mock_io, body, headers, "GET /test HTTP/1.1")
|
|
203
|
+
err = assert_raises(HTTP::ConnectionError) { w.stream }
|
|
204
|
+
|
|
205
|
+
assert_includes err.message, "error writing to socket:"
|
|
206
|
+
assert_includes err.message, "Connection reset by peer"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def test_stream_when_socket_raises_exception_preserves_original_backtrace
|
|
210
|
+
mock_io = Object.new
|
|
211
|
+
mock_io.define_singleton_method(:write) { |*| raise Errno::ECONNRESET }
|
|
212
|
+
body = HTTP::Request::Body.new("")
|
|
213
|
+
headers = HTTP::Headers.new
|
|
214
|
+
w = HTTP::Request::Writer.new(mock_io, body, headers, "GET /test HTTP/1.1")
|
|
215
|
+
err = assert_raises(HTTP::ConnectionError) { w.stream }
|
|
216
|
+
|
|
217
|
+
assert_includes err.backtrace.first, "writer_test.rb"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def test_stream_when_socket_performs_partial_writes_writes_remaining_data
|
|
221
|
+
written = []
|
|
222
|
+
call_count = 0
|
|
223
|
+
mock_io = Object.new
|
|
224
|
+
mock_io.define_singleton_method(:write) do |data|
|
|
225
|
+
call_count += 1
|
|
226
|
+
bytes = call_count == 1 ? [5, data.bytesize].min : data.bytesize
|
|
227
|
+
written << data.byteslice(0, bytes)
|
|
228
|
+
bytes
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
body = HTTP::Request::Body.new("HelloWorld")
|
|
232
|
+
w = HTTP::Request::Writer.new(mock_io, body, HTTP::Headers.new, "GET /test HTTP/1.1")
|
|
233
|
+
w.stream
|
|
234
|
+
|
|
235
|
+
full_output = written.join
|
|
236
|
+
|
|
237
|
+
assert_includes full_output, "HelloWorld"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# #connect_through_proxy
|
|
241
|
+
|
|
242
|
+
def test_connect_through_proxy_writes_headers_without_body
|
|
243
|
+
io = StringIO.new
|
|
244
|
+
writer = build_writer(io: io)
|
|
245
|
+
writer.connect_through_proxy
|
|
246
|
+
|
|
247
|
+
assert_equal "GET /test HTTP/1.1\r\n\r\n", io.string
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def test_connect_through_proxy_with_headers_includes_headers
|
|
251
|
+
io = StringIO.new
|
|
252
|
+
headers = HTTP::Headers.coerce "Host" => "example.org"
|
|
253
|
+
writer = build_writer(io: io, headers: headers)
|
|
254
|
+
writer.connect_through_proxy
|
|
255
|
+
|
|
256
|
+
assert_equal "GET /test HTTP/1.1\r\nHost: example.org\r\n\r\n", io.string
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def test_connect_through_proxy_when_socket_raises_epipe_propagates_error
|
|
260
|
+
mock_io = Object.new
|
|
261
|
+
mock_io.define_singleton_method(:write) { |*| raise Errno::EPIPE }
|
|
262
|
+
body = HTTP::Request::Body.new("")
|
|
263
|
+
headers = HTTP::Headers.new
|
|
264
|
+
w = HTTP::Request::Writer.new(mock_io, body, headers, "GET /test HTTP/1.1")
|
|
265
|
+
|
|
266
|
+
assert_raises(Errno::EPIPE) { w.connect_through_proxy }
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# #each_chunk
|
|
270
|
+
|
|
271
|
+
def test_each_chunk_when_body_has_content_yields_headers_combined_with_first_chunk
|
|
272
|
+
body = HTTP::Request::Body.new("content")
|
|
273
|
+
writer = build_writer(body: body)
|
|
274
|
+
writer.add_headers
|
|
275
|
+
writer.add_body_type_headers
|
|
276
|
+
chunks = []
|
|
277
|
+
writer.each_chunk { |chunk| chunks << chunk.dup }
|
|
278
|
+
|
|
279
|
+
assert_equal 1, chunks.length
|
|
280
|
+
assert_includes chunks.first, "content"
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def test_each_chunk_when_body_is_empty_yields_headers_only_once
|
|
284
|
+
body = HTTP::Request::Body.new("")
|
|
285
|
+
headerstart = "GET /test HTTP/1.1"
|
|
286
|
+
writer = build_writer(body: body, headerstart: headerstart)
|
|
287
|
+
writer.add_headers
|
|
288
|
+
writer.add_body_type_headers
|
|
289
|
+
chunks = []
|
|
290
|
+
writer.each_chunk { |chunk| chunks << chunk.dup }
|
|
291
|
+
|
|
292
|
+
assert_equal 1, chunks.length
|
|
293
|
+
assert_includes chunks.first, headerstart
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# #add_body_type_headers
|
|
297
|
+
|
|
298
|
+
def test_add_body_type_headers_when_body_is_nil_on_put_sets_content_length_zero
|
|
299
|
+
io = StringIO.new
|
|
300
|
+
body = HTTP::Request::Body.new(nil)
|
|
301
|
+
writer = build_writer(io: io, body: body, headerstart: "PUT /test HTTP/1.1")
|
|
302
|
+
writer.stream
|
|
303
|
+
|
|
304
|
+
assert_includes io.string, "Content-Length: 0"
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def test_add_body_type_headers_when_body_is_nil_on_patch_sets_content_length_zero
|
|
308
|
+
io = StringIO.new
|
|
309
|
+
body = HTTP::Request::Body.new(nil)
|
|
310
|
+
writer = build_writer(io: io, body: body, headerstart: "PATCH /test HTTP/1.1")
|
|
311
|
+
writer.stream
|
|
312
|
+
|
|
313
|
+
assert_includes io.string, "Content-Length: 0"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def test_add_body_type_headers_when_body_is_nil_on_options_sets_content_length_zero
|
|
317
|
+
io = StringIO.new
|
|
318
|
+
body = HTTP::Request::Body.new(nil)
|
|
319
|
+
writer = build_writer(io: io, body: body, headerstart: "OPTIONS /test HTTP/1.1")
|
|
320
|
+
writer.stream
|
|
321
|
+
|
|
322
|
+
assert_includes io.string, "Content-Length: 0"
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# #write (private) partial write handling
|
|
326
|
+
|
|
327
|
+
def test_write_partial_writes_exact_correct_bytes_no_duplication
|
|
328
|
+
written_data = +""
|
|
329
|
+
write_calls = 0
|
|
330
|
+
mock_io = Object.new
|
|
331
|
+
mock_io.define_singleton_method(:write) do |data|
|
|
332
|
+
write_calls += 1
|
|
333
|
+
bytes = [2, data.bytesize].min
|
|
334
|
+
written_data << data.byteslice(0, bytes)
|
|
335
|
+
bytes
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
body = HTTP::Request::Body.new("ABCDEF")
|
|
339
|
+
headerstart = "GET /test HTTP/1.1"
|
|
340
|
+
w = HTTP::Request::Writer.new(mock_io, body, HTTP::Headers.new, headerstart)
|
|
341
|
+
w.stream
|
|
342
|
+
|
|
343
|
+
assert_includes written_data, "ABCDEF"
|
|
344
|
+
body_start = written_data.index("ABCDEF")
|
|
345
|
+
|
|
346
|
+
refute_nil body_start
|
|
347
|
+
assert_nil written_data.index("ABCDEF", body_start + 1)
|
|
348
|
+
assert_operator write_calls, :>, 1
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def test_write_when_socket_writes_all_bytes_at_once_calls_write_once
|
|
352
|
+
write_calls = 0
|
|
353
|
+
mock_io = Object.new
|
|
354
|
+
mock_io.define_singleton_method(:write) do |data|
|
|
355
|
+
write_calls += 1
|
|
356
|
+
data.bytesize
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
body = HTTP::Request::Body.new("Hello")
|
|
360
|
+
w = HTTP::Request::Writer.new(mock_io, body, HTTP::Headers.new, "GET /test HTTP/1.1")
|
|
361
|
+
w.stream
|
|
362
|
+
|
|
363
|
+
assert_equal 1, write_calls
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def test_write_when_data_is_split_across_two_writes_correctly_slices_remaining
|
|
367
|
+
written_chunks = []
|
|
368
|
+
call_count = 0
|
|
369
|
+
mock_io = Object.new
|
|
370
|
+
mock_io.define_singleton_method(:write) do |data|
|
|
371
|
+
call_count += 1
|
|
372
|
+
written_chunks << data.dup
|
|
373
|
+
if call_count == 1
|
|
374
|
+
[5, data.bytesize].min
|
|
375
|
+
else
|
|
376
|
+
data.bytesize
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
body = HTTP::Request::Body.new("TESTDATA123")
|
|
381
|
+
headerstart = "GET /test HTTP/1.1"
|
|
382
|
+
w = HTTP::Request::Writer.new(mock_io, body, HTTP::Headers.new, headerstart)
|
|
383
|
+
w.stream
|
|
384
|
+
|
|
385
|
+
full_output = written_chunks.map { |c| c.byteslice(0, [5, c.bytesize].min) }.first +
|
|
386
|
+
written_chunks[1..].join
|
|
387
|
+
|
|
388
|
+
assert_includes full_output, "TESTDATA123"
|
|
389
|
+
assert_operator written_chunks[1].bytesize, :<, written_chunks[0].bytesize
|
|
390
|
+
end
|
|
391
|
+
end
|