v / vlib / net / http / server_test.v
713 lines · 641 sloc · 19.74 KB · 065a450b86f6459b1e4398fe7b0594bbfcc2d691
Raw
1// vtest retry: 5
2import log
3import net
4import net.http
5import time
6
7const atimeout = 500 * time.millisecond
8
9fn testsuite_begin() {
10 log.info(@FN)
11}
12
13fn testsuite_end() {
14 log.info(@FN)
15}
16
17fn test_server_stop() {
18 log.warn('${@FN} started')
19 defer {
20 log.warn('${@FN} finished')
21 }
22 mut server := &http.Server{
23 accept_timeout: atimeout
24 }
25 t := spawn server.listen_and_serve()
26 server.wait_till_running()!
27 mut watch := time.new_stopwatch()
28 server.stop()
29 assert server.status() == .stopped
30 assert watch.elapsed() < 100 * time.millisecond
31 t.wait()
32 assert watch.elapsed() < 999 * time.millisecond
33}
34
35fn test_server_close() {
36 log.warn('${@FN} started')
37 defer {
38 log.warn('${@FN} finished')
39 }
40 mut server := &http.Server{
41 accept_timeout: atimeout
42 handler: MyHttpHandler{}
43 show_startup_message: false
44 }
45 t := spawn server.listen_and_serve()
46 server.wait_till_running()!
47 mut watch := time.new_stopwatch()
48 server.close()
49 assert server.status() == .closed
50 assert watch.elapsed() < 100 * time.millisecond
51 t.wait()
52 assert watch.elapsed() < 999 * time.millisecond
53}
54
55fn test_server_custom_listener() {
56 log.warn('${@FN} started')
57 defer {
58 log.warn('${@FN} finished')
59 }
60 listener := net.listen_tcp(.ip6, ':8081')!
61 mut server := &http.Server{
62 accept_timeout: atimeout
63 listener: listener
64 show_startup_message: false
65 }
66 t := spawn server.listen_and_serve()
67 server.wait_till_running()!
68 mut watch := time.new_stopwatch()
69 server.close()
70 assert server.status() == .closed
71 assert watch.elapsed() < 100 * time.millisecond
72 t.wait()
73 assert watch.elapsed() < 999 * time.millisecond
74}
75
76struct MyHttpHandler {
77mut:
78 counter int
79 oks int
80 not_founds int
81 redirects int
82}
83
84fn (mut handler MyHttpHandler) handle(req http.Request) http.Response {
85 handler.counter++
86 // eprintln('${time.now()} | counter: ${handler.counter} | ${req.method} ${req.url}\n${req.header}\n${req.data} - 200 OK\n')
87 // Note: the response must not echo `req.header` wholesale — the request's
88 // `Content-Length` (0 for GETs) would override the server's correct one,
89 // yielding a malformed response whose declared length disagrees with its
90 // body.
91 mut r := http.Response{
92 body: req.data + ', ${req.url}'
93 }
94 match req.url.all_before('?') {
95 '/endpoint', '/another/endpoint' {
96 r.set_status(.ok)
97 handler.oks++
98 }
99 '/redirect_to_big' {
100 r.header = http.new_header(key: .location, value: '/big')
101 r.status_msg = 'Moved permanently'
102 r.status_code = 301
103 handler.redirects++
104 }
105 '/big' {
106 r.body = 'xyz def '.repeat(5_000)
107 r.set_status(.ok)
108 handler.oks++
109 }
110 else {
111 r.set_status(.not_found)
112 handler.not_founds++
113 }
114 }
115
116 r.set_version(req.version)
117 return r
118}
119
120fn test_server_custom_handler() {
121 log.warn('${@FN} started')
122 defer {
123 log.warn('${@FN} finished')
124 }
125 mut server := &http.Server{
126 accept_timeout: atimeout
127 handler: MyHttpHandler{}
128 addr: '127.0.0.1:18197'
129 }
130 t := spawn server.listen_and_serve()
131 server.wait_till_running()!
132 x := http.fetch(url: 'http://${server.addr}/endpoint?abc=xyz', data: 'my data')!
133 assert x.body == 'my data, /endpoint?abc=xyz'
134 assert x.status_code == 200
135 assert x.status_msg == 'OK'
136 assert x.http_version == '1.1'
137 y := http.fetch(url: 'http://${server.addr}/another/endpoint', data: 'abcde')!
138 assert y.body == 'abcde, /another/endpoint'
139 assert y.status_code == 200
140 assert x.status_msg == 'OK'
141 assert y.status() == .ok
142 assert y.http_version == '1.1'
143
144 http.fetch(url: 'http://${server.addr}/something/else')!
145
146 big_url := 'http://${server.addr}/redirect_to_big'
147 mut progress_calls := &ProgressCalls{}
148 z := http.fetch(
149 url: big_url
150 user_ptr: progress_calls
151 on_redirect: fn (req &http.Request, nredirects int, new_url string) ! {
152 mut progress_calls := unsafe { &ProgressCalls(req.user_ptr) }
153 eprintln('>>>>>>>> on_redirect, req.url: ${req.url} | new_url: ${new_url} | nredirects: ${nredirects}')
154 progress_calls.redirected_to << new_url
155 }
156 on_progress: fn (req &http.Request, chunk []u8, read_so_far u64) ! {
157 mut progress_calls := unsafe { &ProgressCalls(req.user_ptr) }
158 eprintln('>>>>>>>> on_progress, req.url: ${req.url} | got chunk.len: ${chunk.len:5}, read_so_far: ${read_so_far:8}, chunk: ${chunk#[0..30].bytestr()}')
159 progress_calls.chunks << chunk.clone()
160 progress_calls.reads << read_so_far
161 }
162 on_finish: fn (req &http.Request, final_size u64) ! {
163 mut progress_calls := unsafe { &ProgressCalls(req.user_ptr) }
164 eprintln('>>>>>>>> on_finish, req.url: ${req.url}, final_size: ${final_size}')
165 progress_calls.finished_was_called = true
166 progress_calls.final_size = final_size
167 }
168 )!
169 assert z.status_code == 200
170 assert z.body.starts_with('xyz')
171 assert z.body.len > 10000
172 assert progress_calls.final_size > 40_000
173 assert progress_calls.finished_was_called
174 assert progress_calls.chunks.len > 1
175 assert progress_calls.reads.len > 1
176 assert progress_calls.chunks[0].bytestr().starts_with('HTTP/1.1 301 Moved permanently')
177 assert progress_calls.chunks[1].bytestr().starts_with('HTTP/1.1 200 OK')
178 assert progress_calls.chunks.last().bytestr().contains('xyz def')
179 assert progress_calls.redirected_to == ['http://${server.addr}/big']
180
181 server.stop()
182 t.wait()
183
184 if mut server.handler is MyHttpHandler {
185 assert server.handler.counter == 5
186 assert server.handler.oks == 3
187 assert server.handler.not_founds == 1
188 assert server.handler.redirects == 1
189 } else {
190 assert false, 'expected MyHttpHandler, got ${typeof(server.handler).name}'
191 }
192}
193
194struct ProgressCalls {
195mut:
196 chunks [][]u8
197 reads []u64
198 finished_was_called bool
199 redirected_to []string
200 final_size u64
201}
202
203//
204
205struct MyCountingHandler {
206mut:
207 counter int
208}
209
210fn (mut handler MyCountingHandler) handle(req http.Request) http.Response {
211 handler.counter++
212 mut r := http.Response{
213 body: req.data + ', ${req.url}, counter: ${handler.counter}'
214 header: req.header
215 }
216 match req.url.all_before('?') {
217 '/count' {
218 r.set_status(.ok)
219 }
220 else {
221 r.set_status(.not_found)
222 }
223 }
224
225 r.set_version(req.version)
226 return r
227}
228
229fn test_my_counting_handler_on_random_port() {
230 log.warn('${@FN} started')
231 defer {
232 log.warn('${@FN} finished')
233 }
234 mut server := &http.Server{
235 show_startup_message: false
236 addr: ''
237 accept_timeout: atimeout
238 handler: MyCountingHandler{}
239 on_running: fn (mut server http.Server) {
240 spawn fn (mut server http.Server) {
241 log.warn('server started')
242 url := 'http://${server.addr}/count'
243 log.info('fetching from url: ${url}')
244 for _ in 0 .. 5 {
245 x := http.fetch(url: url, data: 'my data') or { panic(err) }
246 log.info(x.body)
247 }
248 server.stop()
249 log.warn('server stopped')
250 }(mut server)
251 }
252 }
253 server.listen_and_serve()
254 if mut server.handler is MyCountingHandler {
255 dump(server.handler.counter)
256 assert server.handler.counter == 5
257 }
258 assert true
259}
260
261//
262
263struct MyCustomHttpHostHandler {}
264
265fn (mut handler MyCustomHttpHostHandler) handle(req http.Request) http.Response {
266 dump(req.header)
267 return http.Response{
268 body: 'Host was: ${req.header.get(.host) or { '-' }}'
269 }
270}
271
272fn test_host_header_sent_to_server() {
273 ip := '127.0.0.1'
274 port := 54671
275 log.warn('${@FN} started')
276 defer { log.warn('${@FN} finished') }
277 mut server := &http.Server{
278 handler: MyCustomHttpHostHandler{}
279 addr: '${ip}:${port}'
280 }
281 t := spawn server.listen_and_serve()
282 server.wait_till_running() or {
283 estr := err.str()
284 if estr == 'maximum retries reached' {
285 log.error('>>>> Skipping test ${@FN} since its server could not start, err: ${err}')
286 return
287 }
288 log.fatal(estr)
289 }
290 defer { server.stop() }
291 dump(server.addr)
292 x := http.get('http://${server.addr}/')!
293 dump(x)
294 assert x.status_code == 200
295 assert x.status_msg == 'OK'
296 assert x.body.ends_with('${ip}:${port}')
297}
298
299//
300
301struct InvalidResponseHandler {}
302
303fn (mut handler InvalidResponseHandler) handle(req http.Request) http.Response {
304 return http.Response{
305 header: http.new_header_from_map({
306 http.CommonHeader.content_type: 'text/plain'
307 })
308 }
309}
310
311struct InvalidStatusCodeHandler {}
312
313fn (mut handler InvalidStatusCodeHandler) handle(req http.Request) http.Response {
314 return http.Response{
315 body: 'broken status'
316 status_code: 42
317 }
318}
319
320fn test_server_normalizes_invalid_handler_response() {
321 log.warn('${@FN} started')
322 defer { log.warn('${@FN} finished') }
323 mut server := &http.Server{
324 accept_timeout: atimeout
325 handler: InvalidResponseHandler{}
326 addr: '127.0.0.1:18202'
327 show_startup_message: false
328 }
329 t := spawn server.listen_and_serve()
330 server.wait_till_running() or {
331 estr := err.str()
332 if estr == 'maximum retries reached' {
333 log.error('>>>> Skipping test ${@FN} since its server could not start, err: ${err}')
334 return
335 }
336 log.fatal(estr)
337 }
338
339 mut conn := net.dial_tcp('127.0.0.1:18202')!
340 defer { conn.close() or {} }
341 conn.set_read_timeout(5 * time.second)
342 conn.set_write_timeout(5 * time.second)
343
344 request := 'GET / HTTP/1.1\r\nHost: 127.0.0.1:18202\r\nConnection: close\r\n\r\n'
345 conn.write(request.bytes())!
346 response := read_http_response(mut conn)!
347 lines := response.split('\r\n')
348 assert lines[0] == 'HTTP/1.1 200 OK'
349 assert response.to_lower().contains('content-type: text/plain')
350 assert response.to_lower().contains('content-length: 0')
351
352 server.stop()
353 t.wait()
354}
355
356fn test_server_coerces_invalid_status_code_to_internal_server_error() {
357 log.warn('${@FN} started')
358 defer { log.warn('${@FN} finished') }
359 mut server := &http.Server{
360 accept_timeout: atimeout
361 handler: InvalidStatusCodeHandler{}
362 addr: '127.0.0.1:18203'
363 show_startup_message: false
364 }
365 t := spawn server.listen_and_serve()
366 server.wait_till_running() or {
367 estr := err.str()
368 if estr == 'maximum retries reached' {
369 log.error('>>>> Skipping test ${@FN} since its server could not start, err: ${err}')
370 return
371 }
372 log.fatal(estr)
373 }
374
375 mut conn := net.dial_tcp('127.0.0.1:18203')!
376 defer { conn.close() or {} }
377 conn.set_read_timeout(5 * time.second)
378 conn.set_write_timeout(5 * time.second)
379
380 request := 'GET / HTTP/1.1\r\nHost: 127.0.0.1:18203\r\nConnection: close\r\n\r\n'
381 conn.write(request.bytes())!
382 response := read_http_response(mut conn)!
383 lines := response.split('\r\n')
384 assert lines[0] == 'HTTP/1.1 500 Internal Server Error'
385 assert response.ends_with('broken status')
386
387 server.stop()
388 t.wait()
389}
390
391//
392
393struct RedirectMethodHandler {}
394
395fn (mut handler RedirectMethodHandler) handle(req http.Request) http.Response {
396 mut r := http.Response{}
397 match req.url {
398 '/redirect-301' {
399 r.header = http.new_header(key: .location, value: '/expect-get')
400 r.set_status(.moved_permanently)
401 }
402 '/redirect-302' {
403 r.header = http.new_header(key: .location, value: '/expect-get')
404 r.set_status(.found)
405 }
406 '/redirect-303' {
407 r.header = http.new_header(key: .location, value: '/expect-get')
408 r.set_status(.see_other)
409 }
410 '/redirect-307' {
411 r.header = http.new_header(key: .location, value: '/expect-post')
412 r.set_status(.temporary_redirect)
413 }
414 '/redirect-308' {
415 r.header = http.new_header(key: .location, value: '/expect-post')
416 r.set_status(.permanent_redirect)
417 }
418 '/expect-get' {
419 if req.method == .get && req.data == '' {
420 r.body = 'redirected-as-get'
421 r.set_status(.ok)
422 } else {
423 r.body = 'expected GET without a body, got ${req.method} `${req.data}`'
424 r.set_status(.method_not_allowed)
425 }
426 }
427 '/expect-post' {
428 if req.method == .post && req.data == 'payload' {
429 r.body = 'preserved-post'
430 r.set_status(.ok)
431 } else {
432 r.body = 'expected POST with payload, got ${req.method} `${req.data}`'
433 r.set_status(.method_not_allowed)
434 }
435 }
436 else {
437 r.set_status(.not_found)
438 }
439 }
440
441 r.set_version(req.version)
442 return r
443}
444
445fn test_redirects_change_post_to_get_only_when_required() {
446 log.warn('${@FN} started')
447 defer { log.warn('${@FN} finished') }
448 mut server := &http.Server{
449 accept_timeout: atimeout
450 handler: RedirectMethodHandler{}
451 addr: '127.0.0.1:18204'
452 show_startup_message: false
453 }
454 t := spawn server.listen_and_serve()
455 server.wait_till_running() or {
456 estr := err.str()
457 if estr == 'maximum retries reached' {
458 log.error('>>>> Skipping test ${@FN} since its server could not start, err: ${err}')
459 return
460 }
461 log.fatal(estr)
462 }
463
464 for path in ['/redirect-301', '/redirect-302', '/redirect-303'] {
465 resp := http.post('http://${server.addr}${path}', 'payload')!
466 assert resp.status() == .ok
467 assert resp.body == 'redirected-as-get'
468 }
469
470 for path in ['/redirect-307', '/redirect-308'] {
471 resp := http.post('http://${server.addr}${path}', 'payload')!
472 assert resp.status() == .ok
473 assert resp.body == 'preserved-post'
474 }
475
476 server.stop()
477 t.wait()
478}
479
480//
481
482struct KeepAliveHandler {
483mut:
484 request_count int
485}
486
487fn (mut handler KeepAliveHandler) handle(req http.Request) http.Response {
488 handler.request_count++
489 mut r := http.Response{
490 body: 'request #${handler.request_count}: ${req.url}'
491 }
492 r.set_status(.ok)
493 r.set_version(req.version)
494 // Echo back the Connection header from the request if present
495 if conn := req.header.get(.connection) {
496 r.header.set(.connection, conn)
497 }
498 return r
499}
500
501fn test_server_keep_alive() {
502 log.warn('${@FN} started')
503 defer { log.warn('${@FN} finished') }
504 mut server := &http.Server{
505 accept_timeout: atimeout
506 handler: KeepAliveHandler{}
507 addr: '127.0.0.1:18198'
508 show_startup_message: false
509 }
510 t := spawn server.listen_and_serve()
511 server.wait_till_running() or {
512 estr := err.str()
513 if estr == 'maximum retries reached' {
514 log.error('>>>> Skipping test ${@FN} since its server could not start, err: ${err}')
515 return
516 }
517 log.fatal(estr)
518 }
519
520 // Test keep-alive by sending multiple requests over a single TCP connection
521 mut conn := net.dial_tcp('127.0.0.1:18198')!
522 defer { conn.close() or {} }
523 conn.set_read_timeout(5 * time.second)
524 conn.set_write_timeout(5 * time.second)
525
526 // Send first request with Connection: keep-alive
527 request1 := 'GET /test1 HTTP/1.1\r\nHost: 127.0.0.1:18198\r\nConnection: keep-alive\r\n\r\n'
528 conn.write(request1.bytes())!
529 mut resp1 := read_http_response(mut conn)!
530 log.info('Response 1: ${resp1}')
531 assert resp1.contains('request #1')
532 assert resp1.to_lower().contains('connection: keep-alive')
533
534 // Send second request on the same connection
535 request2 := 'GET /test2 HTTP/1.1\r\nHost: 127.0.0.1:18198\r\nConnection: keep-alive\r\n\r\n'
536 conn.write(request2.bytes())!
537 mut resp2 := read_http_response(mut conn)!
538 log.info('Response 2: ${resp2}')
539 assert resp2.contains('request #2')
540 assert resp2.to_lower().contains('connection: keep-alive')
541
542 // Send third request with Connection: close to end the connection
543 request3 := 'GET /test3 HTTP/1.1\r\nHost: 127.0.0.1:18198\r\nConnection: close\r\n\r\n'
544 conn.write(request3.bytes())!
545 mut resp3 := read_http_response(mut conn)!
546 log.info('Response 3: ${resp3}')
547 assert resp3.contains('request #3')
548 assert resp3.to_lower().contains('connection: close')
549
550 server.stop()
551 t.wait()
552
553 // Verify all 3 requests were handled
554 if mut server.handler is KeepAliveHandler {
555 assert server.handler.request_count == 3
556 } else {
557 assert false, 'expected KeepAliveHandler, got ${typeof(server.handler).name}'
558 }
559}
560
561fn test_server_keep_alive_many_requests() {
562 log.warn('${@FN} started')
563 defer { log.warn('${@FN} finished') }
564 total_requests := 64
565 mut server := &http.Server{
566 accept_timeout: atimeout
567 handler: KeepAliveHandler{}
568 addr: '127.0.0.1:18199'
569 show_startup_message: false
570 max_keep_alive_requests: 0
571 }
572 t := spawn server.listen_and_serve()
573 server.wait_till_running() or {
574 estr := err.str()
575 if estr == 'maximum retries reached' {
576 log.error('>>>> Skipping test ${@FN} since its server could not start, err: ${err}')
577 return
578 }
579 log.fatal(estr)
580 }
581
582 mut conn := net.dial_tcp('127.0.0.1:18199')!
583 defer { conn.close() or {} }
584 conn.set_read_timeout(5 * time.second)
585 conn.set_write_timeout(5 * time.second)
586
587 for i in 0 .. total_requests {
588 path := if i % 2 == 0 { '/left' } else { '/right' }
589 request := 'GET ${path}?n=${i} HTTP/1.1\r\nHost: 127.0.0.1:18199\r\nConnection: keep-alive\r\n\r\n'
590 conn.write(request.bytes())!
591 resp := read_http_response(mut conn)!
592 assert resp.contains('request #${i + 1}')
593 assert resp.contains(path)
594 assert resp.to_lower().contains('connection: keep-alive')
595 }
596
597 request_done := 'GET /done HTTP/1.1\r\nHost: 127.0.0.1:18199\r\nConnection: close\r\n\r\n'
598 conn.write(request_done.bytes())!
599 resp_done := read_http_response(mut conn)!
600 assert resp_done.contains('request #${total_requests + 1}')
601 assert resp_done.contains('/done')
602 assert resp_done.to_lower().contains('connection: close')
603
604 server.stop()
605 t.wait()
606 if mut server.handler is KeepAliveHandler {
607 assert server.handler.request_count == total_requests + 1
608 } else {
609 assert false, 'expected KeepAliveHandler, got ${typeof(server.handler).name}'
610 }
611}
612
613fn test_server_max_keep_alive_requests() {
614 log.warn('${@FN} started')
615 defer { log.warn('${@FN} finished') }
616 mut server := &http.Server{
617 accept_timeout: atimeout
618 handler: KeepAliveHandler{}
619 addr: '127.0.0.1:18200'
620 show_startup_message: false
621 max_keep_alive_requests: 3 // Limit to 3 requests per connection
622 }
623 t := spawn server.listen_and_serve()
624 server.wait_till_running() or {
625 estr := err.str()
626 if estr == 'maximum retries reached' {
627 log.error('>>>> Skipping test ${@FN} since its server could not start, err: ${err}')
628 return
629 }
630 log.fatal(estr)
631 }
632
633 // Test that connection closes after max_keep_alive_requests
634 mut conn := net.dial_tcp('127.0.0.1:18200')!
635 defer { conn.close() or {} }
636 conn.set_read_timeout(5 * time.second)
637 conn.set_write_timeout(5 * time.second)
638
639 // Send first request
640 request1 := 'GET /test1 HTTP/1.1\r\nHost: 127.0.0.1:18200\r\nConnection: keep-alive\r\n\r\n'
641 conn.write(request1.bytes())!
642 mut resp1 := read_http_response(mut conn)!
643 log.info('Response 1: ${resp1}')
644 assert resp1.contains('request #1')
645 assert resp1.to_lower().contains('connection: keep-alive')
646
647 // Send second request
648 request2 := 'GET /test2 HTTP/1.1\r\nHost: 127.0.0.1:18200\r\nConnection: keep-alive\r\n\r\n'
649 conn.write(request2.bytes())!
650 mut resp2 := read_http_response(mut conn)!
651 log.info('Response 2: ${resp2}')
652 assert resp2.contains('request #2')
653 assert resp2.to_lower().contains('connection: keep-alive')
654
655 // Send third request - should get Connection: close since max is reached
656 request3 := 'GET /test3 HTTP/1.1\r\nHost: 127.0.0.1:18200\r\nConnection: keep-alive\r\n\r\n'
657 conn.write(request3.bytes())!
658 mut resp3 := read_http_response(mut conn)!
659 log.info('Response 3: ${resp3}')
660 assert resp3.contains('request #3')
661 assert resp3.to_lower().contains('connection: close'), 'Expected connection: close after max requests reached'
662
663 server.stop()
664 t.wait()
665
666 if mut server.handler is KeepAliveHandler {
667 assert server.handler.request_count == 3
668 } else {
669 assert false, 'expected KeepAliveHandler, got ${typeof(server.handler).name}'
670 }
671}
672
673fn read_http_response(mut conn net.TcpConn) !string {
674 mut response := []u8{}
675 mut buf := []u8{len: 1024}
676 mut content_length := -1
677 mut headers_end := -1
678
679 for {
680 n := conn.read(mut buf) or { break }
681 if n <= 0 {
682 break
683 }
684 response << buf[..n]
685
686 // Check if we have received all headers
687 response_str := response.bytestr()
688 if headers_end == -1 {
689 headers_end = response_str.index('\r\n\r\n') or { -1 }
690 if headers_end != -1 {
691 // Parse Content-Length from headers
692 headers := response_str[..headers_end]
693 for line in headers.split('\r\n') {
694 if line.to_lower().starts_with('content-length:') {
695 content_length = line.all_after(':').trim_space().int()
696 break
697 }
698 }
699 }
700 }
701
702 // Check if we have received the full response
703 if headers_end != -1 && content_length >= 0 {
704 body_start := headers_end + 4
705 body_received := response.len - body_start
706 if body_received >= content_length {
707 break
708 }
709 }
710 }
711
712 return response.bytestr()
713}
714