v / vlib / net / http / h2_server_test.v
305 lines · 279 sloc · 7.03 KB · 35ffbabf6f192b963340da285b7d2216c318598f
Raw
1// Hermetic round-trip test for the server-side HTTP/2 driver. Uses an
2// in-memory blocking ReadWriter pair so client and server share the same
3// address space and no socket is required.
4module http
5
6import sync
7import time
8
9// PipeBuf is a one-way in-memory FIFO between a writer and a reader.
10// `read` blocks (poll-with-sleep) until data is available, mirroring socket
11// semantics so the h2 client and server can drive each other to completion.
12struct PipeBuf {
13mut:
14 mu &sync.Mutex = sync.new_mutex()
15 data []u8
16 closed bool
17}
18
19fn (mut p PipeBuf) write(buf []u8) !int {
20 p.mu.lock()
21 defer {
22 p.mu.unlock()
23 }
24 if p.closed {
25 return error('pipe: write to closed pipe')
26 }
27 p.data << buf
28 return buf.len
29}
30
31fn (mut p PipeBuf) read(mut buf []u8) !int {
32 for {
33 p.mu.lock()
34 if p.data.len > 0 {
35 mut n := p.data.len
36 if n > buf.len {
37 n = buf.len
38 }
39 for i in 0 .. n {
40 buf[i] = p.data[i]
41 }
42 p.data = p.data[n..].clone()
43 p.mu.unlock()
44 return n
45 }
46 if p.closed {
47 p.mu.unlock()
48 return error('eof')
49 }
50 p.mu.unlock()
51 time.sleep(time.millisecond)
52 }
53 return 0
54}
55
56// PipeEnd is one half of a bidirectional pipe: reads come from `incoming`,
57// writes go to `outgoing`.
58struct PipeEnd {
59mut:
60 incoming &PipeBuf
61 outgoing &PipeBuf
62}
63
64fn (mut p PipeEnd) read(mut buf []u8) !int {
65 return p.incoming.read(mut buf)!
66}
67
68fn (mut p PipeEnd) write(buf []u8) !int {
69 return p.outgoing.write(buf)!
70}
71
72fn new_pipe() (&PipeEnd, &PipeEnd) {
73 mut a := &PipeBuf{}
74 mut b := &PipeBuf{}
75 client := &PipeEnd{
76 incoming: b
77 outgoing: a
78 }
79 server := &PipeEnd{
80 incoming: a
81 outgoing: b
82 }
83 return client, server
84}
85
86struct ServerEchoHandler {
87mut:
88 last_method Method
89 last_url string
90 last_body string
91}
92
93fn (mut h ServerEchoHandler) handle(req Request) Response {
94 h.last_method = req.method
95 h.last_url = req.url
96 h.last_body = req.data
97 mut resp_header := new_header()
98 resp_header.add_custom('content-type', 'text/plain') or {}
99 // Echo the URL and body back so callers can verify end-to-end delivery
100 // without depending on Handler-state mutation across goroutines.
101 return Response{
102 status_code: 200
103 header: resp_header
104 body: 'echo: ${req.url} body=${req.data}'
105 }
106}
107
108fn test_h2_server_basic_request() {
109 mut client_end, mut server_end := new_pipe()
110
111 // Spawn the server-side h2 driver against the server end of the pipe.
112 mut handler := ServerEchoHandler{}
113 mut handler_iface := Handler(handler)
114 spawn fn [mut server_end, mut handler_iface] () {
115 mut transport := H2Transport(server_end)
116 serve_h2_conn(mut transport, mut handler_iface) or {}
117 }()
118
119 // Drive a client-side request through the same pipe.
120 mut conn := new_h2_conn(client_end)
121 resp := conn.do(H2ClientRequest{
122 method: 'GET'
123 authority: 'example.com'
124 path: '/hello'
125 }) or {
126 assert false, 'client do() failed: ${err}'
127 return
128 }
129 assert resp.status == 200
130 assert resp.body.bytestr() == 'echo: /hello body='
131 assert resp.headers.any(it.name == 'content-type' && it.value == 'text/plain')
132}
133
134fn test_h2_server_post_with_body() {
135 mut client_end, mut server_end := new_pipe()
136 mut handler := ServerEchoHandler{}
137 mut handler_iface := Handler(handler)
138 spawn fn [mut server_end, mut handler_iface] () {
139 mut transport := H2Transport(server_end)
140 serve_h2_conn(mut transport, mut handler_iface) or {}
141 }()
142
143 mut conn := new_h2_conn(client_end)
144 resp := conn.do(H2ClientRequest{
145 method: 'POST'
146 authority: 'svc.example'
147 path: '/upload'
148 body: 'hello world'.bytes()
149 }) or {
150 assert false, 'client do() failed: ${err}'
151 return
152 }
153 assert resp.status == 200
154 assert resp.body.bytestr() == 'echo: /upload body=hello world'
155}
156
157struct StatusHandler {
158 code int
159}
160
161fn (mut h StatusHandler) handle(req Request) Response {
162 return Response{
163 status_code: h.code
164 body: 'status ${h.code}'
165 }
166}
167
168fn test_h2_server_non_200_status() {
169 mut client_end, mut server_end := new_pipe()
170 mut handler := StatusHandler{
171 code: 404
172 }
173 mut handler_iface := Handler(handler)
174 spawn fn [mut server_end, mut handler_iface] () {
175 mut transport := H2Transport(server_end)
176 serve_h2_conn(mut transport, mut handler_iface) or {}
177 }()
178
179 mut conn := new_h2_conn(client_end)
180 resp := conn.do(H2ClientRequest{ authority: 'h.example' }) or {
181 assert false, 'client do() failed: ${err}'
182 return
183 }
184 assert resp.status == 404
185 assert resp.body.bytestr() == 'status 404'
186}
187
188struct BigBodyHandler {
189 size int
190}
191
192fn (mut h BigBodyHandler) handle(req Request) Response {
193 return Response{
194 status_code: 200
195 body: 'x'.repeat(h.size)
196 }
197}
198
199// FrameReader reads HTTP/2 frames one at a time off a PipeEnd, buffering any
200// leftover bytes between frames.
201struct FrameReader {
202mut:
203 end &PipeEnd
204 buf []u8
205}
206
207fn (mut r FrameReader) next() !H2Frame {
208 for {
209 if r.buf.len >= h2_frame_header_len {
210 hdr := h2_parse_frame_header(r.buf)!
211 total := h2_frame_header_len + int(hdr.length)
212 if r.buf.len >= total {
213 f := h2_parse_frame(hdr, r.buf[h2_frame_header_len..total])!
214 r.buf = r.buf[total..].clone()
215 return f
216 }
217 }
218 mut tmp := []u8{len: 4096}
219 n := r.end.read(mut tmp)!
220 r.buf << tmp[..n]
221 }
222 return error('unreachable')
223}
224
225fn test_h2_server_respects_send_window() {
226 mut client_end, mut server_end := new_pipe()
227 mut handler_iface := Handler(BigBodyHandler{
228 size: 100
229 })
230 spawn fn [mut server_end, mut handler_iface] () {
231 mut transport := H2Transport(server_end)
232 serve_h2_conn(mut transport, mut handler_iface) or {}
233 }()
234
235 // Raw client: preface + SETTINGS(initial_window_size = 10) + a GET, then a
236 // WINDOW_UPDATE(stream 1, +1000). The server must send at most 10 body
237 // bytes before consuming the WINDOW_UPDATE, then deliver the rest.
238 mut enc := H2HpackEncoder{}
239 block := enc.encode([
240 H2HeaderField{':method', 'GET'},
241 H2HeaderField{':scheme', 'https'},
242 H2HeaderField{':authority', 'h.example'},
243 H2HeaderField{':path', '/big'},
244 ])
245 mut out := []u8{}
246 out << h2_client_preface.bytes()
247 out << H2Frame(H2SettingsFrame{
248 settings: [H2Setting{h2_settings_initial_window_size, 10}]
249 }).encode()
250 out << H2Frame(H2HeadersFrame{
251 stream_id: 1
252 fragment: block
253 end_headers: true
254 end_stream: true
255 }).encode()
256 out << H2Frame(H2WindowUpdateFrame{
257 stream_id: 1
258 window_size_increment: 1000
259 }).encode()
260 client_end.write(out) or {
261 assert false, 'client write failed: ${err}'
262 return
263 }
264
265 mut fr := FrameReader{
266 end: client_end
267 }
268 mut body := []u8{}
269 mut status := 0
270 mut first_data_len := -1
271 mut got_end := false
272 mut dec := H2HpackDecoder{}
273 for !got_end {
274 f := fr.next() or {
275 assert false, 'frame read failed: ${err}'
276 return
277 }
278 match f {
279 H2HeadersFrame {
280 for hf in dec.decode(f.fragment) or { []H2HeaderField{} } {
281 if hf.name == ':status' {
282 status = hf.value.int()
283 }
284 }
285 if f.end_stream {
286 got_end = true
287 }
288 }
289 H2DataFrame {
290 if first_data_len < 0 {
291 first_data_len = f.data.len
292 }
293 body << f.data
294 if f.end_stream {
295 got_end = true
296 }
297 }
298 else {}
299 }
300 }
301 assert status == 200
302 assert body.len == 100
303 // The first DATA frame must not exceed the initial 10-byte window.
304 assert first_data_len <= 10
305}
306