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,424 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ require "support/dummy_server"
6
+
7
+ class HTTPSessionTest < Minitest::Test
8
+ cover "HTTP::Session*"
9
+ run_server(:dummy) { DummyServer.new }
10
+ run_server(:dummy2) { DummyServer.new }
11
+
12
+ def session
13
+ @session ||= HTTP::Session.new
14
+ end
15
+
16
+ # #initialize
17
+
18
+ def test_initialize_creates_a_session_with_default_options
19
+ assert_kind_of HTTP::Options, session.default_options
20
+ end
21
+
22
+ def test_initialize_creates_a_session_with_given_options
23
+ session = HTTP::Session.new(headers: { "Accept" => "text/html" })
24
+
25
+ assert_equal "text/html", session.default_options.headers[:accept]
26
+ end
27
+
28
+ # #request
29
+
30
+ def test_request_returns_an_http_response
31
+ response = session.request(:get, dummy.endpoint)
32
+
33
+ assert_kind_of HTTP::Response, response
34
+ end
35
+
36
+ def test_request_creates_a_new_client_for_each_request
37
+ client_ids = []
38
+ original_new = HTTP::Client.method(:new)
39
+
40
+ HTTP::Client.stub(:new, lambda { |*args|
41
+ c = original_new.call(*args)
42
+ client_ids << c.object_id
43
+ c
44
+ }) do
45
+ session.get(dummy.endpoint)
46
+ session.get(dummy.endpoint)
47
+ end
48
+
49
+ assert_equal 2, client_ids.uniq.size
50
+ end
51
+
52
+ # #request with block
53
+
54
+ def test_request_with_block_yields_the_response_and_returns_block_value
55
+ result = session.get(dummy.endpoint) { |res| res.status.code }
56
+
57
+ assert_equal 200, result
58
+ end
59
+
60
+ def test_request_with_block_closes_the_client_after_the_block
61
+ closed = false
62
+ original_make = session.method(:make_client) # steep:ignore
63
+ session.define_singleton_method(:make_client) do |opts|
64
+ client = original_make.call(opts)
65
+ original_close = client.method(:close)
66
+ client.define_singleton_method(:close) do
67
+ closed = true
68
+ original_close.call
69
+ end
70
+ client
71
+ end
72
+
73
+ session.get(dummy.endpoint, &:status)
74
+
75
+ assert closed, "expected close to have been called"
76
+ end
77
+
78
+ def test_request_with_block_closes_the_client_even_when_the_block_raises
79
+ closed = false
80
+ original_make = session.method(:make_client) # steep:ignore
81
+ session.define_singleton_method(:make_client) do |opts|
82
+ client = original_make.call(opts)
83
+ original_close = client.method(:close)
84
+ client.define_singleton_method(:close) do
85
+ closed = true
86
+ original_close.call
87
+ end
88
+ client
89
+ end
90
+
91
+ assert_raises(RuntimeError) { session.get(dummy.endpoint) { raise "boom" } }
92
+
93
+ assert closed, "expected close to have been called on error"
94
+ end
95
+
96
+ def test_request_with_block_handles_nil_client_when_make_client_raises
97
+ session.define_singleton_method(:make_client) { |*| raise "boom" }
98
+
99
+ assert_raises(RuntimeError) { session.get(dummy.endpoint) { nil } }
100
+ end
101
+
102
+ # Request::Builder
103
+
104
+ def test_request_builder_builds_an_http_request_from_session_options
105
+ builder = HTTP::Request::Builder.new(session.default_options)
106
+ req = builder.build(:get, "http://example.com/")
107
+
108
+ assert_kind_of HTTP::Request, req
109
+ end
110
+
111
+ # #persistent?
112
+
113
+ def test_persistent_returns_false_by_default
114
+ refute_predicate session, :persistent?
115
+ end
116
+
117
+ # chaining
118
+
119
+ def test_chaining_returns_a_session_from_headers
120
+ chained = session.headers("Accept" => "text/html")
121
+
122
+ assert_kind_of HTTP::Session, chained
123
+ end
124
+
125
+ def test_chaining_returns_a_session_from_timeout
126
+ chained = session.timeout(10)
127
+
128
+ assert_kind_of HTTP::Session, chained
129
+ end
130
+
131
+ def test_chaining_returns_a_session_from_cookies
132
+ chained = session.cookies(session_id: "abc")
133
+
134
+ assert_kind_of HTTP::Session, chained
135
+ end
136
+
137
+ def test_chaining_returns_a_session_from_follow
138
+ chained = session.follow
139
+
140
+ assert_kind_of HTTP::Session, chained
141
+ end
142
+
143
+ def test_chaining_returns_a_session_from_use
144
+ chained = session.use(:auto_deflate)
145
+
146
+ assert_kind_of HTTP::Session, chained
147
+ end
148
+
149
+ def test_chaining_returns_a_session_from_nodelay
150
+ chained = session.nodelay
151
+
152
+ assert_kind_of HTTP::Session, chained
153
+ end
154
+
155
+ def test_chaining_returns_a_session_from_encoding
156
+ chained = session.encoding("UTF-8")
157
+
158
+ assert_kind_of HTTP::Session, chained
159
+ end
160
+
161
+ def test_chaining_returns_a_session_from_via
162
+ chained = session.via("proxy.example.com", 8080)
163
+
164
+ assert_kind_of HTTP::Session, chained
165
+ end
166
+
167
+ def test_chaining_returns_a_session_from_retriable
168
+ chained = session.retriable
169
+
170
+ assert_kind_of HTTP::Session, chained
171
+ end
172
+
173
+ def test_chaining_returns_a_session_from_digest_auth
174
+ chained = session.digest_auth(user: "admin", pass: "secret")
175
+
176
+ assert_kind_of HTTP::Session, chained
177
+ end
178
+
179
+ def test_chaining_preserves_options_through_chaining
180
+ chained = session.headers("Accept" => "text/html")
181
+ .timeout(10)
182
+ .cookies(session_id: "abc")
183
+
184
+ assert_equal "text/html", chained.default_options.headers[:accept]
185
+ assert_equal HTTP::Timeout::Global, chained.default_options.timeout_class
186
+ assert_equal "session_id=abc", chained.default_options.headers["Cookie"]
187
+ end
188
+
189
+ # thread safety
190
+
191
+ def test_thread_safety_can_be_shared_across_threads_without_errors
192
+ shared_session = HTTP.headers("Accept" => "text/html").timeout(15)
193
+ errors = []
194
+ mutex = Mutex.new
195
+
196
+ threads = Array.new(5) do
197
+ Thread.new do
198
+ shared_session.get(dummy.endpoint)
199
+ rescue => e
200
+ mutex.synchronize { errors << e }
201
+ end
202
+ end
203
+ threads.each(&:join)
204
+
205
+ assert_empty errors, "Expected no errors but got: #{errors.map(&:message).join(', ')}"
206
+ end
207
+
208
+ # cookies during redirects
209
+
210
+ def test_cookies_during_redirects_forwards_response_cookies_through_redirect_chain
211
+ response = HTTP.follow.get("#{dummy.endpoint}/redirect-with-cookie")
212
+
213
+ assert_includes response.to_s, "from_redirect=yes"
214
+ end
215
+
216
+ def test_cookies_during_redirects_accumulates_cookies_across_redirect_hops
217
+ response = HTTP.follow.get("#{dummy.endpoint}/redirect-cookie-chain/1")
218
+ body = response.to_s
219
+
220
+ assert_includes body, "first=1"
221
+ assert_includes body, "second=2"
222
+ end
223
+
224
+ def test_cookies_during_redirects_forwards_initial_request_cookies_through_redirects
225
+ response = HTTP.cookies(original: "value").follow.get("#{dummy.endpoint}/redirect-no-cookies")
226
+
227
+ assert_includes response.to_s, "original=value"
228
+ end
229
+
230
+ def test_cookies_during_redirects_deletes_cookies_with_empty_value_during_redirect
231
+ response = HTTP.follow.get("#{dummy.endpoint}/redirect-set-then-delete/1")
232
+
233
+ refute_includes response.to_s, "temp="
234
+ end
235
+
236
+ def test_cookies_during_redirects_breaks_redirect_loop_when_cookie_changes_the_server_response
237
+ response = HTTP.follow.get("#{dummy.endpoint}/cookie-loop")
238
+
239
+ assert_equal "authenticated", response.to_s
240
+ end
241
+
242
+ def test_cookies_during_redirects_does_not_set_cookie_header_when_no_cookies_present
243
+ response = HTTP.follow.get("#{dummy.endpoint}/redirect-no-cookies")
244
+
245
+ assert_equal "", response.to_s
246
+ end
247
+
248
+ def test_cookies_during_redirects_applies_features_to_redirect_requests
249
+ response = HTTP.use(:auto_deflate).follow.get("#{dummy.endpoint}/redirect-301")
250
+
251
+ assert_equal "<!doctype html>", response.to_s
252
+ end
253
+
254
+ # persistent
255
+
256
+ def test_persistent_returns_an_http_session
257
+ session = HTTP::Session.new.persistent(dummy.endpoint)
258
+
259
+ assert_kind_of HTTP::Session, session
260
+ ensure
261
+ session&.close
262
+ end
263
+
264
+ # #close
265
+
266
+ def test_close_closes_all_pooled_clients
267
+ session = HTTP.persistent(dummy.endpoint)
268
+ session.get("/")
269
+
270
+ clients = session.instance_variable_get(:@clients)
271
+
272
+ refute_empty clients
273
+
274
+ session.close
275
+
276
+ assert_empty clients
277
+ end
278
+
279
+ def test_close_is_safe_to_call_on_non_persistent_sessions
280
+ session.close
281
+ end
282
+
283
+ # persistent connection reuse with chaining
284
+
285
+ def test_persistent_chaining_reuses_connections_when_chaining_headers
286
+ session = HTTP.persistent(dummy.endpoint)
287
+
288
+ sock1 = session.headers("Accept" => "application/json").get("#{dummy.endpoint}/socket/1").to_s
289
+ sock2 = session.headers("Accept" => "text/html").get("#{dummy.endpoint}/socket/2").to_s
290
+
291
+ refute_equal "", sock1
292
+ assert_equal sock1, sock2
293
+ ensure
294
+ session&.close
295
+ end
296
+
297
+ def test_persistent_chaining_reuses_connections_when_chaining_auth
298
+ session = HTTP.persistent(dummy.endpoint)
299
+
300
+ sock1 = session.auth("Bearer token").get("#{dummy.endpoint}/socket/1").to_s
301
+ sock2 = session.auth("Bearer token").get("#{dummy.endpoint}/socket/2").to_s
302
+
303
+ refute_equal "", sock1
304
+ assert_equal sock1, sock2
305
+ ensure
306
+ session&.close
307
+ end
308
+
309
+ def test_persistent_chaining_shares_the_connection_pool_across_chained_sessions
310
+ session = HTTP.persistent(dummy.endpoint)
311
+ chained = session.headers("Accept" => "application/json")
312
+
313
+ assert_same session.instance_variable_get(:@clients),
314
+ chained.instance_variable_get(:@clients)
315
+ ensure
316
+ session&.close
317
+ end
318
+
319
+ def test_persistent_chaining_does_not_share_pool_for_non_persistent_sessions
320
+ chained = session.headers("Accept" => "application/json")
321
+
322
+ refute_same session.instance_variable_get(:@clients),
323
+ chained.instance_variable_get(:@clients)
324
+ end
325
+
326
+ # base_uri
327
+
328
+ def test_base_uri_returns_a_session_from_base_uri
329
+ chained = session.base_uri(dummy.endpoint)
330
+
331
+ assert_kind_of HTTP::Session, chained
332
+ end
333
+
334
+ def test_base_uri_preserves_base_uri_through_chaining
335
+ chained = session.base_uri("https://example.com/api")
336
+ .headers("Accept" => "application/json")
337
+
338
+ assert_equal "https://example.com/api", chained.default_options.base_uri.to_s
339
+ assert_equal "application/json", chained.default_options.headers[:accept]
340
+ end
341
+
342
+ def test_base_uri_resolves_relative_request_paths_against_base_uri
343
+ response = HTTP.base_uri(dummy.endpoint).get("/")
344
+
345
+ assert_kind_of HTTP::Response, response
346
+ end
347
+
348
+ # persistent cross-origin redirects
349
+
350
+ def test_cross_origin_follows_redirects_to_a_different_origin
351
+ target = "#{dummy2.endpoint}/"
352
+ response = HTTP.persistent(dummy.endpoint).follow
353
+ .get("#{dummy.endpoint}/cross-origin-redirect?target=#{target}")
354
+
355
+ assert_equal 200, response.status.code
356
+ assert_equal "<!doctype html>", response.to_s
357
+ end
358
+
359
+ def test_cross_origin_follows_redirects_back_to_the_original_origin
360
+ bounce_back = "#{dummy.endpoint}/"
361
+ target = "#{dummy2.endpoint}/cross-origin-redirect?target=#{bounce_back}"
362
+ response = HTTP.persistent(dummy.endpoint).follow
363
+ .get("#{dummy.endpoint}/cross-origin-redirect?target=#{target}")
364
+
365
+ assert_equal 200, response.status.code
366
+ assert_equal "<!doctype html>", response.to_s
367
+ end
368
+
369
+ def test_cross_origin_pools_clients_per_origin
370
+ target = "#{dummy2.endpoint}/"
371
+
372
+ HTTP.persistent(dummy.endpoint) do |http|
373
+ session = http.follow
374
+ session.get("#{dummy.endpoint}/cross-origin-redirect?target=#{target}")
375
+ clients = session.instance_variable_get(:@clients)
376
+
377
+ assert_equal 2, clients.size
378
+ assert_includes clients.keys, URI.parse(dummy.endpoint).origin
379
+ assert_includes clients.keys, URI.parse(dummy2.endpoint).origin
380
+
381
+ session.close
382
+ end
383
+ end
384
+
385
+ def test_cross_origin_manages_cookies_across_cross_origin_redirect_hops
386
+ target = "#{dummy2.endpoint}/echo-cookies"
387
+ session = HTTP.persistent(dummy.endpoint).follow
388
+ response = session.get("#{dummy.endpoint}/cross-origin-redirect-with-cookie?target=#{target}")
389
+
390
+ assert_equal 200, response.status.code
391
+ assert_equal "from_origin=yes", response.to_s
392
+ ensure
393
+ session&.close
394
+ end
395
+
396
+ def test_cross_origin_reuses_pooled_connections_within_the_same_origin
397
+ HTTP.persistent(dummy.endpoint) do |http|
398
+ http.get(dummy.endpoint)
399
+ http.get(dummy.endpoint)
400
+
401
+ clients = http.instance_variable_get(:@clients)
402
+
403
+ assert_equal 1, clients.size
404
+ end
405
+ end
406
+
407
+ def test_cross_origin_closes_all_pooled_connections_with_block_form_of_get
408
+ closed_origins = []
409
+ session = HTTP.persistent(dummy.endpoint).follow
410
+
411
+ target = "#{dummy2.endpoint}/"
412
+ session.get("#{dummy.endpoint}/cross-origin-redirect?target=#{target}") do |_res|
413
+ session.instance_variable_get(:@clients).each_value do |client|
414
+ original_close = client.method(:close)
415
+ client.define_singleton_method(:close) do
416
+ closed_origins << default_options.persistent
417
+ original_close.call
418
+ end
419
+ end
420
+ end
421
+
422
+ assert_equal 2, closed_origins.size
423
+ end
424
+ end
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class HTTPTimeoutGlobalTest < Minitest::Test
6
+ cover "HTTP::Timeout::Global*"
7
+
8
+ def setup
9
+ super
10
+ @io = fake(wait_readable: true, wait_writable: true)
11
+ @socket = fake(to_io: @io, closed?: false)
12
+ @timeout = HTTP::Timeout::Global.new(global_timeout: 5)
13
+ @timeout.instance_variable_set(:@socket, @socket)
14
+ end
15
+
16
+ # -- #connect --
17
+
18
+ def test_connect_sets_tcp_nodelay_when_nodelay_is_true
19
+ setsockopt_args = nil
20
+ tcp_socket = fake(
21
+ setsockopt: ->(*args) { setsockopt_args = args }
22
+ )
23
+
24
+ socket_class = fake(open: tcp_socket)
25
+ @timeout.connect(socket_class, "example.com", 80, nodelay: true)
26
+
27
+ assert_equal [Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1], setsockopt_args
28
+ end
29
+
30
+ # -- #connect_ssl --
31
+
32
+ def test_connect_ssl_completes_without_error
33
+ connected = Object.new
34
+ socket = fake(
35
+ to_io: @io,
36
+ closed?: false,
37
+ connect_nonblock: ->(*) { connected }
38
+ )
39
+ @timeout.instance_variable_set(:@socket, socket)
40
+ @timeout.connect_ssl
41
+ end
42
+
43
+ def test_connect_ssl_when_wait_readable_raised_waits_and_retries
44
+ call_count = 0
45
+ connected = Object.new
46
+ socket = fake(
47
+ to_io: @io,
48
+ closed?: false,
49
+ connect_nonblock: proc { |*|
50
+ call_count += 1
51
+ raise IO::EAGAINWaitReadable if call_count == 1
52
+
53
+ connected
54
+ }
55
+ )
56
+ @timeout.instance_variable_set(:@socket, socket)
57
+ @timeout.connect_ssl
58
+ end
59
+
60
+ def test_connect_ssl_when_wait_writable_raised_waits_and_retries
61
+ call_count = 0
62
+ connected = Object.new
63
+ socket = fake(
64
+ to_io: @io,
65
+ closed?: false,
66
+ connect_nonblock: proc { |*|
67
+ call_count += 1
68
+ raise IO::EAGAINWaitWritable if call_count == 1
69
+
70
+ connected
71
+ }
72
+ )
73
+ @timeout.instance_variable_set(:@socket, socket)
74
+ @timeout.connect_ssl
75
+ end
76
+
77
+ # -- #perform_io (via readpartial) --
78
+
79
+ def test_readpartial_when_wait_readable_waits_and_retries
80
+ call_count = 0
81
+ socket = fake(
82
+ to_io: @io,
83
+ closed?: false,
84
+ read_nonblock: proc { |*|
85
+ call_count += 1
86
+ call_count == 1 ? :wait_readable : "data"
87
+ }
88
+ )
89
+ @timeout.instance_variable_set(:@socket, socket)
90
+
91
+ assert_equal "data", @timeout.readpartial(10)
92
+ end
93
+
94
+ def test_write_when_wait_writable_waits_and_retries
95
+ call_count = 0
96
+ socket = fake(
97
+ to_io: @io,
98
+ closed?: false,
99
+ write_nonblock: proc { |*|
100
+ call_count += 1
101
+ call_count == 1 ? :wait_writable : 4
102
+ }
103
+ )
104
+ @timeout.instance_variable_set(:@socket, socket)
105
+
106
+ assert_equal 4, @timeout.write("data")
107
+ end
108
+
109
+ def test_readpartial_when_io_wait_readable_raised_waits_and_retries
110
+ call_count = 0
111
+ socket = fake(
112
+ to_io: @io,
113
+ closed?: false,
114
+ read_nonblock: proc { |*|
115
+ call_count += 1
116
+ raise IO::EAGAINWaitReadable if call_count == 1
117
+
118
+ "data"
119
+ }
120
+ )
121
+ @timeout.instance_variable_set(:@socket, socket)
122
+
123
+ assert_equal "data", @timeout.readpartial(10)
124
+ end
125
+
126
+ def test_write_when_io_wait_writable_raised_waits_and_retries
127
+ call_count = 0
128
+ socket = fake(
129
+ to_io: @io,
130
+ closed?: false,
131
+ write_nonblock: proc { |*|
132
+ call_count += 1
133
+ raise IO::EAGAINWaitWritable if call_count == 1
134
+
135
+ 4
136
+ }
137
+ )
138
+ @timeout.instance_variable_set(:@socket, socket)
139
+
140
+ assert_equal 4, @timeout.write("data")
141
+ end
142
+
143
+ def test_readpartial_when_nil_eof_returns_eof
144
+ socket = fake(
145
+ to_io: @io,
146
+ closed?: false,
147
+ read_nonblock: nil
148
+ )
149
+ @timeout.instance_variable_set(:@socket, socket)
150
+
151
+ assert_equal :eof, @timeout.readpartial(10)
152
+ end
153
+
154
+ def test_readpartial_when_eof_error_raised_returns_eof
155
+ socket = fake(
156
+ to_io: @io,
157
+ closed?: false,
158
+ read_nonblock: ->(*) { raise EOFError }
159
+ )
160
+ @timeout.instance_variable_set(:@socket, socket)
161
+
162
+ assert_equal :eof, @timeout.readpartial(10)
163
+ end
164
+
165
+ # -- with per-operation timeouts --
166
+
167
+ def test_readpartial_with_per_op_timeouts_uses_global_time_left_as_effective_timeout
168
+ timeout = HTTP::Timeout::Global.new(global_timeout: 100, read_timeout: 100, write_timeout: 100,
169
+ connect_timeout: 100)
170
+ call_count = 0
171
+ socket = fake(
172
+ to_io: @io,
173
+ closed?: false,
174
+ read_nonblock: proc { |*|
175
+ call_count += 1
176
+ call_count == 1 ? :wait_readable : "data"
177
+ }
178
+ )
179
+ timeout.instance_variable_set(:@socket, socket)
180
+
181
+ assert_equal "data", timeout.readpartial(10)
182
+ end
183
+
184
+ def test_readpartial_with_tight_per_op_raises_when_read_timeout_fires
185
+ timeout = HTTP::Timeout::Global.new(global_timeout: 100, read_timeout: 0.01, write_timeout: 0.01,
186
+ connect_timeout: 0.01)
187
+ io_nil = fake(wait_readable: nil, wait_writable: true)
188
+ socket = fake(
189
+ to_io: io_nil,
190
+ closed?: false,
191
+ read_nonblock: :wait_readable
192
+ )
193
+ timeout.instance_variable_set(:@socket, socket)
194
+
195
+ err = assert_raises(HTTP::TimeoutError) { timeout.readpartial(10) }
196
+ assert_match(/Read timed out/, err.message)
197
+ end
198
+
199
+ def test_write_with_tight_per_op_raises_when_write_timeout_fires
200
+ timeout = HTTP::Timeout::Global.new(global_timeout: 100, read_timeout: 0.01, write_timeout: 0.01,
201
+ connect_timeout: 0.01)
202
+ io_nil = fake(wait_readable: true, wait_writable: nil)
203
+ socket = fake(
204
+ to_io: io_nil,
205
+ closed?: false,
206
+ write_nonblock: :wait_writable
207
+ )
208
+ timeout.instance_variable_set(:@socket, socket)
209
+
210
+ err = assert_raises(HTTP::TimeoutError) { timeout.write("data") }
211
+ assert_match(/Write timed out/, err.message)
212
+ end
213
+
214
+ def test_connect_ssl_with_tight_per_op_uses_connect_timeout_for_wait_readable
215
+ timeout = HTTP::Timeout::Global.new(global_timeout: 100, read_timeout: 0.01, write_timeout: 0.01,
216
+ connect_timeout: 0.01)
217
+ io_nil = fake(wait_readable: nil, wait_writable: true)
218
+ socket = fake(
219
+ to_io: io_nil,
220
+ closed?: false,
221
+ connect_nonblock: ->(*) { raise IO::EAGAINWaitReadable }
222
+ )
223
+ timeout.instance_variable_set(:@socket, socket)
224
+ assert_raises(HTTP::TimeoutError) { timeout.connect_ssl }
225
+ end
226
+
227
+ def test_connect_ssl_with_tight_per_op_uses_connect_timeout_for_wait_writable
228
+ timeout = HTTP::Timeout::Global.new(global_timeout: 100, read_timeout: 0.01, write_timeout: 0.01,
229
+ connect_timeout: 0.01)
230
+ io_nil = fake(wait_readable: true, wait_writable: nil)
231
+ socket = fake(
232
+ to_io: io_nil,
233
+ closed?: false,
234
+ connect_nonblock: ->(*) { raise IO::EAGAINWaitWritable }
235
+ )
236
+ timeout.instance_variable_set(:@socket, socket)
237
+ assert_raises(HTTP::TimeoutError) { timeout.connect_ssl }
238
+ end
239
+ end