v / vlib / net / http / request_test.v
420 lines · 380 sloc · 12.32 KB · e2e5cf8db56f3562c7baa735061690be936bdf3e
Raw
1// vtest build: !windows
2import net.http
3import net.urllib
4import net
5import io
6import time
7
8struct StringReader {
9 text string
10mut:
11 place int
12}
13
14fn (mut s StringReader) read(mut buf []u8) !int {
15 if s.place >= s.text.len {
16 return io.Eof{}
17 }
18 max_bytes := 100
19 end := if s.place + max_bytes >= s.text.len { s.text.len } else { s.place + max_bytes }
20 n := copy(mut buf, s.text[s.place..end].bytes())
21 s.place += n
22 return n
23}
24
25fn reader(s string) &io.BufferedReader {
26 return io.new_buffered_reader(
27 reader: &StringReader{
28 text: s
29 }
30 )
31}
32
33fn test_parse_request_not_http() {
34 mut reader__ := reader('hello')
35 http.parse_request(mut reader__) or { return }
36 panic('should not have parsed')
37}
38
39fn test_parse_request_no_headers() {
40 mut reader_ := reader('GET / HTTP/1.1\r\n\r\n')
41 req := http.parse_request(mut reader_) or { panic('did not parse: ${err}') }
42 assert req.method == .get
43 assert req.url == '/'
44 assert req.version == .v1_1
45}
46
47fn test_parse_request_two_headers() {
48 mut reader_ := reader('GET / HTTP/1.1\r\nTest1: a\r\nTest2: B\r\n\r\n')
49 req := http.parse_request(mut reader_) or { panic('did not parse: ${err}') }
50 assert req.header.custom_values('Test1') == ['a']
51 assert req.header.custom_values('Test2') == ['B']
52}
53
54fn test_parse_request_two_header_values() {
55 mut reader_ := reader('GET / HTTP/1.1\r\nTest1: a; b\r\nTest2: c\r\nTest2: d\r\n\r\n')
56 req := http.parse_request(mut reader_) or { panic('did not parse: ${err}') }
57 assert req.header.custom_values('Test1') == ['a; b']
58 assert req.header.custom_values('Test2') == ['c', 'd']
59}
60
61fn test_parse_request_body() {
62 mut reader_ :=
63 reader('GET / HTTP/1.1\r\nTest1: a\r\nTest2: b\r\nContent-Length: 4\r\n\r\nbodyabc')
64 req := http.parse_request(mut reader_) or { panic('did not parse: ${err}') }
65 assert req.data == 'body'
66}
67
68fn test_parse_request_line() {
69 method, target, version := http.parse_request_line('GET /target HTTP/1.1') or {
70 panic('did not parse: ${err}')
71 }
72 assert method == .get
73 assert target.str() == '/target'
74 assert version == .v1_1
75}
76
77fn test_parse_request_uri_with_consecutive_slashes() {
78 url := urllib.parse_request_uri('//another.html') or { panic('did not parse: ${err}') }
79 assert url.host == ''
80 assert url.path == '//another.html'
81 assert url.str() == '//another.html'
82
83 absolute := urllib.parse_request_uri('http://localhost:8080//another.html') or {
84 panic('did not parse: ${err}')
85 }
86 assert absolute.host == 'localhost:8080'
87 assert absolute.path == '//another.html'
88 assert absolute.str() == 'http://localhost:8080//another.html'
89}
90
91fn test_parse_request_line_with_consecutive_slashes() {
92 method, target, version := http.parse_request_line('GET //another.html HTTP/1.1') or {
93 panic('did not parse: ${err}')
94 }
95 assert method == .get
96 assert target.host == ''
97 assert target.path == '//another.html'
98 assert target.str() == '//another.html'
99 assert version == .v1_1
100}
101
102fn test_parse_form() {
103 assert http.parse_form('foo=bar&bar=baz') == {
104 'foo': 'bar'
105 'bar': 'baz'
106 }
107 assert http.parse_form('foo=bar=&bar=baz') == {
108 'foo': 'bar='
109 'bar': 'baz'
110 }
111 assert http.parse_form('foo=bar%3D&bar=baz') == {
112 'foo': 'bar='
113 'bar': 'baz'
114 }
115 assert http.parse_form('foo=b%26ar&bar=baz') == {
116 'foo': 'b&ar'
117 'bar': 'baz'
118 }
119 assert http.parse_form('a=b& c=d') == {
120 'a': 'b'
121 ' c': 'd'
122 }
123 assert http.parse_form('a=b&c= d ') == {
124 'a': 'b'
125 'c': ' d '
126 }
127 assert http.parse_form('{json}') == {
128 'json': '{json}'
129 }
130 assert http.parse_form('{
131 "_id": "76c",
132 "friends": [
133 {
134 "id": 0,
135 "name": "Mason Luna"
136 }
137 ],
138 "greeting": "Hello."
139 }') == {
140 'json': '{
141 "_id": "76c",
142 "friends": [
143 {
144 "id": 0,
145 "name": "Mason Luna"
146 }
147 ],
148 "greeting": "Hello."
149 }'
150 }
151}
152
153fn test_parse_multipart_form() {
154 boundary := '6844a625b1f0b299'
155 names := ['foo', 'fooz']
156 file := 'bar.v'
157 ct := 'application/octet-stream'
158 contents := ['baz', 'buzz']
159 data := "--${boundary}
160Content-Disposition: form-data; name=\"${names[0]}\"; filename=\"${file}\"\r
161Content-Type: ${ct}\r
162\r
163${contents[0]}\r
164--${boundary}\r
165Content-Disposition: form-data; name=\"${names[1]}\"\r
166\r
167${contents[1]}\r
168--${boundary}--\r
169"
170 form, files := http.parse_multipart_form(data, boundary)
171 assert files == {
172 names[0]: [
173 http.FileData{
174 filename: file
175 content_type: ct
176 data: contents[0]
177 },
178 ]
179 }
180
181 assert form == {
182 names[1]: contents[1]
183 }
184}
185
186fn test_parse_multipart_form2() {
187 boundary := '---------------------------27472781931927549291906391339'
188 data := '--${boundary}\r
189Content-Disposition: form-data; name="username"\r
190\r
191admin\r
192--${boundary}\r
193Content-Disposition: form-data; name="password"\r
194\r
195admin123\r
196--${boundary}--\r
197'
198 form, files := http.parse_multipart_form(data, boundary)
199 for k, v in form {
200 eprintln('> k: ${k} | v: ${v}')
201 eprintln('>> k.bytes(): ${k.bytes()}')
202 eprintln('>> v.bytes(): ${v.bytes()}')
203 }
204 assert form['username'] == 'admin'
205 assert form['password'] == 'admin123'
206}
207
208fn test_multipart_form_body() {
209 files := {
210 'foo': [
211 http.FileData{
212 filename: 'bar.v'
213 content_type: 'application/octet-stream'
214 data: 'baz'
215 },
216 ]
217 }
218 form := {
219 'fooz': 'buzz'
220 }
221
222 body, boundary := http.multipart_form_body(form, files)
223 parsed_form, parsed_files := http.parse_multipart_form(body, boundary)
224 assert parsed_files == files
225 assert parsed_form == form
226}
227
228fn test_parse_large_body() {
229 body := 'A'.repeat(10_001) // greater than max_bytes
230 req := 'GET / HTTP/1.1\r\nContent-Length: ${body.len}\r\n\r\n${body}'
231 mut reader_ := reader(req)
232 result := http.parse_request(mut reader_)!
233 assert result.data.len == body.len
234 assert result.data == body
235}
236
237fn test_parse_multipart_form_empty_body() {
238 body := ''
239 boundary := '----WebKitFormBoundaryQcBIkwnOACVsvR8b'
240 form, files := http.parse_multipart_form(body, boundary)
241 assert form.len == 0
242 assert files.len == 0
243}
244
245fn test_parse_multipart_form_issue_26204__do_not_panic_for_small_or_partial_forms() {
246 boundary := '----01KDN6J6BKWY9WMYWRW4MG5J59'
247 body := '${boundary}\r\nContent-Disposition: form-data; name="fooz"${boundary}--\r\n'
248 form, files := http.parse_multipart_form(body, boundary)
249 assert form.len == 0
250 assert files.len == 0
251}
252
253fn test_parse_multipart_form_issue_24974_raw() {
254 body := r'------WebKiormBoundaryQcBIkwnOACVsvR8b\r\nContent-Disposition: form-data; name="files"; filename="michael-sum-LEpfefQf4rU-unsplash.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\r\n------WebKitFormBoundaryQcBIkwnOACVsvR8b\r\nContent-Disposition: form-data; name="files"; filename="mikhail-vasilyev-IFxjDdqK_0U-unsplash.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\r\n------WebKitFormBoundaryQcBIkwnOACVsvR8b--\r\n'
255 boundary := r'----WebKitFormBoundaryQcBIkwnOACVsvR8b'
256 form, files := http.parse_multipart_form(body, boundary)
257 assert form.len == 0
258 assert files.len == 0
259}
260
261fn test_parse_multipart_form_issue_24974_cooked() {
262 body := '------WebKiormBoundaryQcBIkwnOACVsvR8b\r\nContent-Disposition: form-data; name="files"; filename="michael-sum-LEpfefQf4rU-unsplash.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\r\n------WebKitFormBoundaryQcBIkwnOACVsvR8b\r\nContent-Disposition: form-data; name="files"; filename="mikhail-vasilyev-IFxjDdqK_0U-unsplash.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\r\n------WebKitFormBoundaryQcBIkwnOACVsvR8b--\r\n'
263 boundary := '----WebKitFormBoundaryQcBIkwnOACVsvR8b'
264 form, files := http.parse_multipart_form(body, boundary)
265 assert form.len == 0
266 assert files.len == 1
267 assert files['files'][0].filename == 'mikhail-vasilyev-IFxjDdqK_0U-unsplash.jpg'
268 assert files['files'][0].content_type == 'image/jpeg'
269}
270
271fn test_parse_request_head_str_basic() {
272 s := 'GET / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 0\r\n\r\n'
273 req := http.parse_request_head_str(s) or { panic('did not parse: ${err}') }
274 assert req.method == .get
275 assert req.url == '/'
276 assert req.version == .v1_1
277 assert req.host == 'example.com'
278}
279
280fn test_parse_request_head_str_post_with_headers() {
281 s := 'POST /api HTTP/1.1\r\nHost: test.com\r\nContent-Type: application/json\r\nContent-Length: 10\r\n\r\n'
282 req := http.parse_request_head_str(s) or { panic('did not parse: ${err}') }
283 assert req.method == .post
284 assert req.url == '/api'
285 assert req.version == .v1_1
286 assert req.host == 'test.com'
287 assert req.header.custom_values('Content-Type') == ['application/json']
288}
289
290fn test_parse_request_head_str_post_with_headers_and_body() {
291 s := 'POST /index HTTP/1.1\r\nHost: localhost:9008\r\nUser-Agent: curl/7.68.0\r\nAccept: */*\r\nContent-Type: application/json\r\nContent-Length: 24\r\nConnection: keep-alive\r\n\r\n{"username": "test"}'
292 req := http.parse_request_head_str(s) or {
293 assert false, 'did not parse: ${err}'
294 return
295 }
296 assert req.method == .post
297 assert req.url == '/index'
298 assert req.version == .v1_1
299 assert req.host == 'localhost:9008'
300 assert req.header.custom_values('User-Agent') == ['curl/7.68.0']
301 assert req.header.custom_values('Accept') == ['*/*']
302 assert req.header.custom_values('Content-Type') == ['application/json']
303 assert req.header.custom_values('Connection') == ['keep-alive']
304 assert req.data == ''
305}
306
307fn test_parse_request_head_post_with_headers_and_body() {
308 s := 'POST /index HTTP/1.1\r\nHost: localhost:9008\r\nUser-Agent: curl/7.68.0\r\nAccept: */*\r\nContent-Type: application/json\r\nContent-Length: 24\r\nConnection: keep-alive\r\n\r\n{"username": "test"}'
309 req := http.parse_request_str(s) or {
310 assert false, 'did not parse: ${err}'
311 return
312 }
313 assert req.data == '{"username": "test"}'
314}
315
316fn test_parse_request_head_str_with_spaces_in_header_values() {
317 s := 'GET /path HTTP/1.1\r\nX-Custom-Header: value with spaces\r\n\r\n'
318 req := http.parse_request_head_str(s) or { panic('did not parse: ${err}') }
319 assert req.method == .get
320 assert req.url == '/path'
321 assert req.header.custom_values('X-Custom-Header') == ['value with spaces']
322}
323
324fn test_parse_request_head_str_multiple_same_header() {
325 s := 'GET / HTTP/1.1\r\nHost: example.com\r\nSet-Cookie: session=abc\r\nSet-Cookie: user=xyz\r\n\r\n'
326 req := http.parse_request_head_str(s) or { panic('did not parse: ${err}') }
327 assert req.method == .get
328 assert req.host == 'example.com'
329 assert req.header.custom_values('Set-Cookie') == ['session=abc', 'user=xyz']
330}
331
332fn test_get_does_not_wait_for_timeout_when_content_length_is_complete() {
333 mut listener := net.listen_tcp(.ip, '127.0.0.1:0')!
334 port := listener.addr()!.port()!
335 t := spawn fn (mut listener net.TcpListener) {
336 mut conn := listener.accept() or {
337 listener.close() or {}
338 return
339 }
340 defer {
341 conn.close() or {}
342 listener.close() or {}
343 }
344
345 mut request_buf := []u8{len: 2048}
346 _ = conn.read(mut request_buf) or { return }
347 response := 'HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: keep-alive\r\n\r\nok'
348 conn.write(response.bytes()) or { return }
349
350 conn.set_read_timeout(5 * time.second)
351 mut drain_buf := []u8{len: 128}
352 for {
353 n := conn.read(mut drain_buf) or { break }
354 if n <= 0 {
355 break
356 }
357 }
358 }(mut listener)
359
360 mut req := http.new_request(.get, 'http://127.0.0.1:${port}', '')
361 req.read_timeout = 2 * time.second
362 start := time.now()
363 res := req.do()!
364 elapsed := time.since(start)
365 t.wait()
366
367 assert res.status() == .ok
368 assert res.body == 'ok'
369 assert elapsed < time.second
370}
371
372fn test_prepare_uses_fetch_config_timeouts() {
373 req := http.prepare(
374 url: 'http://example.com'
375 read_timeout: 123 * time.millisecond
376 write_timeout: 456 * time.millisecond
377 )!
378 assert req.read_timeout == 123 * time.millisecond
379 assert req.write_timeout == 456 * time.millisecond
380}
381
382fn test_get_does_not_wait_for_timeout_when_chunked_body_is_complete() {
383 mut listener := net.listen_tcp(.ip, '127.0.0.1:0')!
384 port := listener.addr()!.port()!
385 t := spawn fn (mut listener net.TcpListener) {
386 mut conn := listener.accept() or {
387 listener.close() or {}
388 return
389 }
390 defer {
391 conn.close() or {}
392 listener.close() or {}
393 }
394
395 mut request_buf := []u8{len: 2048}
396 _ = conn.read(mut request_buf) or { return }
397 response := 'HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\n\r\n2\r\nok\r\n0\r\n\r\n'
398 conn.write(response.bytes()) or { return }
399
400 conn.set_read_timeout(5 * time.second)
401 mut drain_buf := []u8{len: 128}
402 for {
403 n := conn.read(mut drain_buf) or { break }
404 if n <= 0 {
405 break
406 }
407 }
408 }(mut listener)
409
410 mut req := http.new_request(.get, 'http://127.0.0.1:${port}', '')
411 req.read_timeout = 2 * time.second
412 start := time.now()
413 res := req.do()!
414 elapsed := time.since(start)
415 t.wait()
416
417 assert res.status() == .ok
418 assert res.body == 'ok'
419 assert elapsed < time.second
420}
421