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.
Files changed (201) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +241 -41
  3. data/LICENSE.txt +1 -1
  4. data/README.md +110 -13
  5. data/UPGRADING.md +491 -0
  6. data/http.gemspec +32 -29
  7. data/lib/http/base64.rb +11 -1
  8. data/lib/http/chainable/helpers.rb +62 -0
  9. data/lib/http/chainable/verbs.rb +136 -0
  10. data/lib/http/chainable.rb +232 -136
  11. data/lib/http/client.rb +158 -127
  12. data/lib/http/connection/internals.rb +141 -0
  13. data/lib/http/connection.rb +126 -97
  14. data/lib/http/content_type.rb +61 -6
  15. data/lib/http/errors.rb +25 -1
  16. data/lib/http/feature.rb +65 -5
  17. data/lib/http/features/auto_deflate.rb +124 -17
  18. data/lib/http/features/auto_inflate.rb +38 -15
  19. data/lib/http/features/caching/entry.rb +178 -0
  20. data/lib/http/features/caching/in_memory_store.rb +63 -0
  21. data/lib/http/features/caching.rb +216 -0
  22. data/lib/http/features/digest_auth.rb +234 -0
  23. data/lib/http/features/instrumentation.rb +97 -17
  24. data/lib/http/features/logging.rb +183 -5
  25. data/lib/http/features/normalize_uri.rb +17 -0
  26. data/lib/http/features/raise_error.rb +18 -3
  27. data/lib/http/form_data/composite_io.rb +106 -0
  28. data/lib/http/form_data/file.rb +95 -0
  29. data/lib/http/form_data/multipart/param.rb +62 -0
  30. data/lib/http/form_data/multipart.rb +106 -0
  31. data/lib/http/form_data/part.rb +52 -0
  32. data/lib/http/form_data/readable.rb +58 -0
  33. data/lib/http/form_data/urlencoded.rb +175 -0
  34. data/lib/http/form_data/version.rb +8 -0
  35. data/lib/http/form_data.rb +102 -0
  36. data/lib/http/headers/known.rb +3 -0
  37. data/lib/http/headers/normalizer.rb +17 -36
  38. data/lib/http/headers.rb +172 -65
  39. data/lib/http/mime_type/adapter.rb +24 -9
  40. data/lib/http/mime_type/json.rb +19 -4
  41. data/lib/http/mime_type.rb +21 -3
  42. data/lib/http/options/definitions.rb +189 -0
  43. data/lib/http/options.rb +172 -125
  44. data/lib/http/redirector.rb +80 -75
  45. data/lib/http/request/body.rb +87 -6
  46. data/lib/http/request/builder.rb +184 -0
  47. data/lib/http/request/proxy.rb +83 -0
  48. data/lib/http/request/writer.rb +76 -16
  49. data/lib/http/request.rb +214 -98
  50. data/lib/http/response/body.rb +103 -18
  51. data/lib/http/response/inflater.rb +35 -7
  52. data/lib/http/response/parser.rb +98 -4
  53. data/lib/http/response/status/reasons.rb +2 -4
  54. data/lib/http/response/status.rb +141 -31
  55. data/lib/http/response.rb +219 -61
  56. data/lib/http/retriable/delay_calculator.rb +38 -11
  57. data/lib/http/retriable/errors.rb +21 -0
  58. data/lib/http/retriable/performer.rb +82 -38
  59. data/lib/http/session.rb +280 -0
  60. data/lib/http/timeout/global.rb +147 -34
  61. data/lib/http/timeout/null.rb +155 -9
  62. data/lib/http/timeout/per_operation.rb +139 -18
  63. data/lib/http/uri/normalizer.rb +82 -0
  64. data/lib/http/uri/parsing.rb +182 -0
  65. data/lib/http/uri.rb +289 -124
  66. data/lib/http/version.rb +2 -1
  67. data/lib/http.rb +11 -2
  68. data/sig/deps.rbs +122 -0
  69. data/sig/http.rbs +1619 -0
  70. data/test/http/base64_test.rb +28 -0
  71. data/test/http/client_test.rb +739 -0
  72. data/test/http/connection_test.rb +1533 -0
  73. data/test/http/content_type_test.rb +190 -0
  74. data/test/http/errors_test.rb +28 -0
  75. data/test/http/feature_test.rb +49 -0
  76. data/test/http/features/auto_deflate_test.rb +317 -0
  77. data/test/http/features/auto_inflate_test.rb +213 -0
  78. data/test/http/features/caching_test.rb +942 -0
  79. data/test/http/features/digest_auth_test.rb +996 -0
  80. data/test/http/features/instrumentation_test.rb +246 -0
  81. data/test/http/features/logging_test.rb +654 -0
  82. data/test/http/features/normalize_uri_test.rb +41 -0
  83. data/test/http/features/raise_error_test.rb +77 -0
  84. data/test/http/form_data/composite_io_test.rb +215 -0
  85. data/test/http/form_data/file_test.rb +255 -0
  86. data/test/http/form_data/fixtures/the-http-gem.info +1 -0
  87. data/test/http/form_data/multipart_test.rb +303 -0
  88. data/test/http/form_data/part_test.rb +90 -0
  89. data/test/http/form_data/urlencoded_test.rb +164 -0
  90. data/test/http/form_data_test.rb +232 -0
  91. data/test/http/headers/normalizer_test.rb +93 -0
  92. data/test/http/headers_test.rb +888 -0
  93. data/test/http/mime_type/json_test.rb +39 -0
  94. data/test/http/mime_type_test.rb +150 -0
  95. data/test/http/options/base_uri_test.rb +148 -0
  96. data/test/http/options/body_test.rb +21 -0
  97. data/test/http/options/features_test.rb +38 -0
  98. data/test/http/options/form_test.rb +21 -0
  99. data/test/http/options/headers_test.rb +32 -0
  100. data/test/http/options/json_test.rb +21 -0
  101. data/test/http/options/merge_test.rb +78 -0
  102. data/test/http/options/new_test.rb +37 -0
  103. data/test/http/options/proxy_test.rb +32 -0
  104. data/test/http/options_test.rb +575 -0
  105. data/test/http/redirector_test.rb +639 -0
  106. data/test/http/request/body_test.rb +318 -0
  107. data/test/http/request/builder_test.rb +623 -0
  108. data/test/http/request/writer_test.rb +391 -0
  109. data/test/http/request_test.rb +1733 -0
  110. data/test/http/response/body_test.rb +292 -0
  111. data/test/http/response/parser_test.rb +105 -0
  112. data/test/http/response/status_test.rb +322 -0
  113. data/test/http/response_test.rb +502 -0
  114. data/test/http/retriable/delay_calculator_test.rb +194 -0
  115. data/test/http/retriable/errors_test.rb +71 -0
  116. data/test/http/retriable/performer_test.rb +551 -0
  117. data/test/http/session_test.rb +424 -0
  118. data/test/http/timeout/global_test.rb +239 -0
  119. data/test/http/timeout/null_test.rb +218 -0
  120. data/test/http/timeout/per_operation_test.rb +220 -0
  121. data/test/http/uri/normalizer_test.rb +89 -0
  122. data/test/http/uri_test.rb +1140 -0
  123. data/test/http/version_test.rb +15 -0
  124. data/test/http_test.rb +818 -0
  125. data/test/regression_tests.rb +27 -0
  126. data/test/support/dummy_server/encoding_routes.rb +47 -0
  127. data/test/support/dummy_server/routes.rb +201 -0
  128. data/test/support/dummy_server/servlet.rb +81 -0
  129. data/test/support/dummy_server.rb +200 -0
  130. data/{spec → test}/support/fakeio.rb +2 -2
  131. data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
  132. data/test/support/http_handling_shared/timeout_tests.rb +134 -0
  133. data/test/support/http_handling_shared.rb +11 -0
  134. data/test/support/proxy_server.rb +207 -0
  135. data/test/support/servers/runner.rb +67 -0
  136. data/{spec → test}/support/simplecov.rb +11 -2
  137. data/test/support/ssl_helper.rb +108 -0
  138. data/test/test_helper.rb +38 -0
  139. metadata +108 -168
  140. data/.github/workflows/ci.yml +0 -67
  141. data/.gitignore +0 -15
  142. data/.rspec +0 -1
  143. data/.rubocop/layout.yml +0 -8
  144. data/.rubocop/metrics.yml +0 -4
  145. data/.rubocop/rspec.yml +0 -9
  146. data/.rubocop/style.yml +0 -32
  147. data/.rubocop.yml +0 -11
  148. data/.rubocop_todo.yml +0 -219
  149. data/.yardopts +0 -2
  150. data/CHANGES_OLD.md +0 -1002
  151. data/Gemfile +0 -51
  152. data/Guardfile +0 -18
  153. data/Rakefile +0 -64
  154. data/lib/http/headers/mixin.rb +0 -34
  155. data/lib/http/retriable/client.rb +0 -37
  156. data/logo.png +0 -0
  157. data/spec/lib/http/client_spec.rb +0 -556
  158. data/spec/lib/http/connection_spec.rb +0 -88
  159. data/spec/lib/http/content_type_spec.rb +0 -47
  160. data/spec/lib/http/features/auto_deflate_spec.rb +0 -77
  161. data/spec/lib/http/features/auto_inflate_spec.rb +0 -86
  162. data/spec/lib/http/features/instrumentation_spec.rb +0 -81
  163. data/spec/lib/http/features/logging_spec.rb +0 -65
  164. data/spec/lib/http/features/raise_error_spec.rb +0 -62
  165. data/spec/lib/http/headers/mixin_spec.rb +0 -36
  166. data/spec/lib/http/headers/normalizer_spec.rb +0 -52
  167. data/spec/lib/http/headers_spec.rb +0 -527
  168. data/spec/lib/http/options/body_spec.rb +0 -15
  169. data/spec/lib/http/options/features_spec.rb +0 -33
  170. data/spec/lib/http/options/form_spec.rb +0 -15
  171. data/spec/lib/http/options/headers_spec.rb +0 -24
  172. data/spec/lib/http/options/json_spec.rb +0 -15
  173. data/spec/lib/http/options/merge_spec.rb +0 -68
  174. data/spec/lib/http/options/new_spec.rb +0 -30
  175. data/spec/lib/http/options/proxy_spec.rb +0 -20
  176. data/spec/lib/http/options_spec.rb +0 -13
  177. data/spec/lib/http/redirector_spec.rb +0 -530
  178. data/spec/lib/http/request/body_spec.rb +0 -211
  179. data/spec/lib/http/request/writer_spec.rb +0 -121
  180. data/spec/lib/http/request_spec.rb +0 -234
  181. data/spec/lib/http/response/body_spec.rb +0 -85
  182. data/spec/lib/http/response/parser_spec.rb +0 -74
  183. data/spec/lib/http/response/status_spec.rb +0 -253
  184. data/spec/lib/http/response_spec.rb +0 -262
  185. data/spec/lib/http/retriable/delay_calculator_spec.rb +0 -69
  186. data/spec/lib/http/retriable/performer_spec.rb +0 -302
  187. data/spec/lib/http/uri/normalizer_spec.rb +0 -95
  188. data/spec/lib/http/uri_spec.rb +0 -71
  189. data/spec/lib/http_spec.rb +0 -535
  190. data/spec/regression_specs.rb +0 -24
  191. data/spec/spec_helper.rb +0 -89
  192. data/spec/support/black_hole.rb +0 -13
  193. data/spec/support/dummy_server/servlet.rb +0 -203
  194. data/spec/support/dummy_server.rb +0 -44
  195. data/spec/support/fuubar.rb +0 -21
  196. data/spec/support/http_handling_shared.rb +0 -190
  197. data/spec/support/proxy_server.rb +0 -39
  198. data/spec/support/servers/config.rb +0 -11
  199. data/spec/support/servers/runner.rb +0 -19
  200. data/spec/support/ssl_helper.rb +0 -104
  201. /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