v2 / vlib / veb / tests / persistent_connection_test.v
189 lines · 156 sloc · 5.1 KB · 344b9afcfe67902dd9660bd3b077f18464d1d114
Raw
1// vtest build: !windows // fasthttp.Server.run is not implemented on windows yet
2import net
3import net.http
4import io
5import os
6import time
7import veb
8
9const exit_after = time.second * 10
10const port = 13009
11const localserver = 'localhost:${port}'
12const tcp_r_timeout = 2 * time.second
13const tcp_w_timeout = 2 * time.second
14const max_retries = 4
15
16const default_request = 'GET / HTTP/1.1
17User-Agent: VTESTS
18Accept: */*
19\r\n'
20
21const response_body = 'intact!'
22
23pub struct Context {
24 veb.Context
25}
26
27pub struct App {
28mut:
29 started chan bool
30 counter int
31}
32
33pub fn (mut app App) before_accept_loop() {
34 app.started <- true
35}
36
37pub fn (mut app App) index(mut ctx Context) veb.Result {
38 app.counter++
39 return ctx.text('${response_body}:${app.counter}')
40}
41
42pub fn (mut app App) reset(mut ctx Context) veb.Result {
43 app.counter = 0
44 return ctx.ok('')
45}
46
47pub fn (mut app App) reusable(mut ctx Context) veb.Result {
48 ctx.takeover_conn_reusable()
49 body := 'manual:${app.counter}'
50 ctx.conn.write_string('HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n${body.len:x}\r\n${body}\r\n0\r\n\r\n') or {}
51 return veb.no_result()
52}
53
54fn testsuite_begin() {
55 os.chdir(os.dir(@FILE))!
56 mut app := &App{}
57
58 spawn veb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 5)
59 _ := <-app.started
60
61 spawn fn () {
62 time.sleep(exit_after)
63 assert true == false, 'timeout reached!'
64 exit(1)
65 }()
66}
67
68fn test_conn_remains_intact() {
69 http.get('http://${localserver}/reset')!
70
71 mut conn := simple_tcp_client()!
72 conn.write_string(default_request)!
73
74 mut read := io.read_all(reader: conn)!
75 mut response := read.bytestr()
76 assert response.contains('Connection: close') == false, '`Connection` header should NOT be present!'
77 assert response.ends_with('${response_body}:1') == true, 'read response: ${response}'
78
79 // send request again over the same connection
80 conn.write_string(default_request)!
81
82 read = io.read_all(reader: conn)!
83 response = read.bytestr()
84 assert response.contains('Connection: close') == false, '`Connection` header should NOT be present!'
85 assert response.ends_with('${response_body}:2') == true, 'read response: ${response}'
86
87 conn.close() or {}
88}
89
90fn test_conn_remains_intact_after_reusable_takeover() {
91 http.get('http://${localserver}/reset')!
92
93 mut conn := simple_tcp_client()!
94 conn.write_string('GET /reusable HTTP/1.1\r\nUser-Agent: VTESTS\r\nAccept: */*\r\n\r\n')!
95
96 mut response := read_until_contains(mut conn, '\r\n0\r\n\r\n')!
97 assert response.contains('Connection: close') == false, '`Connection` header should NOT be present!'
98 assert response.contains('manual:0') == true, 'read response: ${response}'
99 assert response.contains('\r\n0\r\n\r\n') == true, 'read response: ${response}'
100
101 // send request again over the same connection
102 conn.write_string(default_request)!
103
104 read := io.read_all(reader: conn)!
105 response = read.bytestr()
106 assert response.contains('Connection: close') == false, '`Connection` header should NOT be present!'
107 assert response.ends_with('${response_body}:1') == true, 'read response: ${response}'
108
109 conn.close() or {}
110}
111
112fn test_reusable_takeover_honors_connection_close() {
113 http.get('http://${localserver}/reset')!
114
115 mut conn := simple_tcp_client()!
116 conn.write_string('GET /reusable HTTP/1.1\r\nUser-Agent: VTESTS\r\nAccept: */*\r\nConnection: close\r\n\r\n')!
117
118 response := read_until_contains(mut conn, '\r\n0\r\n\r\n')!
119 assert response.contains('manual:0') == true, 'read response: ${response}'
120 assert response.contains('\r\n0\r\n\r\n') == true, 'read response: ${response}'
121
122 conn.write_string(default_request) or {}
123 next := io.read_all(reader: conn) or { []u8{} }
124 assert next.len == 0
125
126 conn.close() or {}
127}
128
129fn test_support_http_1() {
130 http.get('http://${localserver}/reset')!
131 // HTTP 1.0 always closes the connection after each request, so the client must
132 // send the Connection: close header. If that header is present the connection
133 // needs to be closed and a `Connection: close` header needs to be send back
134 mut x := http.fetch(http.FetchConfig{
135 url: 'http://${localserver}/'
136 header: http.new_header_from_map({
137 .connection: 'close'
138 })
139 })!
140 assert x.status() == .ok
141 if conn_header := x.header.get(.connection) {
142 assert conn_header == 'close'
143 } else {
144 assert false, '`Connection: close` header should be present!'
145 }
146}
147
148// utility code:
149
150fn simple_tcp_client() !&net.TcpConn {
151 mut client := &net.TcpConn(unsafe { nil })
152 mut tries := 0
153 for tries < max_retries {
154 tries++
155 eprintln('> client retries: ${tries}')
156 client = net.dial_tcp(localserver) or {
157 eprintln('dial error: ${err.msg()}')
158 if tries > max_retries {
159 return err
160 }
161 time.sleep(100 * time.millisecond)
162 continue
163 }
164 break
165 }
166 if client == unsafe { nil } {
167 eprintln('could not create a tcp client connection to http://${localserver} after ${max_retries} retries')
168 exit(1)
169 }
170 client.set_read_timeout(tcp_r_timeout)
171 client.set_write_timeout(tcp_w_timeout)
172 return client
173}
174
175fn read_until_contains(mut conn net.TcpConn, marker string) !string {
176 mut raw := ''
177 mut buf := []u8{len: 1024}
178 for _ in 0 .. 16 {
179 n := conn.read(mut buf)!
180 if n <= 0 {
181 break
182 }
183 raw += buf[..n].bytestr()
184 if raw.contains(marker) {
185 break
186 }
187 }
188 return raw
189}
190