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,739 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ require "uri"
6
+ require "logger"
7
+
8
+ require "support/http_handling_shared"
9
+ require "support/dummy_server"
10
+ require "support/ssl_helper"
11
+
12
+ StubbedClient = Class.new(HTTP::Client) do
13
+ def perform(request, options)
14
+ stubbed = stubs[HTTP::URI::NORMALIZER.call(request.uri).to_s]
15
+ stubbed ? stubbed.call(request) : super
16
+ end
17
+
18
+ def stubs
19
+ @stubs ||= {}
20
+ end
21
+
22
+ def stub(stubs)
23
+ @stubs = stubs.transform_keys do |k|
24
+ HTTP::URI::NORMALIZER.call(k).to_s
25
+ end
26
+
27
+ self
28
+ end
29
+ end
30
+
31
+ class HTTPClientTest < Minitest::Test
32
+ cover "HTTP::Client*"
33
+ run_server(:dummy) { DummyServer.new }
34
+
35
+ def capture_request(client, &)
36
+ captured_req = nil
37
+ client.stub(:perform, lambda { |req, _opts|
38
+ captured_req = req
39
+ nil
40
+ }, &)
41
+ captured_req
42
+ end
43
+
44
+ def redirect_response(location, status = 302)
45
+ lambda do |request|
46
+ HTTP::Response.new(
47
+ status: status,
48
+ version: "1.1",
49
+ headers: { "Location" => location },
50
+ body: "",
51
+ request: request
52
+ )
53
+ end
54
+ end
55
+
56
+ def simple_response(body, status = 200)
57
+ lambda do |request|
58
+ HTTP::Response.new(
59
+ status: status,
60
+ version: "1.1",
61
+ body: body,
62
+ request: request
63
+ )
64
+ end
65
+ end
66
+
67
+ def client
68
+ @client ||= HTTP::Client.new
69
+ end
70
+
71
+ def parse_query(str)
72
+ URI.decode_www_form(str).group_by(&:first).transform_values { |v| v.map(&:last) }
73
+ end
74
+
75
+ # following redirects
76
+
77
+ def test_following_redirects_returns_response_of_new_location
78
+ client = StubbedClient.new(follow: true).stub(
79
+ "http://example.com/" => redirect_response("http://example.com/blog"),
80
+ "http://example.com/blog" => simple_response("OK")
81
+ )
82
+
83
+ assert_equal "OK", client.get("http://example.com/").to_s
84
+ end
85
+
86
+ def test_following_redirects_prepends_previous_request_uri_scheme_and_host_if_needed
87
+ client = StubbedClient.new(follow: true).stub(
88
+ "http://example.com/" => redirect_response("/index"),
89
+ "http://example.com/index" => redirect_response("/index.html"),
90
+ "http://example.com/index.html" => simple_response("OK")
91
+ )
92
+
93
+ assert_equal "OK", client.get("http://example.com/").to_s
94
+ end
95
+
96
+ def test_following_redirects_fails_upon_endless_redirects
97
+ client = StubbedClient.new(follow: true).stub(
98
+ "http://example.com/" => redirect_response("/")
99
+ )
100
+
101
+ assert_raises(HTTP::Redirector::EndlessRedirectError) { client.get("http://example.com/") }
102
+ end
103
+
104
+ def test_following_redirects_fails_if_max_amount_of_hops_reached
105
+ client = StubbedClient.new(follow: { max_hops: 5 }).stub(
106
+ "http://example.com/" => redirect_response("/1"),
107
+ "http://example.com/1" => redirect_response("/2"),
108
+ "http://example.com/2" => redirect_response("/3"),
109
+ "http://example.com/3" => redirect_response("/4"),
110
+ "http://example.com/4" => redirect_response("/5"),
111
+ "http://example.com/5" => redirect_response("/6"),
112
+ "http://example.com/6" => simple_response("OK")
113
+ )
114
+
115
+ assert_raises(HTTP::Redirector::TooManyRedirectsError) { client.get("http://example.com/") }
116
+ end
117
+
118
+ def test_following_redirects_with_non_ascii_urls_theoretically_works_like_a_charm
119
+ client = StubbedClient.new(follow: true).stub(
120
+ "http://example.com/" => redirect_response("/könig"),
121
+ "http://example.com/könig" => simple_response("OK")
122
+ )
123
+
124
+ client.get "http://example.com/könig"
125
+ end
126
+
127
+ def test_following_redirects_with_non_ascii_urls_follows_redirects
128
+ client = StubbedClient.new(follow: true).stub(
129
+ "http://example.com/" => redirect_response("/könig"),
130
+ "http://example.com/könig" => simple_response("OK")
131
+ )
132
+
133
+ assert_equal "OK", client.get("http://example.com/").to_s
134
+ end
135
+
136
+ # following redirects with logging
137
+
138
+ def test_following_redirects_with_logging_logs_all_requests
139
+ logdev = StringIO.new
140
+ logger = Logger.new(logdev)
141
+ logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
142
+ logger.level = Logger::INFO
143
+
144
+ client = StubbedClient.new(follow: true, features: { logging: { logger: logger } }).stub(
145
+ "http://example.com/" => redirect_response("/1"),
146
+ "http://example.com/1" => redirect_response("/2"),
147
+ "http://example.com/2" => redirect_response("/3"),
148
+ "http://example.com/3" => simple_response("OK")
149
+ )
150
+
151
+ client.get("http://example.com/")
152
+
153
+ assert_equal <<~OUTPUT, logdev.string
154
+ ** INFO **
155
+ > GET http://example.com/
156
+ ** INFO **
157
+ > GET http://example.com/1
158
+ ** INFO **
159
+ > GET http://example.com/2
160
+ ** INFO **
161
+ > GET http://example.com/3
162
+ OUTPUT
163
+ end
164
+
165
+ # base_uri
166
+
167
+ def test_base_uri_resolves_relative_paths_against_base_uri
168
+ client = StubbedClient.new(base_uri: "https://example.com/api").stub(
169
+ "https://example.com/api/users" => simple_response("OK")
170
+ )
171
+
172
+ assert_equal "OK", client.get("users").to_s
173
+ end
174
+
175
+ def test_base_uri_resolves_absolute_paths_from_host_root
176
+ client = StubbedClient.new(base_uri: "https://example.com/api").stub(
177
+ "https://example.com/users" => simple_response("OK")
178
+ )
179
+
180
+ assert_equal "OK", client.get("/users").to_s
181
+ end
182
+
183
+ def test_base_uri_ignores_base_uri_for_absolute_urls
184
+ client = StubbedClient.new(base_uri: "https://example.com/api").stub(
185
+ "https://other.com/path" => simple_response("OK")
186
+ )
187
+
188
+ assert_equal "OK", client.get("https://other.com/path").to_s
189
+ end
190
+
191
+ def test_base_uri_handles_parent_path_traversal
192
+ client = StubbedClient.new(base_uri: "https://example.com/api/v1").stub(
193
+ "https://example.com/api/v2" => simple_response("OK")
194
+ )
195
+
196
+ assert_equal "OK", client.get("../v2").to_s
197
+ end
198
+
199
+ def test_base_uri_handles_base_uri_without_trailing_slash
200
+ client = StubbedClient.new(base_uri: "https://example.com/api").stub(
201
+ "https://example.com/api/users" => simple_response("OK")
202
+ )
203
+
204
+ assert_equal "OK", client.get("users").to_s
205
+ end
206
+
207
+ def test_base_uri_handles_base_uri_with_trailing_slash
208
+ client = StubbedClient.new(base_uri: "https://example.com/api/").stub(
209
+ "https://example.com/api/users" => simple_response("OK")
210
+ )
211
+
212
+ assert_equal "OK", client.get("users").to_s
213
+ end
214
+
215
+ # parsing params
216
+
217
+ def test_parsing_params_accepts_params_within_the_provided_url
218
+ req = capture_request(client) { client.get("http://example.com/?foo=bar") }
219
+
220
+ assert_equal({ "foo" => %w[bar] }, parse_query(req.uri.query))
221
+ end
222
+
223
+ def test_parsing_params_combines_get_params_from_the_uri_with_the_passed_in_params
224
+ req = capture_request(client) { client.get("http://example.com/?foo=bar", params: { baz: "quux" }) }
225
+
226
+ assert_equal({ "foo" => %w[bar], "baz" => %w[quux] }, parse_query(req.uri.query))
227
+ end
228
+
229
+ def test_parsing_params_merges_duplicate_values
230
+ req = capture_request(client) { client.get("http://example.com/?a=1", params: { a: 2 }) }
231
+
232
+ assert_match(/^(a=1&a=2|a=2&a=1)$/, req.uri.query)
233
+ end
234
+
235
+ def test_parsing_params_does_not_modify_query_part_if_no_params_were_given
236
+ req = capture_request(client) { client.get("http://example.com/?deadbeef") }
237
+
238
+ assert_equal "deadbeef", req.uri.query
239
+ end
240
+
241
+ def test_parsing_params_does_not_corrupt_index_less_arrays
242
+ req = capture_request(client) { client.get("http://example.com/?a[]=b&a[]=c", params: { d: "e" }) }
243
+
244
+ assert_equal({ "a[]" => %w[b c], "d" => %w[e] }, parse_query(req.uri.query))
245
+ end
246
+
247
+ def test_parsing_params_properly_encodes_colons
248
+ req = capture_request(client) { client.get("http://example.com/", params: { t: "1970-01-01T00:00:00Z" }) }
249
+
250
+ assert_equal "t=1970-01-01T00%3A00%3A00Z", req.uri.query
251
+ end
252
+
253
+ def test_parsing_params_does_not_convert_newlines_into_crlf_before_encoding_string_values
254
+ req = capture_request(client) { client.get("http://example.com/", params: { foo: "bar\nbaz" }) }
255
+
256
+ assert_equal "foo=bar%0Abaz", req.uri.query
257
+ end
258
+
259
+ # passing multipart form data
260
+
261
+ def test_passing_multipart_form_data_creates_url_encoded_form_data_object
262
+ req = capture_request(client) { client.get("http://example.com/", form: { foo: "bar" }) }
263
+
264
+ assert_kind_of HTTP::FormData::Urlencoded, req.body.source
265
+ assert_equal "foo=bar", req.body.source.to_s
266
+ end
267
+
268
+ def test_passing_multipart_form_data_creates_multipart_form_data_object
269
+ req = capture_request(client) { client.get("http://example.com/", form: { foo: HTTP::FormData::Part.new("content") }) }
270
+
271
+ assert_kind_of HTTP::FormData::Multipart, req.body.source
272
+ assert_includes req.body.source.to_s, "content"
273
+ end
274
+
275
+ def test_passing_multipart_form_data_with_multipart_object_passes_it_through_unchanged
276
+ form_data = HTTP::FormData::Multipart.new({ foo: "bar" })
277
+ req = capture_request(client) { client.get("http://example.com/", form: form_data) }
278
+
279
+ assert_same form_data, req.body.source
280
+ assert_match(/^Content-Disposition: form-data; name="foo"\r\n\r\nbar\r\n/m, req.body.source.to_s)
281
+ end
282
+
283
+ def test_passing_multipart_form_data_with_urlencoded_object_passes_it_through_unchanged
284
+ form_data = HTTP::FormData::Urlencoded.new({ foo: "bar" })
285
+ req = capture_request(client) { client.get("http://example.com/", form: form_data) }
286
+
287
+ assert_same form_data, req.body.source
288
+ end
289
+
290
+ # passing json
291
+
292
+ def test_passing_json_encodes_given_object
293
+ req = capture_request(client) { client.get("http://example.com/", json: { foo: :bar }) }
294
+
295
+ assert_equal '{"foo":"bar"}', req.body.source
296
+ assert_equal "application/json; charset=utf-8", req.headers["Content-Type"]
297
+ end
298
+
299
+ # #request with non-ASCII URLs
300
+
301
+ def test_request_with_non_ascii_urls_theoretically_works_like_a_charm
302
+ client.get "#{dummy.endpoint}/könig"
303
+ end
304
+
305
+ def test_request_with_non_ascii_urls_handles_multi_byte_characters
306
+ client.get "#{dummy.endpoint}/héllö-wörld"
307
+ end
308
+
309
+ # #request with explicitly given Host header
310
+
311
+ def test_request_with_explicitly_given_host_header_keeps_host_header_as_is
312
+ headers = { "Host" => "another.example.com" }
313
+ host_client = HTTP::Client.new(headers: headers)
314
+ req = capture_request(host_client) { host_client.request(:get, "http://example.com/") }
315
+
316
+ assert_equal "another.example.com", req.headers["Host"]
317
+ end
318
+
319
+ # #request when :auto_deflate was specified
320
+
321
+ def test_request_when_auto_deflate_deletes_content_length_header
322
+ headers = { "Content-Length" => "12" }
323
+ deflate_client = HTTP::Client.new(headers: headers, features: { auto_deflate: {} }, body: "foo")
324
+ req = capture_request(deflate_client) { deflate_client.request(:get, "http://example.com/") }
325
+
326
+ assert_nil req.headers["Content-Length"]
327
+ end
328
+
329
+ def test_request_when_auto_deflate_sets_content_encoding_header
330
+ headers = { "Content-Length" => "12" }
331
+ deflate_client = HTTP::Client.new(headers: headers, features: { auto_deflate: {} }, body: "foo")
332
+ req = capture_request(deflate_client) { deflate_client.request(:get, "http://example.com/") }
333
+
334
+ assert_equal "gzip", req.headers["Content-Encoding"]
335
+ end
336
+
337
+ def test_request_when_auto_deflate_and_no_body_does_not_set_content_encoding_header
338
+ headers = { "Content-Length" => "12" }
339
+ deflate_client = HTTP::Client.new(headers: headers, features: { auto_deflate: {} })
340
+ req = capture_request(deflate_client) { deflate_client.request(:get, "http://example.com/") }
341
+
342
+ refute_includes req.headers, "Content-Encoding"
343
+ end
344
+
345
+ # #request Feature
346
+
347
+ def feature_class
348
+ @feature_class ||= Class.new(HTTP::Feature) do
349
+ attr_reader :captured_request, :captured_response, :captured_error
350
+
351
+ def wrap_request(request)
352
+ @captured_request = request
353
+ end
354
+
355
+ def wrap_response(response)
356
+ @captured_response = response
357
+ end
358
+
359
+ def on_error(request, error)
360
+ @captured_request = request
361
+ @captured_error = error
362
+ end
363
+ end
364
+ end
365
+
366
+ def test_feature_is_given_a_chance_to_wrap_the_request
367
+ feature_instance = feature_class.new
368
+
369
+ response = client.use(test_feature: feature_instance)
370
+ .request(:get, dummy.endpoint)
371
+
372
+ assert_equal 200, response.code
373
+ assert_equal :get, feature_instance.captured_request.verb
374
+ assert_equal "#{dummy.endpoint}/", feature_instance.captured_request.uri.to_s
375
+ end
376
+
377
+ def test_feature_is_given_a_chance_to_wrap_the_response
378
+ feature_instance = feature_class.new
379
+
380
+ response = client.use(test_feature: feature_instance)
381
+ .request(:get, dummy.endpoint)
382
+
383
+ assert_equal response, feature_instance.captured_response
384
+ end
385
+
386
+ def test_feature_is_given_a_chance_to_handle_an_error
387
+ sleep_url = "#{dummy.endpoint}/sleep"
388
+ feature_instance = feature_class.new
389
+
390
+ assert_raises(HTTP::TimeoutError) do
391
+ client.use(test_feature: feature_instance)
392
+ .timeout(0.01)
393
+ .request(:post, sleep_url)
394
+ end
395
+
396
+ assert_kind_of HTTP::TimeoutError, feature_instance.captured_error
397
+ assert_equal :post, feature_instance.captured_request.verb
398
+ assert_equal sleep_url, feature_instance.captured_request.uri.to_s
399
+ end
400
+
401
+ def test_feature_is_given_a_chance_to_handle_a_connection_timeout_error
402
+ sleep_url = "#{dummy.endpoint}/sleep"
403
+ feature_instance = feature_class.new
404
+
405
+ TCPSocket.stub(:open, ->(*) { sleep 0.1 }) do
406
+ assert_raises(HTTP::ConnectTimeoutError) do
407
+ client.use(test_feature: feature_instance)
408
+ .timeout(0.001)
409
+ .request(:post, sleep_url)
410
+ end
411
+ end
412
+ assert_kind_of HTTP::ConnectTimeoutError, feature_instance.captured_error
413
+ end
414
+
415
+ def test_feature_handles_responses_in_the_reverse_order_from_the_requests
416
+ feature_class_order =
417
+ Class.new(HTTP::Feature) do
418
+ @order = []
419
+
420
+ class << self
421
+ attr_reader :order
422
+ end
423
+
424
+ def initialize(id:)
425
+ super()
426
+ @id = id
427
+ end
428
+
429
+ def wrap_request(req)
430
+ self.class.order << "request.#{@id}"
431
+ req
432
+ end
433
+
434
+ def wrap_response(res)
435
+ self.class.order << "response.#{@id}"
436
+ res
437
+ end
438
+ end
439
+ feature_instance_a = feature_class_order.new(id: "a")
440
+ feature_instance_b = feature_class_order.new(id: "b")
441
+ feature_instance_c = feature_class_order.new(id: "c")
442
+
443
+ client.use(
444
+ test_feature_a: feature_instance_a,
445
+ test_feature_b: feature_instance_b,
446
+ test_feature_c: feature_instance_c
447
+ ).request(:get, dummy.endpoint)
448
+
449
+ assert_equal(
450
+ ["request.a", "request.b", "request.c", "response.c", "response.b", "response.a"],
451
+ feature_class_order.order
452
+ )
453
+ end
454
+
455
+ def test_feature_calls_on_request_once_per_attempt
456
+ feature_class_on_request =
457
+ Class.new(HTTP::Feature) do
458
+ attr_reader :call_count
459
+
460
+ def initialize
461
+ super
462
+ @call_count = 0
463
+ end
464
+
465
+ def on_request(_request)
466
+ @call_count += 1
467
+ end
468
+ end
469
+ feature_instance = feature_class_on_request.new
470
+
471
+ client.use(test_feature: feature_instance)
472
+ .request(:get, dummy.endpoint)
473
+
474
+ assert_equal 1, feature_instance.call_count
475
+ end
476
+
477
+ def test_feature_calls_on_request_once_per_retry_attempt
478
+ feature_class_on_request =
479
+ Class.new(HTTP::Feature) do
480
+ attr_reader :call_count
481
+
482
+ def initialize
483
+ super
484
+ @call_count = 0
485
+ end
486
+
487
+ def on_request(_request)
488
+ @call_count += 1
489
+ end
490
+ end
491
+ feature_instance = feature_class_on_request.new
492
+
493
+ client.use(test_feature: feature_instance)
494
+ .retriable(delay: 0, retry_statuses: [500])
495
+ .request(:get, "#{dummy.endpoint}/retry-2")
496
+
497
+ assert_equal 2, feature_instance.call_count
498
+ end
499
+
500
+ def test_feature_wraps_each_retry_attempt_with_around_request
501
+ feature_class_around =
502
+ Class.new(HTTP::Feature) do
503
+ attr_reader :events
504
+
505
+ def initialize
506
+ super
507
+ @events = []
508
+ end
509
+
510
+ def around_request(request)
511
+ @events << :before
512
+ yield(request).tap do
513
+ @events << :after
514
+ end
515
+ end
516
+ end
517
+ feature_instance = feature_class_around.new
518
+
519
+ client.use(test_feature: feature_instance)
520
+ .retriable(delay: 0, retry_statuses: [500])
521
+ .request(:get, "#{dummy.endpoint}/retry-2")
522
+
523
+ assert_equal %i[before after before after], feature_instance.events
524
+ end
525
+
526
+ def test_feature_wraps_the_exchange_with_around_request_in_feature_order
527
+ feature_class_around =
528
+ Class.new(HTTP::Feature) do
529
+ @order = []
530
+
531
+ class << self
532
+ attr_reader :order
533
+ end
534
+
535
+ def initialize(id:)
536
+ super()
537
+ @id = id
538
+ end
539
+
540
+ def around_request(request)
541
+ self.class.order << "before.#{@id}"
542
+ yield(request).tap do
543
+ self.class.order << "after.#{@id}"
544
+ end
545
+ end
546
+ end
547
+ feature_instance_a = feature_class_around.new(id: "a")
548
+ feature_instance_b = feature_class_around.new(id: "b")
549
+ feature_instance_c = feature_class_around.new(id: "c")
550
+
551
+ client.use(
552
+ test_feature_a: feature_instance_a,
553
+ test_feature_b: feature_instance_b,
554
+ test_feature_c: feature_instance_c
555
+ ).request(:get, dummy.endpoint)
556
+
557
+ assert_equal(
558
+ ["before.a", "before.b", "before.c", "after.c", "after.b", "after.a"],
559
+ feature_class_around.order
560
+ )
561
+ end
562
+
563
+ # #perform
564
+
565
+ def test_perform_calls_finish_response_once_body_was_fully_flushed
566
+ body = client.get(dummy.endpoint).to_s
567
+
568
+ assert_equal "<!doctype html>", body
569
+ end
570
+
571
+ def test_perform_provides_access_to_the_request_from_the_response
572
+ unique_value = "20190424"
573
+ response = client.headers("X-Value" => unique_value).get(dummy.endpoint)
574
+
575
+ assert_kind_of HTTP::Request, response.request
576
+ assert_equal unique_value, response.request.headers["X-Value"]
577
+ end
578
+
579
+ def test_perform_with_head_request_does_not_iterate_through_body
580
+ response = client.head(dummy.endpoint)
581
+
582
+ assert_equal 200, response.status.to_i
583
+ end
584
+
585
+ def test_perform_with_head_request_finishes_response_after_headers_were_received
586
+ response = client.head(dummy.endpoint)
587
+
588
+ assert_equal 200, response.status.to_i
589
+ end
590
+
591
+ def test_perform_when_server_fully_flushes_response_in_one_chunk_properly_reads_body
592
+ response_data = [
593
+ "HTTP/1.1 200 OK\r\n" \
594
+ "Content-Type: text/html\r\n" \
595
+ "Server: WEBrick/1.3.1 (Ruby/1.9.3/2013-11-22)\r\n" \
596
+ "Date: Mon, 24 Mar 2014 00:32:22 GMT\r\n" \
597
+ "Content-Length: 15\r\n" \
598
+ "Connection: Keep-Alive\r\n" \
599
+ "\r\n" \
600
+ "<!doctype html>"
601
+ ]
602
+
603
+ socket_spy = fake(
604
+ close: nil,
605
+ closed?: true,
606
+ readpartial: proc { response_data.shift || :eof },
607
+ write: proc(&:bytesize)
608
+ )
609
+
610
+ TCPSocket.stub(:open, socket_spy) do
611
+ body = client.get(dummy.endpoint).to_s
612
+
613
+ assert_equal "<!doctype html>", body
614
+ end
615
+ end
616
+
617
+ def test_perform_when_uses_chunked_transfer_encoding_properly_reads_body
618
+ response_data = [
619
+ "HTTP/1.1 200 OK\r\n" \
620
+ "Content-Type: application/json\r\n" \
621
+ "Transfer-Encoding: chunked\r\n" \
622
+ "Connection: close\r\n" \
623
+ "\r\n" \
624
+ "9\r\n" \
625
+ "{\"state\":\r\n" \
626
+ "5\r\n" \
627
+ "\"ok\"}\r\n" \
628
+ "0\r\n" \
629
+ "\r\n"
630
+ ]
631
+
632
+ socket_spy = fake(
633
+ close: nil,
634
+ closed?: true,
635
+ readpartial: proc { response_data.shift || :eof },
636
+ write: proc(&:bytesize)
637
+ )
638
+
639
+ TCPSocket.stub(:open, socket_spy) do
640
+ body = client.get(dummy.endpoint).to_s
641
+
642
+ assert_equal '{"state":"ok"}', body
643
+ end
644
+ end
645
+
646
+ def test_perform_when_uses_chunked_transfer_encoding_with_broken_body_raises_connection_error
647
+ response_data = [
648
+ "HTTP/1.1 200 OK\r\n" \
649
+ "Content-Type: application/json\r\n" \
650
+ "Transfer-Encoding: chunked\r\n" \
651
+ "Connection: close\r\n" \
652
+ "\r\n" \
653
+ "9\r\n" \
654
+ "{\"state\":\r\n"
655
+ ]
656
+
657
+ socket_spy = fake(
658
+ close: nil,
659
+ closed?: true,
660
+ readpartial: proc { response_data.shift || :eof },
661
+ write: proc(&:bytesize)
662
+ )
663
+
664
+ TCPSocket.stub(:open, socket_spy) do
665
+ assert_raises(HTTP::ConnectionError) { client.get(dummy.endpoint).to_s }
666
+ end
667
+ end
668
+
669
+ # #perform with failed proxy connect
670
+
671
+ def test_perform_with_failed_proxy_connect_skips_sending_request
672
+ proxy_client = HTTP::Client.new
673
+ conn = fake(
674
+ failed_proxy_connect?: true,
675
+ proxy_response_headers: {},
676
+ status_code: 407,
677
+ http_version: "1.1",
678
+ headers: HTTP::Headers.new,
679
+ finish_response: nil,
680
+ keep_alive?: true,
681
+ expired?: false,
682
+ close: nil,
683
+ "pending_response=": ->(*) {}
684
+ )
685
+ proxy_client.instance_variable_set(:@connection, conn)
686
+ proxy_client.instance_variable_set(:@state, :clean)
687
+ req = HTTP::Request.new(verb: :get, uri: "http://example.com/", headers: {})
688
+ response = proxy_client.perform(req, HTTP::Options.new)
689
+
690
+ assert_equal 407, response.status.to_i
691
+ end
692
+ end
693
+
694
+ class HTTPClientHTTPHandlingTest < Minitest::Test
695
+ run_server(:dummy) { DummyServer.new }
696
+
697
+ def server
698
+ dummy
699
+ end
700
+
701
+ def build_client(**)
702
+ HTTP::Client.new(**)
703
+ end
704
+
705
+ include HTTPHandlingTests
706
+ end
707
+
708
+ class HTTPClientSSLTest < Minitest::Test
709
+ run_server(:dummy_ssl) { DummyServer.new(ssl: true) }
710
+
711
+ def server
712
+ dummy_ssl
713
+ end
714
+
715
+ def build_client(**)
716
+ HTTP::Client.new(**, ssl_context: SSLHelper.client_context)
717
+ end
718
+
719
+ include HTTPHandlingTests
720
+
721
+ def test_ssl_just_works
722
+ response = build_client.get(dummy_ssl.endpoint)
723
+
724
+ assert_equal "<!doctype html>", response.body.to_s
725
+ end
726
+
727
+ def test_ssl_fails_with_ssl_error_if_host_mismatch
728
+ assert_raises(OpenSSL::SSL::SSLError) do
729
+ build_client.get(dummy_ssl.endpoint.gsub("127.0.0.1", "localhost"))
730
+ end
731
+ end
732
+
733
+ def test_ssl_with_ssl_options_instead_of_a_context_just_works
734
+ ssl_client = HTTP::Client.new(ssl: SSLHelper.client_params)
735
+ response = ssl_client.get(dummy_ssl.endpoint)
736
+
737
+ assert_equal "<!doctype html>", response.body.to_s
738
+ end
739
+ end