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,639 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class HTTPRedirectorTest < Minitest::Test
6
+ cover "HTTP::Redirector*"
7
+
8
+ def simple_response(status, body = "", headers = {})
9
+ HTTP::Response.new(
10
+ status: status,
11
+ version: "1.1",
12
+ headers: headers,
13
+ body: body,
14
+ request: HTTP::Request.new(verb: :get, uri: "http://example.com")
15
+ )
16
+ end
17
+
18
+ def redirect_response(status, location)
19
+ simple_response status, "", "Location" => location
20
+ end
21
+
22
+ # #strict
23
+
24
+ def test_strict_returns_true_by_default
25
+ redirector = HTTP::Redirector.new
26
+
27
+ assert redirector.strict
28
+ end
29
+
30
+ # #max_hops
31
+
32
+ def test_max_hops_returns_5_by_default
33
+ redirector = HTTP::Redirector.new
34
+
35
+ assert_equal 5, redirector.max_hops
36
+ end
37
+
38
+ def test_max_hops_coerces_string_value_to_integer
39
+ redirector = HTTP::Redirector.new(max_hops: "3")
40
+
41
+ assert_equal 3, redirector.max_hops
42
+ end
43
+
44
+ # #perform
45
+
46
+ def test_perform_fails_with_too_many_redirects_error_if_max_hops_reached
47
+ redirector = HTTP::Redirector.new
48
+ req = HTTP::Request.new verb: :head, uri: "http://example.com"
49
+ res = proc { |prev_req| redirect_response(301, "#{prev_req.uri}/1") }
50
+
51
+ assert_raises(HTTP::Redirector::TooManyRedirectsError) do
52
+ redirector.perform(req, res.call(req), &res)
53
+ end
54
+ end
55
+
56
+ def test_perform_fails_with_endless_redirect_error_if_endless_loop_detected
57
+ redirector = HTTP::Redirector.new
58
+ req = HTTP::Request.new verb: :head, uri: "http://example.com"
59
+ res = redirect_response(301, req.uri)
60
+
61
+ assert_raises(HTTP::Redirector::EndlessRedirectError) do
62
+ redirector.perform(req, res) { res }
63
+ end
64
+ end
65
+
66
+ def test_perform_fails_with_state_error_if_no_location_header
67
+ redirector = HTTP::Redirector.new
68
+ req = HTTP::Request.new verb: :head, uri: "http://example.com"
69
+ res = simple_response(301)
70
+
71
+ assert_raises(HTTP::StateError) do
72
+ redirector.perform(req, res) { |_| nil }
73
+ end
74
+ end
75
+
76
+ def test_perform_returns_first_non_redirect_response
77
+ redirector = HTTP::Redirector.new
78
+ req = HTTP::Request.new verb: :head, uri: "http://example.com"
79
+ hops = [
80
+ redirect_response(301, "http://example.com/1"),
81
+ redirect_response(301, "http://example.com/2"),
82
+ redirect_response(301, "http://example.com/3"),
83
+ simple_response(200, "foo"),
84
+ redirect_response(301, "http://example.com/4"),
85
+ simple_response(200, "bar")
86
+ ]
87
+
88
+ res = redirector.perform(req, hops.shift) { hops.shift }
89
+
90
+ assert_equal "foo", res.to_s
91
+ end
92
+
93
+ def test_perform_concatenates_multiple_location_headers
94
+ redirector = HTTP::Redirector.new
95
+ req = HTTP::Request.new verb: :head, uri: "http://example.com"
96
+ headers = HTTP::Headers.new
97
+
98
+ %w[http://example.com /123].each { |loc| headers.add("Location", loc) }
99
+
100
+ res = redirector.perform(req, simple_response(301, "", headers)) do |redirect|
101
+ simple_response(200, redirect.uri.to_s)
102
+ end
103
+
104
+ assert_equal "http://example.com/123", res.to_s
105
+ end
106
+
107
+ # on_redirect callback
108
+
109
+ def test_perform_with_on_redirect_calls_on_redirect
110
+ redirect_response_captured = nil
111
+ redirect_location_captured = nil
112
+ redirector = HTTP::Redirector.new(
113
+ on_redirect: proc do |response, location|
114
+ redirect_response_captured = response
115
+ redirect_location_captured = location
116
+ end
117
+ )
118
+
119
+ req = HTTP::Request.new verb: :head, uri: "http://example.com"
120
+ hops = [
121
+ redirect_response(301, "http://example.com/1"),
122
+ redirect_response(301, "http://example.com/2"),
123
+ simple_response(200, "foo")
124
+ ]
125
+
126
+ redirector.perform(req, hops.shift) do |prev_req, _|
127
+ assert_equal prev_req.uri.to_s, redirect_location_captured.uri.to_s
128
+ assert_equal 301, redirect_response_captured.code
129
+ hops.shift
130
+ end
131
+ end
132
+
133
+ # following 300, 301, 302 redirects (strict mode)
134
+
135
+ unsafe_verbs = %i[put post delete]
136
+
137
+ [300, 301, 302].each do |status_code|
138
+ define_method(:"test_following_#{status_code}_strict_follows_with_original_verb_if_safe") do
139
+ redirector = HTTP::Redirector.new(strict: true)
140
+ req = HTTP::Request.new verb: :head, uri: "http://example.com"
141
+ res = redirect_response status_code, "http://example.com/1"
142
+
143
+ redirector.perform(req, res) do |prev_req, _|
144
+ assert_equal :head, prev_req.verb
145
+ simple_response 200
146
+ end
147
+ end
148
+
149
+ unsafe_verbs.each do |verb|
150
+ define_method(:"test_following_#{status_code}_strict_raises_state_error_for_#{verb}") do
151
+ redirector = HTTP::Redirector.new(strict: true)
152
+ req = HTTP::Request.new verb: verb, uri: "http://example.com"
153
+ res = redirect_response status_code, "http://example.com/1"
154
+
155
+ assert_raises(HTTP::StateError) do
156
+ redirector.perform(req, res) { simple_response 200 }
157
+ end
158
+ end
159
+ end
160
+
161
+ define_method(:"test_following_#{status_code}_non_strict_follows_with_original_verb_if_safe") do
162
+ redirector = HTTP::Redirector.new(strict: false)
163
+ req = HTTP::Request.new verb: :head, uri: "http://example.com"
164
+ res = redirect_response status_code, "http://example.com/1"
165
+
166
+ redirector.perform(req, res) do |prev_req, _|
167
+ assert_equal :head, prev_req.verb
168
+ simple_response 200
169
+ end
170
+ end
171
+
172
+ unsafe_verbs.each do |verb|
173
+ define_method(:"test_following_#{status_code}_non_strict_follows_with_get_for_#{verb}") do
174
+ redirector = HTTP::Redirector.new(strict: false)
175
+ req = HTTP::Request.new verb: verb, uri: "http://example.com"
176
+ res = redirect_response status_code, "http://example.com/1"
177
+
178
+ redirector.perform(req, res) do |prev_req, _|
179
+ assert_equal :get, prev_req.verb
180
+ simple_response 200
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ # following 303 redirect
187
+
188
+ def test_following_303_follows_with_head_if_original_request_was_head
189
+ redirector = HTTP::Redirector.new
190
+ req = HTTP::Request.new verb: :head, uri: "http://example.com"
191
+ res = redirect_response 303, "http://example.com/1"
192
+
193
+ redirector.perform(req, res) do |prev_req, _|
194
+ assert_equal :head, prev_req.verb
195
+ simple_response 200
196
+ end
197
+ end
198
+
199
+ def test_following_303_follows_with_get_if_original_request_was_get
200
+ redirector = HTTP::Redirector.new
201
+ req = HTTP::Request.new verb: :get, uri: "http://example.com"
202
+ res = redirect_response 303, "http://example.com/1"
203
+
204
+ redirector.perform(req, res) do |prev_req, _|
205
+ assert_equal :get, prev_req.verb
206
+ simple_response 200
207
+ end
208
+ end
209
+
210
+ def test_following_303_follows_with_get_if_original_request_was_neither_get_nor_head
211
+ redirector = HTTP::Redirector.new
212
+ req = HTTP::Request.new verb: :post, uri: "http://example.com"
213
+ res = redirect_response 303, "http://example.com/1"
214
+
215
+ redirector.perform(req, res) do |prev_req, _|
216
+ assert_equal :get, prev_req.verb
217
+ simple_response 200
218
+ end
219
+ end
220
+
221
+ # following 307 redirect
222
+
223
+ def test_following_307_follows_with_original_requests_verb
224
+ redirector = HTTP::Redirector.new
225
+ req = HTTP::Request.new verb: :post, uri: "http://example.com"
226
+ res = redirect_response 307, "http://example.com/1"
227
+
228
+ redirector.perform(req, res) do |prev_req, _|
229
+ assert_equal :post, prev_req.verb
230
+ simple_response 200
231
+ end
232
+ end
233
+
234
+ # following 308 redirect
235
+
236
+ def test_following_308_follows_with_original_requests_verb
237
+ redirector = HTTP::Redirector.new
238
+ req = HTTP::Request.new verb: :post, uri: "http://example.com"
239
+ res = redirect_response 308, "http://example.com/1"
240
+
241
+ redirector.perform(req, res) do |prev_req, _|
242
+ assert_equal :post, prev_req.verb
243
+ simple_response 200
244
+ end
245
+ end
246
+
247
+ # changing verbs during redirects
248
+
249
+ def test_changing_verbs_follows_without_body_content_type_if_it_has_to_change_verb
250
+ redirector = HTTP::Redirector.new(strict: false)
251
+ post_body = HTTP::Request::Body.new("i might be way longer in real life")
252
+ cookie = "dont=eat my cookies"
253
+
254
+ req = HTTP::Request.new(
255
+ verb: :post, uri: "http://example.com",
256
+ body: post_body, headers: {
257
+ "Content-Type" => "meme",
258
+ "Cookie" => cookie
259
+ }
260
+ )
261
+ res = redirect_response 302, "http://example.com/1"
262
+
263
+ redirector.perform(req, res) do |prev_req, _|
264
+ assert_equal HTTP::Request::Body.new(nil), prev_req.body
265
+ assert_equal cookie, prev_req.headers["Cookie"]
266
+ assert_nil prev_req.headers["Content-Type"]
267
+ simple_response 200
268
+ end
269
+ end
270
+
271
+ def test_changing_verbs_leaves_body_content_type_intact_if_it_does_not_have_to_change_verb
272
+ redirector = HTTP::Redirector.new(strict: false)
273
+ post_body = HTTP::Request::Body.new("i might be way longer in real life")
274
+ cookie = "dont=eat my cookies"
275
+
276
+ req = HTTP::Request.new(
277
+ verb: :post, uri: "http://example.com",
278
+ body: post_body, headers: {
279
+ "Content-Type" => "meme",
280
+ "Cookie" => cookie
281
+ }
282
+ )
283
+ res = redirect_response 307, "http://example.com/1"
284
+
285
+ redirector.perform(req, res) do |prev_req, _|
286
+ assert_equal post_body, prev_req.body
287
+ assert_equal cookie, prev_req.headers["Cookie"]
288
+ assert_equal "meme", prev_req.headers["Content-Type"]
289
+ simple_response 200
290
+ end
291
+ end
292
+
293
+ # max_hops: 0
294
+
295
+ def test_with_max_hops_0_does_not_limit_redirects
296
+ redirector = HTTP::Redirector.new(max_hops: 0)
297
+ req = HTTP::Request.new verb: :head, uri: "http://example.com"
298
+ hops = (1..10).map { |i| redirect_response(301, "http://example.com/#{i}") }
299
+ hops << simple_response(200, "done")
300
+
301
+ res = redirector.perform(req, hops.shift) { hops.shift }
302
+
303
+ assert_equal "done", res.to_s
304
+ end
305
+
306
+ # max_hops: 1
307
+
308
+ def test_with_max_hops_1_allows_exactly_one_redirect
309
+ redirector = HTTP::Redirector.new(max_hops: 1)
310
+ req = HTTP::Request.new verb: :head, uri: "http://example.com"
311
+ hops = [
312
+ redirect_response(301, "http://example.com/1"),
313
+ simple_response(200, "one hop")
314
+ ]
315
+
316
+ res = redirector.perform(req, hops.shift) { hops.shift }
317
+
318
+ assert_equal "one hop", res.to_s
319
+ end
320
+
321
+ def test_with_max_hops_1_raises_too_many_redirects_error_on_the_second_redirect
322
+ redirector = HTTP::Redirector.new(max_hops: 1)
323
+ req = HTTP::Request.new verb: :head, uri: "http://example.com"
324
+ hops = [
325
+ redirect_response(301, "http://example.com/1"),
326
+ redirect_response(301, "http://example.com/2"),
327
+ simple_response(200, "unreachable")
328
+ ]
329
+
330
+ assert_raises(HTTP::Redirector::TooManyRedirectsError) do
331
+ redirector.perform(req, hops.shift) { hops.shift }
332
+ end
333
+ end
334
+
335
+ # with :get verb on strict-sensitive codes
336
+
337
+ [300, 301, 302].each do |status_code|
338
+ define_method(:"test_strict_follows_#{status_code}_redirect_with_get_verb_without_raising") do
339
+ redirector = HTTP::Redirector.new(strict: true)
340
+ req = HTTP::Request.new verb: :get, uri: "http://example.com"
341
+ res = redirect_response status_code, "http://example.com/1"
342
+
343
+ result = redirector.perform(req, res) do |prev_req, _|
344
+ assert_equal :get, prev_req.verb
345
+ simple_response 200, "ok"
346
+ end
347
+
348
+ assert_equal "ok", result.to_s
349
+ end
350
+ end
351
+
352
+ # following 303 redirect with unsafe verbs
353
+
354
+ def test_following_303_follows_with_get_if_original_request_was_put
355
+ redirector = HTTP::Redirector.new
356
+ req = HTTP::Request.new verb: :put, uri: "http://example.com"
357
+ res = redirect_response 303, "http://example.com/1"
358
+
359
+ redirector.perform(req, res) do |prev_req, _|
360
+ assert_equal :get, prev_req.verb
361
+ simple_response 200
362
+ end
363
+ end
364
+
365
+ def test_following_303_follows_with_get_if_original_request_was_delete
366
+ redirector = HTTP::Redirector.new
367
+ req = HTTP::Request.new verb: :delete, uri: "http://example.com"
368
+ res = redirect_response 303, "http://example.com/1"
369
+
370
+ redirector.perform(req, res) do |prev_req, _|
371
+ assert_equal :get, prev_req.verb
372
+ simple_response 200
373
+ end
374
+ end
375
+
376
+ # on_redirect callback behavior
377
+
378
+ def test_on_redirect_passes_both_response_and_request_to_on_redirect
379
+ captured_response = nil
380
+ captured_request = nil
381
+ redirector = HTTP::Redirector.new(
382
+ on_redirect: proc do |response, request|
383
+ captured_response = response
384
+ captured_request = request
385
+ end
386
+ )
387
+
388
+ req = HTTP::Request.new verb: :get, uri: "http://example.com"
389
+ hops = [
390
+ redirect_response(301, "http://example.com/1"),
391
+ simple_response(200, "done")
392
+ ]
393
+
394
+ redirector.perform(req, hops.shift) { hops.shift }
395
+
396
+ refute_nil captured_response
397
+ refute_nil captured_request
398
+ assert_equal 301, captured_response.code
399
+ assert_equal "http://example.com/1", captured_request.uri.to_s
400
+ end
401
+
402
+ def test_on_redirect_works_without_on_redirect_callback
403
+ redirector = HTTP::Redirector.new
404
+
405
+ req = HTTP::Request.new verb: :get, uri: "http://example.com"
406
+ hops = [
407
+ redirect_response(301, "http://example.com/1"),
408
+ simple_response(200, "done")
409
+ ]
410
+
411
+ res = redirector.perform(req, hops.shift) { hops.shift }
412
+
413
+ assert_equal "done", res.to_s
414
+ end
415
+
416
+ def test_on_redirect_works_when_on_redirect_is_explicitly_nil
417
+ redirector = HTTP::Redirector.new(on_redirect: nil)
418
+
419
+ req = HTTP::Request.new verb: :get, uri: "http://example.com"
420
+ hops = [
421
+ redirect_response(301, "http://example.com/1"),
422
+ simple_response(200, "done")
423
+ ]
424
+
425
+ res = redirector.perform(req, hops.shift) { hops.shift }
426
+
427
+ assert_equal "done", res.to_s
428
+ end
429
+
430
+ # block yielding
431
+
432
+ def test_perform_yields_the_request_to_the_block
433
+ redirector = HTTP::Redirector.new
434
+ req = HTTP::Request.new verb: :get, uri: "http://example.com"
435
+ hops = [
436
+ redirect_response(301, "http://example.com/1"),
437
+ simple_response(200, "done")
438
+ ]
439
+
440
+ yielded_request = nil
441
+ redirector.perform(req, hops.shift) do |r|
442
+ yielded_request = r
443
+ hops.shift
444
+ end
445
+
446
+ refute_nil yielded_request
447
+ assert_equal "http://example.com/1", yielded_request.uri.to_s
448
+ end
449
+
450
+ # flush
451
+
452
+ def test_perform_calls_flush_on_intermediate_redirect_responses
453
+ redirector = HTTP::Redirector.new
454
+ req = HTTP::Request.new verb: :get, uri: "http://example.com"
455
+ res = redirect_response(301, "http://example.com/1")
456
+
457
+ flushed = false
458
+ original_flush = res.method(:flush)
459
+ res.define_singleton_method(:flush) do
460
+ flushed = true
461
+ original_flush.call
462
+ end
463
+
464
+ redirector.perform(req, res) { simple_response(200, "done") }
465
+
466
+ assert flushed, "expected response.flush to be called during redirect"
467
+ end
468
+
469
+ # endless redirect detection
470
+
471
+ def test_perform_tracks_visited_urls_with_verb_uri_and_cookies
472
+ redirector = HTTP::Redirector.new
473
+ req = HTTP::Request.new verb: :head, uri: "http://example.com"
474
+ res = redirect_response(301, "http://example.com")
475
+
476
+ err = assert_raises(HTTP::Redirector::EndlessRedirectError) do
477
+ redirector.perform(req, res) { redirect_response(301, "http://example.com") }
478
+ end
479
+ assert_kind_of HTTP::Redirector::TooManyRedirectsError, err
480
+ end
481
+
482
+ def test_perform_does_not_falsely_detect_endless_loop_when_cookies_change
483
+ redirector = HTTP::Redirector.new
484
+ req = HTTP::Request.new verb: :get, uri: "http://example.com"
485
+ res = redirect_response(302, "http://example.com")
486
+
487
+ call_count = 0
488
+ result = redirector.perform(req, res) do |redirect_req|
489
+ call_count += 1
490
+ redirect_req.headers.set("Cookie", "auth=ok")
491
+ if call_count == 1
492
+ redirect_response(302, "http://example.com")
493
+ else
494
+ simple_response(200, "authenticated")
495
+ end
496
+ end
497
+
498
+ assert_equal 2, call_count
499
+ assert_equal "authenticated", result.to_s
500
+ end
501
+
502
+ def test_perform_raises_state_error_with_descriptive_message_when_no_location_header
503
+ redirector = HTTP::Redirector.new
504
+ req = HTTP::Request.new verb: :head, uri: "http://example.com"
505
+ res = simple_response(301)
506
+
507
+ err = assert_raises(HTTP::StateError) do
508
+ redirector.perform(req, res) { |_| nil }
509
+ end
510
+ assert_match(/no Location header/, err.message)
511
+ end
512
+
513
+ # strict mode StateError messages
514
+
515
+ def test_strict_mode_includes_status_in_the_error_message
516
+ redirector = HTTP::Redirector.new(strict: true)
517
+ req = HTTP::Request.new verb: :post, uri: "http://example.com"
518
+ res = redirect_response 301, "http://example.com/1"
519
+
520
+ err = assert_raises(HTTP::StateError) do
521
+ redirector.perform(req, res) { simple_response 200 }
522
+ end
523
+ assert_match(/301/, err.message)
524
+ end
525
+
526
+ # max_hops: 2 with endless redirect loop
527
+
528
+ def test_with_max_hops_2_detects_endless_loop_before_reaching_max_hops
529
+ redirector = HTTP::Redirector.new(max_hops: 2)
530
+ req = HTTP::Request.new verb: :head, uri: "http://example.com"
531
+ res = redirect_response(301, "http://example.com")
532
+
533
+ assert_raises(HTTP::Redirector::EndlessRedirectError) do
534
+ redirector.perform(req, res) { redirect_response(301, "http://example.com") }
535
+ end
536
+ end
537
+
538
+ def test_perform_detects_endless_loop_when_repeated_url_is_not_the_first_one_visited
539
+ redirector = HTTP::Redirector.new
540
+ req = HTTP::Request.new verb: :get, uri: "http://a.example.com"
541
+ hops = [
542
+ redirect_response(301, "http://b.example.com"),
543
+ redirect_response(301, "http://c.example.com"),
544
+ redirect_response(301, "http://b.example.com"),
545
+ redirect_response(301, "http://d.example.com"),
546
+ simple_response(200, "unreachable")
547
+ ]
548
+
549
+ assert_raises(HTTP::Redirector::EndlessRedirectError) do
550
+ redirector.perform(req, hops.shift) { hops.shift }
551
+ end
552
+ end
553
+
554
+ # sensitive headers
555
+
556
+ def test_sensitive_headers_preserves_authorization_and_cookie_when_redirecting_to_same_origin
557
+ redirector = HTTP::Redirector.new
558
+ req = HTTP::Request.new verb: :get, uri: "http://example.com"
559
+ req.headers.set("Authorization", "Bearer secret")
560
+ req.headers.set("Cookie", "session=abc")
561
+ hops = [
562
+ redirect_response(301, "http://example.com/other"),
563
+ simple_response(200, "done")
564
+ ]
565
+
566
+ redirector.perform(req, hops.shift) do |request|
567
+ assert_equal "Bearer secret", request.headers["Authorization"]
568
+ assert_equal "session=abc", request.headers["Cookie"]
569
+ hops.shift
570
+ end
571
+ end
572
+
573
+ def test_sensitive_headers_strips_authorization_and_cookie_when_redirecting_to_different_host
574
+ redirector = HTTP::Redirector.new
575
+ req = HTTP::Request.new verb: :get, uri: "http://example.com"
576
+ req.headers.set("Authorization", "Bearer secret")
577
+ req.headers.set("Cookie", "session=abc")
578
+ hops = [
579
+ redirect_response(301, "http://other.example.com/"),
580
+ simple_response(200, "done")
581
+ ]
582
+
583
+ redirector.perform(req, hops.shift) do |request|
584
+ assert_nil request.headers["Authorization"]
585
+ assert_nil request.headers["Cookie"]
586
+ hops.shift
587
+ end
588
+ end
589
+
590
+ def test_sensitive_headers_strips_authorization_and_cookie_when_redirecting_to_different_scheme
591
+ redirector = HTTP::Redirector.new
592
+ req = HTTP::Request.new verb: :get, uri: "http://example.com"
593
+ req.headers.set("Authorization", "Bearer secret")
594
+ req.headers.set("Cookie", "session=abc")
595
+ hops = [
596
+ redirect_response(301, "https://example.com/"),
597
+ simple_response(200, "done")
598
+ ]
599
+
600
+ redirector.perform(req, hops.shift) do |request|
601
+ assert_nil request.headers["Authorization"]
602
+ assert_nil request.headers["Cookie"]
603
+ hops.shift
604
+ end
605
+ end
606
+
607
+ def test_sensitive_headers_strips_authorization_and_cookie_when_redirecting_to_different_port
608
+ redirector = HTTP::Redirector.new
609
+ req = HTTP::Request.new verb: :get, uri: "http://example.com"
610
+ req.headers.set("Authorization", "Bearer secret")
611
+ req.headers.set("Cookie", "session=abc")
612
+ hops = [
613
+ redirect_response(301, "http://example.com:8080/"),
614
+ simple_response(200, "done")
615
+ ]
616
+
617
+ redirector.perform(req, hops.shift) do |request|
618
+ assert_nil request.headers["Authorization"]
619
+ assert_nil request.headers["Cookie"]
620
+ hops.shift
621
+ end
622
+ end
623
+
624
+ # verb change does not cause false endless loop detection
625
+
626
+ def test_perform_does_not_falsely_detect_endless_loop_when_verb_changes_for_same_url
627
+ req = HTTP::Request.new verb: :post, uri: "http://example.com"
628
+ hops = [
629
+ redirect_response(302, "http://example.com/done"),
630
+ simple_response(200, "done")
631
+ ]
632
+
633
+ res = HTTP::Redirector.new(strict: false, max_hops: 5).perform(
634
+ req, redirect_response(302, "http://example.com")
635
+ ) { hops.shift }
636
+
637
+ assert_equal "done", res.to_s
638
+ end
639
+ end