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
data/test/http_test.rb ADDED
@@ -0,0 +1,818 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ require "json"
6
+
7
+ require "support/dummy_server"
8
+ require "support/proxy_server"
9
+
10
+ class HTTPTest < Minitest::Test
11
+ cover "HTTP::Chainable*"
12
+ run_server(:dummy) { DummyServer.new }
13
+ run_server(:dummy_ssl) { DummyServer.new(ssl: true) }
14
+
15
+ # getting resources
16
+
17
+ def test_getting_resources_is_easy
18
+ response = HTTP.get dummy.endpoint
19
+
20
+ assert_match(/<!doctype html>/, response.to_s)
21
+ end
22
+
23
+ def test_getting_resources_with_uri_instance
24
+ response = HTTP.get HTTP::URI.parse(dummy.endpoint)
25
+
26
+ assert_match(/<!doctype html>/, response.to_s)
27
+ end
28
+
29
+ def test_getting_resources_with_query_string_parameters
30
+ response = HTTP.get "#{dummy.endpoint}/params", params: { foo: "bar" }
31
+
32
+ assert_match(/Params!/, response.to_s)
33
+ end
34
+
35
+ def test_getting_resources_with_query_string_parameters_in_uri_and_opts
36
+ response = HTTP.get "#{dummy.endpoint}/multiple-params?foo=bar", params: { baz: "quux" }
37
+
38
+ assert_match(/More Params!/, response.to_s)
39
+ end
40
+
41
+ def test_getting_resources_with_two_leading_slashes_in_path
42
+ HTTP.get "#{dummy.endpoint}//"
43
+ end
44
+
45
+ def test_getting_resources_with_headers
46
+ response = HTTP.accept("application/json").get dummy.endpoint
47
+
48
+ assert_includes response.to_s, "json"
49
+ end
50
+
51
+ # getting resources with a large request body + timeout variants
52
+
53
+ [:null, 6, { read: 2, write: 2, connect: 2 }, { global: 6, read: 2, write: 2, connect: 2 }].each do |timeout|
54
+ define_method :"test_large_request_body_with_timeout_#{timeout.inspect}" do
55
+ request_body = "\xE2\x80\x9C" * 1_000_000
56
+ client = HTTP.timeout(timeout)
57
+ response = client.post "#{dummy.endpoint}/echo-body", body: request_body
58
+
59
+ assert_equal request_body.b, response.body.to_s
60
+ assert_equal request_body.bytesize, response.headers["Content-Length"].to_i
61
+ end
62
+ end
63
+
64
+ # with a block
65
+
66
+ def test_block_yields_the_response
67
+ HTTP.get(dummy.endpoint) do |response|
68
+ assert_match(/<!doctype html>/, response.to_s)
69
+ end
70
+ end
71
+
72
+ def test_block_returns_the_block_value
73
+ result = HTTP.get(dummy.endpoint) { |response| response.status.code }
74
+
75
+ assert_equal 200, result
76
+ end
77
+
78
+ def test_block_closes_the_connection_after_the_block
79
+ client = nil
80
+ HTTP.stub(:make_client, lambda { |opts|
81
+ client = HTTP::Client.new(opts)
82
+ original_close = client.method(:close)
83
+ client.define_singleton_method(:close) do
84
+ @test_closed = true
85
+ original_close.call
86
+ end
87
+ client.define_singleton_method(:test_closed?) { @test_closed == true }
88
+ client
89
+ }) do
90
+ HTTP.get(dummy.endpoint, &:status)
91
+ end
92
+
93
+ assert_predicate client, :test_closed?, "expected close to have been called"
94
+ end
95
+
96
+ def test_block_closes_the_connection_even_when_the_block_raises
97
+ client = nil
98
+
99
+ HTTP.stub(:make_client, lambda { |opts|
100
+ client = HTTP::Client.new(opts)
101
+ original_close = client.method(:close)
102
+ client.define_singleton_method(:close) do
103
+ @test_closed = true
104
+ original_close.call
105
+ end
106
+ client.define_singleton_method(:test_closed?) { @test_closed == true }
107
+ client
108
+ }) do
109
+ assert_raises(RuntimeError) do
110
+ HTTP.get(dummy.endpoint) { raise "boom" }
111
+ end
112
+ end
113
+
114
+ assert_predicate client, :test_closed?, "expected close to have been called on error"
115
+ end
116
+
117
+ def test_block_works_with_chained_options
118
+ result = HTTP.headers("Accept" => "application/json").get(dummy.endpoint) do |response|
119
+ response.status.code
120
+ end
121
+
122
+ assert_equal 200, result
123
+ end
124
+
125
+ def test_block_handles_nil_client_when_make_client_raises
126
+ HTTP.stub(:make_client, ->(*) { raise "boom" }) do
127
+ assert_raises(RuntimeError) { HTTP.get(dummy.endpoint) { nil } }
128
+ end
129
+ end
130
+
131
+ # .retry
132
+
133
+ def test_retry_ensures_endpoint_counts_retries
134
+ assert_equal "retried 1x", HTTP.get("#{dummy.endpoint}/retry-2").to_s
135
+ assert_equal "retried 2x", HTTP.get("#{dummy.endpoint}/retry-2").to_s
136
+ end
137
+
138
+ def test_retry_retries_the_request
139
+ response = HTTP.retriable(delay: 0, retry_statuses: 500...600).get "#{dummy.endpoint}/retry-2"
140
+
141
+ assert_equal "retried 2x", response.to_s
142
+ end
143
+
144
+ def test_retry_retries_the_request_and_gives_access_to_failed_requests
145
+ err = nil
146
+ retry_callback = ->(_, _, res) { assert_match(/^retried \dx$/, res.to_s) }
147
+ begin
148
+ HTTP.retriable(
149
+ should_retry: ->(*) { true },
150
+ tries: 3,
151
+ delay: 0,
152
+ on_retry: retry_callback
153
+ ).get "#{dummy.endpoint}/retry-2"
154
+ rescue HTTP::Error => e
155
+ err = e
156
+ end
157
+
158
+ assert_equal "retried 3x", err.response.to_s
159
+ end
160
+
161
+ # posting forms to resources
162
+
163
+ def test_posting_forms_is_easy
164
+ response = HTTP.post "#{dummy.endpoint}/form", form: { example: "testing-form" }
165
+
166
+ assert_equal "passed :)", response.to_s
167
+ end
168
+
169
+ # loading binary data
170
+
171
+ def test_binary_data_is_encoded_as_bytes
172
+ response = HTTP.get "#{dummy.endpoint}/bytes"
173
+
174
+ assert_equal Encoding::BINARY, response.to_s.encoding
175
+ end
176
+
177
+ # loading endpoint with charset
178
+
179
+ def test_charset_uses_charset_from_headers
180
+ response = HTTP.get "#{dummy.endpoint}/iso-8859-1"
181
+
182
+ assert_equal Encoding::ISO8859_1, response.to_s.encoding
183
+ assert_equal "testæ", response.to_s.encode(Encoding::UTF_8)
184
+ end
185
+
186
+ def test_charset_with_encoding_option_respects_option
187
+ response = HTTP.get "#{dummy.endpoint}/iso-8859-1", encoding: Encoding::BINARY
188
+
189
+ assert_equal Encoding::BINARY, response.to_s.encoding
190
+ end
191
+
192
+ # passing a string encoding type
193
+
194
+ def test_string_encoding_type_finds_encoding
195
+ response = HTTP.get dummy.endpoint, encoding: "ascii"
196
+
197
+ assert_equal Encoding::ASCII, response.to_s.encoding
198
+ end
199
+
200
+ # loading text with no charset
201
+
202
+ def test_text_with_no_charset_is_binary_encoded
203
+ response = HTTP.get dummy.endpoint
204
+
205
+ assert_equal Encoding::BINARY, response.to_s.encoding
206
+ end
207
+
208
+ # posting with an explicit body
209
+
210
+ def test_posting_with_explicit_body_is_easy
211
+ response = HTTP.post "#{dummy.endpoint}/body", body: "testing-body"
212
+
213
+ assert_equal "passed :)", response.to_s
214
+ end
215
+
216
+ # with redirects
217
+
218
+ def test_redirects_is_easy_for_301
219
+ response = HTTP.follow.get("#{dummy.endpoint}/redirect-301")
220
+
221
+ assert_match(/<!doctype html>/, response.to_s)
222
+ end
223
+
224
+ def test_redirects_is_easy_for_302
225
+ response = HTTP.follow.get("#{dummy.endpoint}/redirect-302")
226
+
227
+ assert_match(/<!doctype html>/, response.to_s)
228
+ end
229
+
230
+ # head requests
231
+
232
+ def test_head_request_is_easy
233
+ response = HTTP.head dummy.endpoint
234
+
235
+ assert_equal 200, response.status.to_i
236
+ assert_match(/html/, response.headers["content-type"])
237
+ end
238
+
239
+ # .auth
240
+
241
+ def test_auth_sets_authorization_header
242
+ client = HTTP.auth "abc"
243
+
244
+ assert_equal "abc", client.default_options.headers[:authorization]
245
+ end
246
+
247
+ def test_auth_accepts_any_to_s_object
248
+ client = HTTP.auth fake(to_s: "abc")
249
+
250
+ assert_equal "abc", client.default_options.headers[:authorization]
251
+ end
252
+
253
+ # .basic_auth
254
+
255
+ def test_basic_auth_fails_when_pass_is_not_given
256
+ assert_raises(ArgumentError) { HTTP.basic_auth(user: "[USER]") }
257
+ end
258
+
259
+ def test_basic_auth_fails_when_user_is_not_given
260
+ assert_raises(ArgumentError) { HTTP.basic_auth(pass: "[PASS]") }
261
+ end
262
+
263
+ def test_basic_auth_sets_authorization_header
264
+ client = HTTP.basic_auth user: "foo", pass: "bar"
265
+
266
+ assert_match(%r{^Basic [A-Za-z0-9+/]+=*$}, client.default_options.headers[:authorization])
267
+ end
268
+
269
+ # .base_uri
270
+
271
+ def test_base_uri_resolves_relative_paths
272
+ response = HTTP.base_uri(dummy.endpoint).get("/")
273
+
274
+ assert_match(/<!doctype html>/, response.to_s)
275
+ end
276
+
277
+ def test_base_uri_resolves_paths_without_leading_slash
278
+ response = HTTP.base_uri(dummy.endpoint).get("params?foo=bar")
279
+
280
+ assert_match(/Params!/, response.to_s)
281
+ end
282
+
283
+ def test_base_uri_ignores_base_uri_for_absolute_urls
284
+ response = HTTP.base_uri("https://other.example.com").get(dummy.endpoint)
285
+
286
+ assert_match(/<!doctype html>/, response.to_s)
287
+ end
288
+
289
+ def test_base_uri_chains_base_uris
290
+ session = HTTP.base_uri("https://example.com").base_uri("api/v1")
291
+
292
+ assert_equal "https://example.com/api/v1", session.default_options.base_uri.to_s
293
+ end
294
+
295
+ def test_base_uri_works_with_other_chainable_methods
296
+ response = HTTP.base_uri(dummy.endpoint)
297
+ .headers("Accept" => "application/json")
298
+ .get("/")
299
+
300
+ assert_includes response.to_s, "json"
301
+ end
302
+
303
+ def test_base_uri_raises_for_uri_without_scheme
304
+ assert_raises(HTTP::Error) { HTTP.base_uri("/users") }
305
+ end
306
+
307
+ def test_base_uri_derives_persistent_host_from_base_uri
308
+ p_client = HTTP.base_uri(dummy.endpoint).persistent
309
+
310
+ assert_predicate p_client, :persistent?
311
+ ensure
312
+ p_client&.close
313
+ end
314
+
315
+ def test_base_uri_raises_when_persistent_host_not_given_and_no_base_uri
316
+ assert_raises(ArgumentError) { HTTP.persistent }
317
+ end
318
+
319
+ # .persistent
320
+
321
+ def test_persistent_with_host_returns_http_session
322
+ persistent_client = HTTP.persistent dummy.endpoint
323
+
324
+ assert_kind_of HTTP::Session, persistent_client
325
+ end
326
+
327
+ def test_persistent_with_host_is_persistent
328
+ persistent_client = HTTP.persistent dummy.endpoint
329
+
330
+ assert_predicate persistent_client, :persistent?
331
+ end
332
+
333
+ def test_persistent_with_block_returns_last_expression
334
+ assert_equal :http, HTTP.persistent(dummy.endpoint) { :http }
335
+ end
336
+
337
+ def test_persistent_with_block_auto_closes_connection
338
+ closed = false
339
+ HTTP.persistent dummy.endpoint do |session|
340
+ original_close = session.method(:close)
341
+ session.define_singleton_method(:close) do
342
+ closed = true
343
+ original_close.call
344
+ end
345
+ session.get("/")
346
+ end
347
+
348
+ assert closed, "expected close to have been called"
349
+ end
350
+
351
+ def test_persistent_when_initialization_raises_handles_nil_session
352
+ opts = HTTP.default_options
353
+
354
+ opts.stub(:merge, ->(*) { raise "boom" }) do
355
+ assert_raises(RuntimeError) { HTTP.persistent(dummy.endpoint) { nil } }
356
+ end
357
+ end
358
+
359
+ def test_persistent_with_timeout_sets_keep_alive_timeout
360
+ persistent_client = HTTP.persistent dummy.endpoint, timeout: 100
361
+ options = persistent_client.default_options
362
+
363
+ assert_equal 100, options.keep_alive_timeout
364
+ end
365
+
366
+ # .timeout
367
+
368
+ def test_timeout_null_sets_timeout_class_to_null
369
+ client = HTTP.timeout :null
370
+
371
+ assert_equal HTTP::Timeout::Null, client.default_options.timeout_class
372
+ end
373
+
374
+ def test_timeout_per_operation_sets_timeout_class
375
+ client = HTTP.timeout read: 123
376
+
377
+ assert_equal HTTP::Timeout::PerOperation, client.default_options.timeout_class
378
+ end
379
+
380
+ def test_timeout_per_operation_sets_timeout_options
381
+ client = HTTP.timeout read: 123
382
+
383
+ assert_equal({ read_timeout: 123 }, client.default_options.timeout_options)
384
+ end
385
+
386
+ def test_timeout_per_operation_long_form_keys
387
+ client = HTTP.timeout read_timeout: 123
388
+
389
+ assert_equal({ read_timeout: 123 }, client.default_options.timeout_options)
390
+ end
391
+
392
+ def test_timeout_all_per_operation_sets_all_options
393
+ client = HTTP.timeout read: 1, write: 2, connect: 3
394
+
395
+ assert_equal({ read_timeout: 1, write_timeout: 2, connect_timeout: 3 }, client.default_options.timeout_options)
396
+ end
397
+
398
+ def test_timeout_per_operation_frozen_hash_does_not_raise
399
+ frozen_options = { read: 123 }.freeze
400
+ HTTP.timeout(frozen_options)
401
+ end
402
+
403
+ def test_timeout_empty_hash_raises_argument_error
404
+ assert_raises(ArgumentError) { HTTP.timeout({}) }
405
+ end
406
+
407
+ def test_timeout_unknown_key_raises_argument_error
408
+ assert_raises(ArgumentError) { HTTP.timeout(timeout: 2) }
409
+ end
410
+
411
+ def test_timeout_both_short_and_long_form_raises_argument_error
412
+ assert_raises(ArgumentError) { HTTP.timeout(read: 2, read_timeout: 2) }
413
+ end
414
+
415
+ def test_timeout_non_numeric_value_raises_argument_error
416
+ assert_raises(ArgumentError) { HTTP.timeout(read: "2") }
417
+ end
418
+
419
+ def test_timeout_string_keys_raises_argument_error
420
+ assert_raises(ArgumentError) { HTTP.timeout("read" => 2) }
421
+ end
422
+
423
+ def test_timeout_global_as_hash_key_sets_timeout_class
424
+ client = HTTP.timeout global: 60
425
+
426
+ assert_equal HTTP::Timeout::Global, client.default_options.timeout_class
427
+ end
428
+
429
+ def test_timeout_global_as_hash_key_sets_timeout_option
430
+ client = HTTP.timeout global: 60
431
+
432
+ assert_equal({ global_timeout: 60 }, client.default_options.timeout_options)
433
+ end
434
+
435
+ def test_timeout_global_long_form_sets_timeout_class
436
+ client = HTTP.timeout global_timeout: 60
437
+
438
+ assert_equal HTTP::Timeout::Global, client.default_options.timeout_class
439
+ end
440
+
441
+ def test_timeout_global_long_form_sets_timeout_option
442
+ client = HTTP.timeout global_timeout: 60
443
+
444
+ assert_equal({ global_timeout: 60 }, client.default_options.timeout_options)
445
+ end
446
+
447
+ def test_timeout_combined_global_and_per_operation_sets_timeout_class
448
+ client = HTTP.timeout global: 60, read: 30, write: 20, connect: 5
449
+
450
+ assert_equal HTTP::Timeout::Global, client.default_options.timeout_class
451
+ end
452
+
453
+ def test_timeout_combined_global_and_per_operation_sets_all_options
454
+ client = HTTP.timeout global: 60, read: 30, write: 20, connect: 5
455
+ expected = { read_timeout: 30, write_timeout: 20, connect_timeout: 5, global_timeout: 60 }
456
+
457
+ assert_equal expected, client.default_options.timeout_options
458
+ end
459
+
460
+ def test_timeout_combined_global_and_partial_per_operation_sets_timeout_class
461
+ client = HTTP.timeout global: 60, read: 30
462
+
463
+ assert_equal HTTP::Timeout::Global, client.default_options.timeout_class
464
+ end
465
+
466
+ def test_timeout_combined_global_and_partial_per_operation_includes_both
467
+ client = HTTP.timeout global: 60, read: 30
468
+ expected = { read_timeout: 30, global_timeout: 60 }
469
+
470
+ assert_equal expected, client.default_options.timeout_options
471
+ end
472
+
473
+ def test_timeout_both_short_and_long_form_of_global_raises_argument_error
474
+ assert_raises(ArgumentError) { HTTP.timeout(global: 60, global_timeout: 60) }
475
+ end
476
+
477
+ def test_timeout_non_numeric_global_raises_argument_error
478
+ assert_raises(ArgumentError) { HTTP.timeout(global: "60") }
479
+ end
480
+
481
+ def test_timeout_global_numeric_sets_timeout_class
482
+ client = HTTP.timeout 123
483
+
484
+ assert_equal HTTP::Timeout::Global, client.default_options.timeout_class
485
+ end
486
+
487
+ def test_timeout_global_numeric_sets_timeout_option
488
+ client = HTTP.timeout 123
489
+
490
+ assert_equal({ global_timeout: 123 }, client.default_options.timeout_options)
491
+ end
492
+
493
+ def test_timeout_float_global_sets_timeout_option
494
+ client = HTTP.timeout 2.5
495
+
496
+ assert_equal({ global_timeout: 2.5 }, client.default_options.timeout_options)
497
+ end
498
+
499
+ def test_timeout_unsupported_options_raises_argument_error
500
+ assert_raises(ArgumentError) { HTTP.timeout("invalid") }
501
+ end
502
+
503
+ # .cookies
504
+
505
+ def test_cookies_passes_correct_cookie_header
506
+ endpoint = "#{dummy.endpoint}/cookies"
507
+
508
+ assert_equal "abc: def", HTTP.cookies(abc: :def).get(endpoint).to_s
509
+ end
510
+
511
+ def test_cookies_properly_works_with_cookies_from_response
512
+ endpoint = "#{dummy.endpoint}/cookies"
513
+ res = HTTP.get(endpoint).flush
514
+
515
+ assert_equal "foo: bar", HTTP.cookies(res.cookies).get(endpoint).to_s
516
+ end
517
+
518
+ def test_cookies_replaces_previously_set_cookies
519
+ endpoint = "#{dummy.endpoint}/cookies"
520
+ client = HTTP.cookies(foo: 123, bar: 321).cookies(baz: :moo)
521
+
522
+ assert_equal "baz: moo", client.get(endpoint).to_s
523
+ end
524
+
525
+ # .nodelay
526
+
527
+ def test_nodelay_sets_tcp_nodelay_on_underlying_socket
528
+ socket_spy_class = Class.new(TCPSocket) do
529
+ def self.setsockopt_calls
530
+ @setsockopt_calls ||= []
531
+ end
532
+
533
+ def setsockopt(*args)
534
+ self.class.setsockopt_calls << args
535
+ super
536
+ end
537
+ end
538
+
539
+ HTTP.default_options = { socket_class: socket_spy_class }
540
+
541
+ HTTP.get(dummy.endpoint)
542
+
543
+ assert_equal [], socket_spy_class.setsockopt_calls
544
+ HTTP.nodelay.get(dummy.endpoint)
545
+
546
+ assert_equal [[Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1]], socket_spy_class.setsockopt_calls
547
+ ensure
548
+ HTTP.default_options = {}
549
+ end
550
+
551
+ # .use
552
+
553
+ def test_use_turns_on_given_feature
554
+ client = HTTP.use :auto_deflate
555
+
556
+ assert_equal [:auto_deflate], client.default_options.features.keys
557
+ end
558
+
559
+ def test_use_auto_deflate_sends_gzipped_body
560
+ client = HTTP.use :auto_deflate
561
+ body = "Hello!"
562
+ response = client.post("#{dummy.endpoint}/echo-body", body: body)
563
+ encoded = response.to_s
564
+
565
+ assert_equal body, Zlib::GzipReader.new(StringIO.new(encoded)).read
566
+ end
567
+
568
+ def test_use_auto_deflate_sends_deflated_body
569
+ client = HTTP.use auto_deflate: { method: "deflate" }
570
+ body = "Hello!"
571
+ response = client.post("#{dummy.endpoint}/echo-body", body: body)
572
+ encoded = response.to_s
573
+
574
+ assert_equal body, Zlib::Inflate.inflate(encoded)
575
+ end
576
+
577
+ def test_use_auto_inflate_returns_raw_body_when_content_encoding_missing
578
+ client = HTTP.use :auto_inflate
579
+ body = "Hello!"
580
+ response = client.post("#{dummy.endpoint}/encoded-body", body: body)
581
+
582
+ assert_equal "#{body}-raw", response.to_s
583
+ end
584
+
585
+ def test_use_auto_inflate_returns_decoded_body
586
+ client = HTTP.use(:auto_inflate).headers("Accept-Encoding" => "gzip")
587
+ body = "Hello!"
588
+ response = client.post("#{dummy.endpoint}/encoded-body", body: body)
589
+
590
+ assert_equal "#{body}-gzipped", response.to_s
591
+ end
592
+
593
+ def test_use_auto_inflate_returns_deflated_body
594
+ client = HTTP.use(:auto_inflate).headers("Accept-Encoding" => "deflate")
595
+ body = "Hello!"
596
+ response = client.post("#{dummy.endpoint}/encoded-body", body: body)
597
+
598
+ assert_equal "#{body}-deflated", response.to_s
599
+ end
600
+
601
+ def test_use_auto_inflate_returns_empty_body_for_204_with_gzip
602
+ client = HTTP.use(:auto_inflate).headers("Accept-Encoding" => "gzip")
603
+ body = "Hello!"
604
+ response = client.post("#{dummy.endpoint}/no-content-204", body: body)
605
+
606
+ assert_equal "", response.to_s
607
+ end
608
+
609
+ def test_use_auto_inflate_returns_empty_body_for_204_with_deflate
610
+ client = HTTP.use(:auto_inflate).headers("Accept-Encoding" => "deflate")
611
+ body = "Hello!"
612
+ response = client.post("#{dummy.endpoint}/no-content-204", body: body)
613
+
614
+ assert_equal "", response.to_s
615
+ end
616
+
617
+ def test_use_normalize_uri_normalizes_uri
618
+ response = HTTP.get "#{dummy.endpoint}/héllö-wörld"
619
+
620
+ assert_equal "hello world", response.to_s
621
+ end
622
+
623
+ def test_use_normalize_uri_uses_custom_normalizer
624
+ client = HTTP.use(normalize_uri: { normalizer: :itself.to_proc })
625
+ response = client.get("#{dummy.endpoint}/héllö-wörld")
626
+
627
+ assert_equal 400, response.status.to_i
628
+ end
629
+
630
+ def test_use_normalize_uri_raises_if_custom_normalizer_returns_invalid_path
631
+ client = HTTP.use(normalize_uri: { normalizer: :itself.to_proc })
632
+ err = assert_raises(HTTP::RequestError) { client.get("#{dummy.endpoint}/hello\nworld") }
633
+ assert_equal 'Invalid request URI: "/hello\nworld"', err.message
634
+ end
635
+
636
+ def test_use_normalize_uri_raises_if_custom_normalizer_returns_invalid_host
637
+ normalizer = lambda do |uri|
638
+ uri.instance_variable_set(:@host, "example\ncom")
639
+ uri
640
+ end
641
+ client = HTTP.use(normalize_uri: { normalizer: normalizer })
642
+ err = assert_raises(HTTP::RequestError) { client.get(dummy.endpoint) }
643
+ assert_match(/Invalid host: "example\\ncom/, err.message)
644
+ end
645
+
646
+ def test_use_normalize_uri_uses_default_normalizer
647
+ client = HTTP.use :normalize_uri
648
+ response = client.get("#{dummy.endpoint}/héllö-wörld")
649
+
650
+ assert_equal "hello world", response.to_s
651
+ end
652
+
653
+ # dynamic verb tests
654
+
655
+ %i[put delete trace options connect patch].each do |verb|
656
+ define_method :"test_#{verb}_delegates_to_request" do
657
+ mock_client = Minitest::Mock.new
658
+ mock_client.expect(:request, nil, [verb, "http://example.com/"])
659
+ HTTP::Client.stub(:new, mock_client) do
660
+ HTTP.public_send(verb, "http://example.com/")
661
+ end
662
+ mock_client.verify
663
+ end
664
+ end
665
+
666
+ # Request::Builder
667
+
668
+ def test_request_builder_builds_http_request_from_options
669
+ options = HTTP::Options.new
670
+ builder = HTTP::Request::Builder.new(options)
671
+ req = builder.build(:get, "http://example.com/")
672
+
673
+ assert_kind_of HTTP::Request, req
674
+ end
675
+
676
+ # .encoding
677
+
678
+ def test_encoding_returns_session_with_specified_encoding
679
+ session = HTTP::Client.new.encoding("UTF-8")
680
+
681
+ assert_kind_of HTTP::Session, session
682
+ end
683
+
684
+ # .via - proxy_headers tests (no actual proxy server needed)
685
+
686
+ def test_via_with_proxy_headers_as_third_argument
687
+ client = HTTP.via("proxy.example.com", 8080, { "X-Custom" => "val" })
688
+ proxy = client.default_options.proxy
689
+
690
+ assert_equal({ "X-Custom" => "val" }, proxy[:proxy_headers])
691
+ end
692
+
693
+ def test_via_with_proxy_headers_as_fifth_argument
694
+ hdrs = { "X-Custom" => "val" }
695
+ client = HTTP.via("proxy.example.com", 8080, "user", "pass", hdrs)
696
+ proxy = client.default_options.proxy
697
+
698
+ assert_equal({ "X-Custom" => "val" }, proxy[:proxy_headers])
699
+ end
700
+
701
+ def test_via_with_non_string_first_argument_skips_proxy_address
702
+ client = HTTP.via(nil, 8080, { "X-Custom" => "val" })
703
+ proxy = client.default_options.proxy
704
+
705
+ refute proxy.key?(:proxy_address)
706
+ end
707
+
708
+ # socket error unification
709
+
710
+ def test_unifies_socket_errors_into_http_connection_error
711
+ original_open = TCPSocket.method(:open)
712
+ stub_open = lambda do |*args|
713
+ raise SocketError if args[0] == "thishostshouldnotexists.com"
714
+
715
+ original_open.call(*args)
716
+ end
717
+ TCPSocket.stub(:open, stub_open) do
718
+ assert_raises(HTTP::ConnectionError) { HTTP.get "http://thishostshouldnotexists.com" }
719
+ assert_raises(HTTP::ConnectionError) { HTTP.get "http://127.0.0.1:111" }
720
+ end
721
+ end
722
+ end
723
+
724
+ class HTTPViaAnonymousProxyTest < Minitest::Test
725
+ run_server(:dummy) { DummyServer.new }
726
+ run_server(:dummy_ssl) { DummyServer.new(ssl: true) }
727
+ run_server(:proxy) { ProxyServer.new }
728
+
729
+ def ssl_client
730
+ HTTP::Client.new ssl_context: SSLHelper.client_context
731
+ end
732
+
733
+ def test_anonymous_proxy_proxies_the_request
734
+ response = HTTP.via(proxy.addr, proxy.port).get dummy.endpoint
735
+
736
+ assert_equal "true", response.headers["X-Proxied"]
737
+ end
738
+
739
+ def test_anonymous_proxy_responds_with_endpoint_body
740
+ response = HTTP.via(proxy.addr, proxy.port).get dummy.endpoint
741
+
742
+ assert_match(/<!doctype html>/, response.to_s)
743
+ end
744
+
745
+ def test_anonymous_proxy_raises_argument_error_if_no_port_given
746
+ assert_raises(HTTP::RequestError) { HTTP.via(proxy.addr) }
747
+ end
748
+
749
+ def test_anonymous_proxy_ignores_credentials
750
+ response = HTTP.via(proxy.addr, proxy.port, "username", "password").get dummy.endpoint
751
+
752
+ assert_match(/<!doctype html>/, response.to_s)
753
+ end
754
+
755
+ def test_anonymous_proxy_ssl_responds_with_endpoint_body
756
+ response = ssl_client.via(proxy.addr, proxy.port).get dummy_ssl.endpoint
757
+
758
+ assert_match(/<!doctype html>/, response.to_s)
759
+ end
760
+
761
+ def test_anonymous_proxy_ssl_ignores_credentials
762
+ response = ssl_client.via(proxy.addr, proxy.port, "username", "password").get dummy_ssl.endpoint
763
+
764
+ assert_match(/<!doctype html>/, response.to_s)
765
+ end
766
+ end
767
+
768
+ class HTTPViaAuthProxyTest < Minitest::Test
769
+ run_server(:dummy) { DummyServer.new }
770
+ run_server(:dummy_ssl) { DummyServer.new(ssl: true) }
771
+ run_server(:proxy) { AuthProxyServer.new }
772
+
773
+ def ssl_client
774
+ HTTP::Client.new ssl_context: SSLHelper.client_context
775
+ end
776
+
777
+ def test_auth_proxy_proxies_the_request
778
+ response = HTTP.via(proxy.addr, proxy.port, "username", "password").get dummy.endpoint
779
+
780
+ assert_equal "true", response.headers["X-Proxied"]
781
+ end
782
+
783
+ def test_auth_proxy_responds_with_endpoint_body
784
+ response = HTTP.via(proxy.addr, proxy.port, "username", "password").get dummy.endpoint
785
+
786
+ assert_match(/<!doctype html>/, response.to_s)
787
+ end
788
+
789
+ def test_auth_proxy_responds_with_407_when_wrong_credentials
790
+ response = HTTP.via(proxy.addr, proxy.port, "user", "pass").get dummy.endpoint
791
+
792
+ assert_equal 407, response.status.to_i
793
+ end
794
+
795
+ def test_auth_proxy_responds_with_407_if_no_credentials
796
+ response = HTTP.via(proxy.addr, proxy.port).get dummy.endpoint
797
+
798
+ assert_equal 407, response.status.to_i
799
+ end
800
+
801
+ def test_auth_proxy_ssl_responds_with_endpoint_body
802
+ response = ssl_client.via(proxy.addr, proxy.port, "username", "password").get dummy_ssl.endpoint
803
+
804
+ assert_match(/<!doctype html>/, response.to_s)
805
+ end
806
+
807
+ def test_auth_proxy_ssl_responds_with_407_when_wrong_credentials
808
+ response = ssl_client.via(proxy.addr, proxy.port, "user", "pass").get dummy_ssl.endpoint
809
+
810
+ assert_equal 407, response.status.to_i
811
+ end
812
+
813
+ def test_auth_proxy_ssl_responds_with_407_if_no_credentials
814
+ response = ssl_client.via(proxy.addr, proxy.port).get dummy_ssl.endpoint
815
+
816
+ assert_equal 407, response.status.to_i
817
+ end
818
+ end