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,502 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class HTTPResponseTest < Minitest::Test
|
|
6
|
+
cover "HTTP::Response*"
|
|
7
|
+
|
|
8
|
+
def build_response(status: 200, version: "1.1", headers: {}, body: "Hello world!", uri: "http://example.com/", **opts)
|
|
9
|
+
request = opts.delete(:request) || HTTP::Request.new(verb: :get, uri: uri)
|
|
10
|
+
HTTP::Response.new(status: status, version: version, headers: headers, body: body, request: request, **opts)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
# #headers
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
def test_provides_a_headers_accessor
|
|
17
|
+
response = build_response
|
|
18
|
+
|
|
19
|
+
assert_kind_of HTTP::Headers, response.headers
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# #to_a
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
def test_to_a_returns_a_rack_like_array
|
|
26
|
+
headers = { "Content-Type" => "text/plain" }
|
|
27
|
+
response = build_response(headers: headers, body: "Hello world")
|
|
28
|
+
|
|
29
|
+
assert_equal [200, headers, "Hello world"], response.to_a
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_to_a_returns_an_integer_status_code
|
|
33
|
+
headers = { "Content-Type" => "text/plain" }
|
|
34
|
+
response = build_response(headers: headers, body: "Hello world")
|
|
35
|
+
|
|
36
|
+
assert_instance_of Integer, response.to_a.fetch(0)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_to_a_returns_a_plain_hash_for_headers
|
|
40
|
+
headers = { "Content-Type" => "text/plain" }
|
|
41
|
+
response = build_response(headers: headers, body: "Hello world")
|
|
42
|
+
result = response.to_a.fetch(1)
|
|
43
|
+
|
|
44
|
+
assert_instance_of Hash, result
|
|
45
|
+
refute_instance_of HTTP::Headers, result
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_to_a_returns_a_string_for_body
|
|
49
|
+
request = HTTP::Request.new(verb: :get, uri: "http://example.com/")
|
|
50
|
+
headers = { "Content-Type" => "text/plain" }
|
|
51
|
+
conn = fake(sequence_id: 0, readpartial: proc { raise EOFError }, body_completed?: true)
|
|
52
|
+
resp = HTTP::Response.new(status: 200, version: "1.1", headers: headers,
|
|
53
|
+
connection: conn, request: request)
|
|
54
|
+
result = resp.to_a.fetch(2)
|
|
55
|
+
|
|
56
|
+
assert_instance_of String, result
|
|
57
|
+
refute_instance_of HTTP::Response::Body, result
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# #deconstruct_keys
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
def test_deconstruct_keys_returns_all_keys_when_given_nil
|
|
64
|
+
response = build_response
|
|
65
|
+
result = response.deconstruct_keys(nil)
|
|
66
|
+
|
|
67
|
+
assert_instance_of HTTP::Response::Status, result[:status]
|
|
68
|
+
assert_equal "1.1", result[:version]
|
|
69
|
+
assert_instance_of HTTP::Headers, result[:headers]
|
|
70
|
+
assert_equal "Hello world!", result[:body]
|
|
71
|
+
assert_equal response.request, result[:request]
|
|
72
|
+
assert_instance_of HTTP::Headers, result[:proxy_headers]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def test_deconstruct_keys_returns_only_requested_keys
|
|
76
|
+
response = build_response
|
|
77
|
+
result = response.deconstruct_keys(%i[status version])
|
|
78
|
+
|
|
79
|
+
assert_equal 2, result.size
|
|
80
|
+
assert_instance_of HTTP::Response::Status, result[:status]
|
|
81
|
+
assert_equal "1.1", result[:version]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_deconstruct_keys_excludes_unrequested_keys
|
|
85
|
+
response = build_response
|
|
86
|
+
result = response.deconstruct_keys([:status])
|
|
87
|
+
|
|
88
|
+
refute_includes result.keys, :version
|
|
89
|
+
refute_includes result.keys, :body
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_deconstruct_keys_returns_empty_hash_for_empty_keys
|
|
93
|
+
response = build_response
|
|
94
|
+
|
|
95
|
+
assert_equal({}, response.deconstruct_keys([]))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_deconstruct_keys_supports_hash_pattern_matching
|
|
99
|
+
response = build_response
|
|
100
|
+
matched = case response
|
|
101
|
+
in { status: HTTP::Response::Status, version: "1.1" }
|
|
102
|
+
true
|
|
103
|
+
else
|
|
104
|
+
false
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
assert matched
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
# #deconstruct
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
def test_deconstruct_returns_a_rack_like_array
|
|
114
|
+
headers = { "Content-Type" => "text/plain" }
|
|
115
|
+
response = build_response(headers: headers, body: "Hello world")
|
|
116
|
+
|
|
117
|
+
assert_equal [200, headers, "Hello world"], response.deconstruct
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def test_deconstruct_supports_array_pattern_matching
|
|
121
|
+
headers = { "Content-Type" => "text/plain" }
|
|
122
|
+
response = build_response(headers: headers, body: "Hello world")
|
|
123
|
+
matched = case response
|
|
124
|
+
in [200, *, String]
|
|
125
|
+
true
|
|
126
|
+
else
|
|
127
|
+
false
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
assert matched
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
# #content_length
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
def test_content_length_without_header_returns_nil
|
|
137
|
+
response = build_response
|
|
138
|
+
|
|
139
|
+
assert_nil response.content_length
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def test_content_length_with_content_length_5_returns_5
|
|
143
|
+
response = build_response(headers: { "Content-Length" => "5" })
|
|
144
|
+
|
|
145
|
+
assert_equal 5, response.content_length
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def test_content_length_with_invalid_content_length_returns_nil
|
|
149
|
+
response = build_response(headers: { "Content-Length" => "foo" })
|
|
150
|
+
|
|
151
|
+
assert_nil response.content_length
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def test_content_length_with_duplicate_identical_returns_deduplicated_value
|
|
155
|
+
h = HTTP::Headers.new
|
|
156
|
+
h.add("Content-Length", "5")
|
|
157
|
+
h.add("Content-Length", "5")
|
|
158
|
+
response = build_response(headers: h)
|
|
159
|
+
|
|
160
|
+
assert_equal 5, response.content_length
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def test_content_length_with_conflicting_values_returns_nil
|
|
164
|
+
h = HTTP::Headers.new
|
|
165
|
+
h.add("Content-Length", "5")
|
|
166
|
+
h.add("Content-Length", "10")
|
|
167
|
+
response = build_response(headers: h)
|
|
168
|
+
|
|
169
|
+
assert_nil response.content_length
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def test_content_length_with_transfer_encoding_header_returns_nil
|
|
173
|
+
response = build_response(headers: { "Transfer-Encoding" => "chunked", "Content-Length" => "5" })
|
|
174
|
+
|
|
175
|
+
assert_nil response.content_length
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
# #mime_type
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
def test_mime_type_without_content_type_returns_nil
|
|
182
|
+
response = build_response(headers: {})
|
|
183
|
+
|
|
184
|
+
assert_nil response.mime_type
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def test_mime_type_with_text_html_returns_text_html
|
|
188
|
+
response = build_response(headers: { "Content-Type" => "text/html" })
|
|
189
|
+
|
|
190
|
+
assert_equal "text/html", response.mime_type
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def test_mime_type_with_charset_returns_mime_type_only
|
|
194
|
+
response = build_response(headers: { "Content-Type" => "text/html; charset=utf-8" })
|
|
195
|
+
|
|
196
|
+
assert_equal "text/html", response.mime_type
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# #charset
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
def test_charset_without_content_type_returns_nil
|
|
203
|
+
response = build_response(headers: {})
|
|
204
|
+
|
|
205
|
+
assert_nil response.charset
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def test_charset_with_text_html_no_charset_returns_nil
|
|
209
|
+
response = build_response(headers: { "Content-Type" => "text/html" })
|
|
210
|
+
|
|
211
|
+
assert_nil response.charset
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def test_charset_with_charset_utf8_returns_utf8
|
|
215
|
+
response = build_response(headers: { "Content-Type" => "text/html; charset=utf-8" })
|
|
216
|
+
|
|
217
|
+
assert_equal "utf-8", response.charset
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
# #parse
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
def test_parse_with_known_content_type_returns_parsed_body
|
|
224
|
+
response = build_response(headers: { "Content-Type" => "application/json" }, body: '{"foo":"100%s"}')
|
|
225
|
+
|
|
226
|
+
assert_equal({ "foo" => "100%s" }, response.parse)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def test_parse_with_unknown_content_type_raises_parse_error
|
|
230
|
+
response = build_response(headers: { "Content-Type" => "application/deadbeef" }, body: '{"foo":"100%s"}')
|
|
231
|
+
|
|
232
|
+
assert_raises(HTTP::ParseError) { response.parse }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def test_parse_with_explicit_mime_type_ignores_response_mime_type
|
|
236
|
+
response = build_response(headers: { "Content-Type" => "application/deadbeef" }, body: '{"foo":"100%s"}')
|
|
237
|
+
|
|
238
|
+
assert_equal({ "foo" => "100%s" }, response.parse("application/json"))
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def test_parse_supports_mime_type_aliases
|
|
242
|
+
response = build_response(headers: { "Content-Type" => "application/deadbeef" }, body: '{"foo":"100%s"}')
|
|
243
|
+
|
|
244
|
+
assert_equal({ "foo" => "100%s" }, response.parse(:json))
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def test_parse_when_underlying_parser_fails_raises_parse_error
|
|
248
|
+
response = build_response(headers: { "Content-Type" => "application/deadbeef" }, body: "")
|
|
249
|
+
|
|
250
|
+
assert_raises(HTTP::ParseError) { response.parse }
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def test_parse_when_underlying_parser_fails_preserves_original_error_message
|
|
254
|
+
response = build_response(headers: { "Content-Type" => "application/deadbeef" }, body: "")
|
|
255
|
+
err = assert_raises(HTTP::ParseError) { response.parse }
|
|
256
|
+
|
|
257
|
+
assert_includes err.message, "application/deadbeef"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
# #flush
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
def test_flush_returns_response_self_reference
|
|
264
|
+
request = HTTP::Request.new(verb: :get, uri: "http://example.com/")
|
|
265
|
+
mock_body = fake(to_s: "")
|
|
266
|
+
resp = HTTP::Response.new(status: 200, version: "1.1", body: mock_body, request: request)
|
|
267
|
+
|
|
268
|
+
assert_same resp, resp.flush
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def test_flush_flushes_body
|
|
272
|
+
request = HTTP::Request.new(verb: :get, uri: "http://example.com/")
|
|
273
|
+
to_s_called = false
|
|
274
|
+
mock_body = Object.new
|
|
275
|
+
mock_body.define_singleton_method(:to_s) do
|
|
276
|
+
to_s_called = true
|
|
277
|
+
""
|
|
278
|
+
end
|
|
279
|
+
resp = HTTP::Response.new(status: 200, version: "1.1", body: mock_body, request: request)
|
|
280
|
+
resp.flush
|
|
281
|
+
|
|
282
|
+
assert to_s_called, "expected body.to_s to be called"
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# ---------------------------------------------------------------------------
|
|
286
|
+
# #inspect
|
|
287
|
+
# ---------------------------------------------------------------------------
|
|
288
|
+
def test_inspect_returns_useful_string_representation
|
|
289
|
+
response = build_response(headers: { content_type: "text/plain" }, body: fake(to_s: "foobar"))
|
|
290
|
+
|
|
291
|
+
assert_equal "#<HTTP::Response/1.1 200 OK text/plain>", response.inspect
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# ---------------------------------------------------------------------------
|
|
295
|
+
# #cookies
|
|
296
|
+
# ---------------------------------------------------------------------------
|
|
297
|
+
def test_cookies_returns_an_array_of_http_cookie
|
|
298
|
+
cookies = ["a=1", "b=2; domain=example.com", "c=3; domain=bad.org"]
|
|
299
|
+
response = build_response(headers: { "Set-Cookie" => cookies })
|
|
300
|
+
cookie_list = response.cookies
|
|
301
|
+
|
|
302
|
+
assert_kind_of Array, cookie_list
|
|
303
|
+
cookie_list.each { |c| assert_kind_of HTTP::Cookie, c }
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def test_cookies_contains_cookies_without_domain_restriction
|
|
307
|
+
cookies = ["a=1", "b=2; domain=example.com", "c=3; domain=bad.org"]
|
|
308
|
+
response = build_response(headers: { "Set-Cookie" => cookies })
|
|
309
|
+
cookie_list = response.cookies
|
|
310
|
+
|
|
311
|
+
assert_equal(1, cookie_list.count { |c| "a" == c.name })
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def test_cookies_contains_cookies_limited_to_domain_of_request_uri
|
|
315
|
+
cookies = ["a=1", "b=2; domain=example.com", "c=3; domain=bad.org"]
|
|
316
|
+
response = build_response(headers: { "Set-Cookie" => cookies })
|
|
317
|
+
cookie_list = response.cookies
|
|
318
|
+
|
|
319
|
+
assert_equal(1, cookie_list.count { |c| "b" == c.name })
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def test_cookies_does_not_contain_cookies_limited_to_non_requested_uri
|
|
323
|
+
cookies = ["a=1", "b=2; domain=example.com", "c=3; domain=bad.org"]
|
|
324
|
+
response = build_response(headers: { "Set-Cookie" => cookies })
|
|
325
|
+
cookie_list = response.cookies
|
|
326
|
+
|
|
327
|
+
assert_equal(0, cookie_list.count { |c| "c" == c.name })
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# ---------------------------------------------------------------------------
|
|
331
|
+
# #connection
|
|
332
|
+
# ---------------------------------------------------------------------------
|
|
333
|
+
def test_connection_returns_connection_object
|
|
334
|
+
request = HTTP::Request.new(verb: :get, uri: "http://example.com/")
|
|
335
|
+
connection = fake
|
|
336
|
+
response = HTTP::Response.new(
|
|
337
|
+
version: "1.1",
|
|
338
|
+
status: 200,
|
|
339
|
+
connection: connection,
|
|
340
|
+
request: request
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
assert_equal connection, response.connection
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# ---------------------------------------------------------------------------
|
|
347
|
+
# #chunked?
|
|
348
|
+
# ---------------------------------------------------------------------------
|
|
349
|
+
def test_chunked_returns_true_when_encoding_is_chunked
|
|
350
|
+
response = build_response(headers: { "Transfer-Encoding" => "chunked" })
|
|
351
|
+
|
|
352
|
+
assert_predicate response, :chunked?
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def test_chunked_returns_false_by_default
|
|
356
|
+
response = build_response
|
|
357
|
+
|
|
358
|
+
refute_predicate response, :chunked?
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# ---------------------------------------------------------------------------
|
|
362
|
+
# backwards compatibility with :uri
|
|
363
|
+
# ---------------------------------------------------------------------------
|
|
364
|
+
def test_backwards_compat_with_uri_defaults_uri
|
|
365
|
+
response = HTTP::Response.new(
|
|
366
|
+
status: 200,
|
|
367
|
+
version: "1.1",
|
|
368
|
+
headers: {},
|
|
369
|
+
body: "Hello world!",
|
|
370
|
+
uri: "http://example.com/"
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
assert_equal "http://example.com/", response.request.uri.to_s
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def test_backwards_compat_with_uri_defaults_verb_to_get
|
|
377
|
+
response = HTTP::Response.new(
|
|
378
|
+
status: 200,
|
|
379
|
+
version: "1.1",
|
|
380
|
+
headers: {},
|
|
381
|
+
body: "Hello world!",
|
|
382
|
+
uri: "http://example.com/"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
assert_equal :get, response.request.verb
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def test_backwards_compat_with_both_request_and_uri_raises_argument_error
|
|
389
|
+
request = HTTP::Request.new(verb: :get, uri: "http://example.com/")
|
|
390
|
+
err = assert_raises(ArgumentError) do
|
|
391
|
+
HTTP::Response.new(
|
|
392
|
+
status: 200,
|
|
393
|
+
version: "1.1",
|
|
394
|
+
headers: {},
|
|
395
|
+
body: "Hello world!",
|
|
396
|
+
uri: "http://example.com/",
|
|
397
|
+
request: request
|
|
398
|
+
)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
assert_includes err.message, ":uri"
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# ---------------------------------------------------------------------------
|
|
405
|
+
# #body encoding
|
|
406
|
+
# ---------------------------------------------------------------------------
|
|
407
|
+
def test_body_with_no_content_type_returns_binary_encoding
|
|
408
|
+
request = HTTP::Request.new(verb: :get, uri: "http://example.com/")
|
|
409
|
+
chunks = ["Hello, ", "World!"]
|
|
410
|
+
connection = fake(sequence_id: 0, readpartial: proc { chunks.shift || raise(EOFError) }, body_completed?: proc {
|
|
411
|
+
chunks.empty?
|
|
412
|
+
})
|
|
413
|
+
response = HTTP::Response.new(
|
|
414
|
+
status: 200, version: "1.1", headers: {},
|
|
415
|
+
request: request, connection: connection
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
assert_equal Encoding::BINARY, response.body.to_s.encoding
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def test_body_with_application_json_returns_utf8_encoding
|
|
422
|
+
request = HTTP::Request.new(verb: :get, uri: "http://example.com/")
|
|
423
|
+
chunks = ["Hello, ", "World!"]
|
|
424
|
+
connection = fake(sequence_id: 0, readpartial: proc { chunks.shift || raise(EOFError) }, body_completed?: proc {
|
|
425
|
+
chunks.empty?
|
|
426
|
+
})
|
|
427
|
+
response = HTTP::Response.new(
|
|
428
|
+
status: 200, version: "1.1", headers: { "Content-Type" => "application/json" },
|
|
429
|
+
request: request, connection: connection
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
assert_equal Encoding::UTF_8, response.body.to_s.encoding
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def test_body_with_text_html_returns_binary_encoding
|
|
436
|
+
request = HTTP::Request.new(verb: :get, uri: "http://example.com/")
|
|
437
|
+
chunks = ["Hello, ", "World!"]
|
|
438
|
+
connection = fake(sequence_id: 0, readpartial: proc { chunks.shift || raise(EOFError) }, body_completed?: proc {
|
|
439
|
+
chunks.empty?
|
|
440
|
+
})
|
|
441
|
+
response = HTTP::Response.new(
|
|
442
|
+
status: 200, version: "1.1", headers: { "Content-Type" => "text/html" },
|
|
443
|
+
request: request, connection: connection
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
assert_equal Encoding::BINARY, response.body.to_s.encoding
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def test_body_with_charset_utf8_uses_charset_for_encoding
|
|
450
|
+
request = HTTP::Request.new(verb: :get, uri: "http://example.com/")
|
|
451
|
+
chunks = ["Hello, ", "World!"]
|
|
452
|
+
connection = fake(sequence_id: 0, readpartial: proc { chunks.shift || raise(EOFError) }, body_completed?: proc {
|
|
453
|
+
chunks.empty?
|
|
454
|
+
})
|
|
455
|
+
response = HTTP::Response.new(
|
|
456
|
+
status: 200, version: "1.1", headers: { "Content-Type" => "text/html; charset=utf-8" },
|
|
457
|
+
request: request, connection: connection
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
assert_equal Encoding::UTF_8, response.body.to_s.encoding
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def test_body_with_explicit_encoding_passes_encoding_to_body
|
|
464
|
+
request = HTTP::Request.new(verb: :get, uri: "http://example.com/")
|
|
465
|
+
chunks = ["Hello, ", "World!"]
|
|
466
|
+
conn = fake(sequence_id: 0, readpartial: proc { chunks.shift || raise(EOFError) },
|
|
467
|
+
body_completed?: proc { chunks.empty? })
|
|
468
|
+
resp = HTTP::Response.new(
|
|
469
|
+
status: 200, version: "1.1", headers: {},
|
|
470
|
+
request: request, connection: conn, encoding: "UTF-8"
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
assert_equal Encoding::UTF_8, resp.body.to_s.encoding
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# ---------------------------------------------------------------------------
|
|
477
|
+
# #initialize defaults
|
|
478
|
+
# ---------------------------------------------------------------------------
|
|
479
|
+
def test_initialize_defaults_headers_to_empty
|
|
480
|
+
request = HTTP::Request.new(verb: :get, uri: "http://example.com/")
|
|
481
|
+
resp = HTTP::Response.new(status: 200, version: "1.1", body: "ok", request: request)
|
|
482
|
+
|
|
483
|
+
assert_empty resp.headers
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def test_initialize_defaults_proxy_headers_to_empty
|
|
487
|
+
request = HTTP::Request.new(verb: :get, uri: "http://example.com/")
|
|
488
|
+
resp = HTTP::Response.new(status: 200, version: "1.1", body: "ok", request: request)
|
|
489
|
+
|
|
490
|
+
assert_empty resp.proxy_headers
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def test_initialize_passes_proxy_headers_through_to_accessor
|
|
494
|
+
request = HTTP::Request.new(verb: :get, uri: "http://example.com/")
|
|
495
|
+
resp = HTTP::Response.new(
|
|
496
|
+
status: 200, version: "1.1", body: "ok", request: request,
|
|
497
|
+
proxy_headers: { "Via" => "1.1 proxy" }
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
assert_equal "1.1 proxy", resp.proxy_headers["Via"]
|
|
501
|
+
end
|
|
502
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class HTTPRetriableDelayCalculatorTest < Minitest::Test
|
|
6
|
+
cover "HTTP::Retriable::DelayCalculator*"
|
|
7
|
+
|
|
8
|
+
def response
|
|
9
|
+
@response ||= HTTP::Response.new(
|
|
10
|
+
status: 200,
|
|
11
|
+
version: "1.1",
|
|
12
|
+
headers: {},
|
|
13
|
+
body: "Hello world!",
|
|
14
|
+
request: HTTP::Request.new(verb: :get, uri: "http://example.com")
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call_delay(iterations, response: self.response, **)
|
|
19
|
+
HTTP::Retriable::DelayCalculator.new(**).call(iterations, response)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call_retry_header(value, **)
|
|
23
|
+
response.headers["Retry-After"] = value
|
|
24
|
+
HTTP::Retriable::DelayCalculator.new(**).call(rand(1...100), response)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_prevents_negative_sleep_time
|
|
28
|
+
assert_equal 0, call_delay(20, delay: -20)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_backs_off_exponentially
|
|
32
|
+
val1 = call_delay(1)
|
|
33
|
+
|
|
34
|
+
assert_operator val1, :>=, 0
|
|
35
|
+
assert_operator val1, :<=, 1
|
|
36
|
+
|
|
37
|
+
val2 = call_delay(2)
|
|
38
|
+
|
|
39
|
+
assert_operator val2, :>=, 1
|
|
40
|
+
assert_operator val2, :<=, 2
|
|
41
|
+
|
|
42
|
+
val3 = call_delay(3)
|
|
43
|
+
|
|
44
|
+
assert_operator val3, :>=, 3
|
|
45
|
+
assert_operator val3, :<=, 4
|
|
46
|
+
|
|
47
|
+
val4 = call_delay(4)
|
|
48
|
+
|
|
49
|
+
assert_operator val4, :>=, 7
|
|
50
|
+
assert_operator val4, :<=, 8
|
|
51
|
+
|
|
52
|
+
val5 = call_delay(5)
|
|
53
|
+
|
|
54
|
+
assert_operator val5, :>=, 15
|
|
55
|
+
assert_operator val5, :<=, 16
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_includes_jitter_in_exponential_backoff
|
|
59
|
+
results = Array.new(10) { call_delay(3) }
|
|
60
|
+
|
|
61
|
+
assert results.any? { |v| v > 3 }, "expected at least one value with jitter above base delay of 3"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_always_returns_a_float
|
|
65
|
+
assert_instance_of Float, call_delay(1, delay: 2)
|
|
66
|
+
assert_instance_of Float, call_delay(1)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_can_have_a_maximum_wait_time
|
|
70
|
+
val1 = call_delay(1, max_delay: 5)
|
|
71
|
+
|
|
72
|
+
assert_operator val1, :>=, 0
|
|
73
|
+
assert_operator val1, :<=, 1
|
|
74
|
+
assert_equal 5, call_delay(5, max_delay: 5)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_caps_delay_at_max_delay
|
|
78
|
+
assert_in_delta(5.0, call_delay(10, max_delay: 5, delay: 100))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_converts_max_delay_to_float
|
|
82
|
+
calc = HTTP::Retriable::DelayCalculator.new(max_delay: 10)
|
|
83
|
+
|
|
84
|
+
assert_instance_of Float, calc.instance_variable_get(:@max_delay)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# -- with a delay proc --
|
|
88
|
+
|
|
89
|
+
def test_with_delay_proc_calls_the_proc_with_iteration_number
|
|
90
|
+
received_iteration = nil
|
|
91
|
+
delay_proc = proc do |iteration|
|
|
92
|
+
received_iteration = iteration
|
|
93
|
+
iteration * 2
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
result = call_delay(3, delay: delay_proc)
|
|
97
|
+
|
|
98
|
+
assert_equal 3, received_iteration
|
|
99
|
+
assert_in_delta(6.0, result)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def test_with_delay_proc_uses_proc_return_value_as_delay
|
|
103
|
+
delay_proc = ->(i) { i * 10 }
|
|
104
|
+
|
|
105
|
+
assert_in_delta(10.0, call_delay(1, delay: delay_proc))
|
|
106
|
+
assert_in_delta(50.0, call_delay(5, delay: delay_proc))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_with_delay_proc_clamps_return_value_to_max_delay
|
|
110
|
+
delay_proc = ->(_i) { 100 }
|
|
111
|
+
|
|
112
|
+
assert_in_delta(5.0, call_delay(1, delay: delay_proc, max_delay: 5))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# -- with a nil response --
|
|
116
|
+
|
|
117
|
+
def test_with_nil_response_falls_back_to_iteration_based_delay
|
|
118
|
+
result = call_delay(1, response: nil)
|
|
119
|
+
|
|
120
|
+
assert_operator result, :>=, 0
|
|
121
|
+
assert_operator result, :<=, 1
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def test_with_nil_response_uses_fixed_delay_when_provided
|
|
125
|
+
assert_in_delta(2.0, call_delay(1, delay: 2, response: nil))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# -- Retry-After headers --
|
|
129
|
+
|
|
130
|
+
def test_respects_retry_after_headers_as_integer
|
|
131
|
+
delay_time = rand(6...2500)
|
|
132
|
+
header_value = delay_time.to_s
|
|
133
|
+
|
|
134
|
+
assert_equal delay_time, call_retry_header(header_value)
|
|
135
|
+
assert_equal 5, call_retry_header(header_value, max_delay: 5)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def test_respects_retry_after_headers_as_integer_with_whitespace
|
|
139
|
+
assert_equal 42, call_retry_header(" 42 ")
|
|
140
|
+
assert_equal 10, call_retry_header("10\t")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def test_respects_retry_after_headers_as_rfc2822_timestamp
|
|
144
|
+
delay_time = rand(6...2500)
|
|
145
|
+
header_value = (Time.now.gmtime + delay_time).to_datetime.rfc2822.sub("+0000", "GMT")
|
|
146
|
+
|
|
147
|
+
assert_in_delta delay_time, call_retry_header(header_value), 1
|
|
148
|
+
assert_equal 5, call_retry_header(header_value, max_delay: 5)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def test_respects_retry_after_headers_as_rfc2822_timestamp_in_the_past
|
|
152
|
+
delay_time = rand(6...2500)
|
|
153
|
+
header_value = (Time.now.gmtime - delay_time).to_datetime.rfc2822.sub("+0000", "GMT")
|
|
154
|
+
|
|
155
|
+
assert_equal 0, call_retry_header(header_value)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def test_handles_non_string_retry_after_header_values
|
|
159
|
+
response.headers["Retry-After"] = 42
|
|
160
|
+
calc = HTTP::Retriable::DelayCalculator.new
|
|
161
|
+
result = calc.call(1, response)
|
|
162
|
+
|
|
163
|
+
assert_in_delta(42.0, result)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def test_does_not_error_on_invalid_retry_after_header
|
|
167
|
+
[
|
|
168
|
+
"This is a string with a number 5 in it",
|
|
169
|
+
"8 Eight is the first digit in this string",
|
|
170
|
+
"This is a string with a #{Time.now.gmtime.to_datetime.rfc2822} timestamp in it"
|
|
171
|
+
].each do |header_value|
|
|
172
|
+
assert_equal 0, call_retry_header(header_value)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def test_returns_zero_for_invalid_retry_after_header
|
|
177
|
+
calc = HTTP::Retriable::DelayCalculator.new
|
|
178
|
+
result = calc.delay_from_retry_header("invalid-value")
|
|
179
|
+
|
|
180
|
+
assert_equal 0, result
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def test_coerces_non_string_retry_after_values_via_to_s
|
|
184
|
+
calc = HTTP::Retriable::DelayCalculator.new
|
|
185
|
+
|
|
186
|
+
assert_in_delta(42.0, calc.delay_from_retry_header(42))
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def test_parses_integer_retry_after_with_embedded_newline_via_to_i
|
|
190
|
+
calc = HTTP::Retriable::DelayCalculator.new
|
|
191
|
+
|
|
192
|
+
assert_in_delta(5.0, calc.delay_from_retry_header("5\nfoo"))
|
|
193
|
+
end
|
|
194
|
+
end
|