v / vlib / net / http / h2_conn_test.v
554 lines · 525 sloc · 14.98 KB · 94a763e85cd34a51ee9c9609c445d6f9d5490ad1
Raw
1module http
2
3// Tests for the synchronous HTTP/2 client connection, driven over an in-memory
4// transport so no socket is required.
5
6// MockTransport plays a fixed script of server->client bytes and records what
7// the client writes.
8struct MockTransport {
9mut:
10 inbound []u8 // server -> client; the client reads these
11 rpos int
12 outbound []u8 // client -> server; what the client wrote
13}
14
15fn (mut m MockTransport) read(mut buf []u8) !int {
16 if m.rpos >= m.inbound.len {
17 return error('eof')
18 }
19 mut n := m.inbound.len - m.rpos
20 if n > buf.len {
21 n = buf.len
22 }
23 for i in 0 .. n {
24 buf[i] = m.inbound[m.rpos + i]
25 }
26 m.rpos += n
27 return n
28}
29
30fn (mut m MockTransport) write(buf []u8) !int {
31 m.outbound << buf
32 return buf.len
33}
34
35// build_server_stream encodes a server-side response on stream 1: SETTINGS, an
36// ACK of our SETTINGS, a HEADERS frame, and optional DATA frames.
37fn build_server_stream(resp_fields []H2HeaderField, body_chunks [][]u8) []u8 {
38 mut senc := H2HpackEncoder{}
39 hdr := senc.encode(resp_fields)
40 mut out := []u8{}
41 out << H2Frame(H2SettingsFrame{}).encode()
42 out << H2Frame(H2SettingsFrame{
43 ack: true
44 }).encode()
45 last_is_headers := body_chunks.len == 0
46 out << H2Frame(H2HeadersFrame{
47 stream_id: 1
48 fragment: hdr
49 end_headers: true
50 end_stream: last_is_headers
51 }).encode()
52 for i, chunk in body_chunks {
53 out << H2Frame(H2DataFrame{
54 stream_id: 1
55 data: chunk
56 end_stream: i == body_chunks.len - 1
57 }).encode()
58 }
59 return out
60}
61
62fn test_h2_conn_basic_get() {
63 inbound := build_server_stream([
64 H2HeaderField{':status', '200'},
65 H2HeaderField{'content-type', 'text/plain'},
66 ], [' hello'.bytes()])
67 mut t := &MockTransport{
68 inbound: inbound
69 }
70 mut c := new_h2_conn(t)
71 resp := c.do(H2ClientRequest{
72 method: 'GET'
73 authority: 'example.com'
74 path: '/'
75 })!
76 assert resp.status == 200
77 assert resp.body.bytestr() == ' hello'
78 assert resp.headers.any(it.name == 'content-type' && it.value == 'text/plain')
79
80 // The client must open with the connection preface.
81 assert t.outbound.len > h2_client_preface.len
82 assert t.outbound[..h2_client_preface.len].bytestr() == h2_client_preface
83
84 // First client frame after the preface is its SETTINGS.
85 f1, n1 := h2_read_frame(t.outbound[h2_client_preface.len..])!
86 assert f1 is H2SettingsFrame
87
88 // Next is the request HEADERS on stream 1; decode and check pseudo-headers.
89 f2, _ := h2_read_frame(t.outbound[h2_client_preface.len + n1..])!
90 headers := f2 as H2HeadersFrame
91 assert headers.stream_id == 1
92 assert headers.end_stream // GET with no body
93 mut dec := H2HpackDecoder{}
94 req_fields := dec.decode(headers.fragment)!
95 assert req_fields.any(it.name == ':method' && it.value == 'GET')
96 assert req_fields.any(it.name == ':scheme' && it.value == 'https')
97 assert req_fields.any(it.name == ':authority' && it.value == 'example.com')
98 assert req_fields.any(it.name == ':path' && it.value == '/')
99}
100
101fn test_h2_conn_multi_data_frames() {
102 inbound := build_server_stream([H2HeaderField{':status', '200'}], [
103 'foo'.bytes(),
104 'bar'.bytes(),
105 'baz'.bytes(),
106 ])
107 mut t := &MockTransport{
108 inbound: inbound
109 }
110 mut c := new_h2_conn(t)
111 resp := c.do(H2ClientRequest{ authority: 'h.example' })!
112 assert resp.status == 200
113 assert resp.body.bytestr() == 'foobarbaz'
114}
115
116fn test_h2_conn_post_body() {
117 inbound := build_server_stream([H2HeaderField{':status', '201'}], [])
118 mut t := &MockTransport{
119 inbound: inbound
120 }
121 mut c := new_h2_conn(t)
122 resp := c.do(H2ClientRequest{
123 method: 'POST'
124 authority: 'h.example'
125 path: '/submit'
126 body: 'payload-data'.bytes()
127 })!
128 assert resp.status == 201
129
130 // Walk the client's frames and confirm a DATA frame carried the body with
131 // END_STREAM, after a HEADERS frame that did not end the stream.
132 mut pos := h2_client_preface.len
133 mut saw_headers_open := false
134 mut data_payload := []u8{}
135 mut data_end := false
136 for pos < t.outbound.len {
137 frame, n := h2_read_frame(t.outbound[pos..])!
138 pos += n
139 match frame {
140 H2HeadersFrame {
141 if frame.stream_id == 1 {
142 assert !frame.end_stream
143 saw_headers_open = true
144 }
145 }
146 H2DataFrame {
147 if frame.stream_id == 1 {
148 data_payload << frame.data
149 if frame.end_stream {
150 data_end = true
151 }
152 }
153 }
154 else {}
155 }
156 }
157 assert saw_headers_open
158 assert data_end
159 assert data_payload.bytestr() == 'payload-data'
160}
161
162fn test_h2_conn_goaway_errors() {
163 mut inbound := []u8{}
164 inbound << H2Frame(H2SettingsFrame{}).encode()
165 inbound << H2Frame(H2GoawayFrame{
166 last_stream_id: 0
167 error_code: u32(H2ErrorCode.protocol_error)
168 }).encode()
169 mut t := &MockTransport{
170 inbound: inbound
171 }
172 mut c := new_h2_conn(t)
173 c.do(H2ClientRequest{ authority: 'h.example' }) or {
174 assert err.msg().contains('GOAWAY')
175 assert err.msg().contains('PROTOCOL_ERROR')
176 return
177 }
178 assert false, 'expected GOAWAY error'
179}
180
181fn test_h2_conn_rst_stream_errors() {
182 mut senc := H2HpackEncoder{}
183 mut inbound := []u8{}
184 inbound << H2Frame(H2SettingsFrame{}).encode()
185 inbound << H2Frame(H2HeadersFrame{
186 stream_id: 1
187 fragment: senc.encode([H2HeaderField{':status', '200'}])
188 end_headers: true
189 }).encode()
190 inbound << H2Frame(H2RstStreamFrame{
191 stream_id: 1
192 error_code: u32(H2ErrorCode.internal_error)
193 }).encode()
194 mut t := &MockTransport{
195 inbound: inbound
196 }
197 mut c := new_h2_conn(t)
198 c.do(H2ClientRequest{ authority: 'h.example' }) or {
199 assert err.msg().contains('reset')
200 return
201 }
202 assert false, 'expected RST_STREAM error'
203}
204
205fn test_h2_conn_continuation() {
206 // Split the response header block across HEADERS + CONTINUATION.
207 mut senc := H2HpackEncoder{}
208 full := senc.encode([
209 H2HeaderField{':status', '200'},
210 H2HeaderField{'x-test', 'continued'},
211 ])
212 split := full.len / 2
213 mut inbound := []u8{}
214 inbound << H2Frame(H2SettingsFrame{}).encode()
215 inbound << H2Frame(H2HeadersFrame{
216 stream_id: 1
217 fragment: full[..split]
218 end_headers: false
219 }).encode()
220 inbound << H2Frame(H2ContinuationFrame{
221 stream_id: 1
222 fragment: full[split..]
223 end_headers: true
224 }).encode()
225 inbound << H2Frame(H2DataFrame{
226 stream_id: 1
227 data: 'ok'.bytes()
228 end_stream: true
229 }).encode()
230 mut t := &MockTransport{
231 inbound: inbound
232 }
233 mut c := new_h2_conn(t)
234 resp := c.do(H2ClientRequest{ authority: 'h.example' })!
235 assert resp.status == 200
236 assert resp.body.bytestr() == 'ok'
237 assert resp.headers.any(it.name == 'x-test' && it.value == 'continued')
238}
239
240fn test_h2_conn_ping_ack() {
241 // A server PING before the response must be answered with a PING ACK.
242 mut senc := H2HpackEncoder{}
243 mut inbound := []u8{}
244 inbound << H2Frame(H2SettingsFrame{}).encode()
245 inbound << H2Frame(H2PingFrame{
246 data: [u8(1), 2, 3, 4, 5, 6, 7, 8]
247 }).encode()
248 inbound << H2Frame(H2HeadersFrame{
249 stream_id: 1
250 fragment: senc.encode([H2HeaderField{':status', '204'}])
251 end_headers: true
252 end_stream: true
253 }).encode()
254 mut t := &MockTransport{
255 inbound: inbound
256 }
257 mut c := new_h2_conn(t)
258 resp := c.do(H2ClientRequest{ authority: 'h.example' })!
259 assert resp.status == 204
260
261 // Find a PING ACK in the client's output carrying the same opaque data.
262 mut pos := h2_client_preface.len
263 mut saw_ping_ack := false
264 for pos < t.outbound.len {
265 frame, n := h2_read_frame(t.outbound[pos..])!
266 pos += n
267 if frame is H2PingFrame {
268 if frame.ack && frame.data == [u8(1), 2, 3, 4, 5, 6, 7, 8] {
269 saw_ping_ack = true
270 }
271 }
272 }
273 assert saw_ping_ack
274}
275
276fn test_h2_conn_splits_large_request_headers() {
277 inbound := build_server_stream([H2HeaderField{':status', '200'}], [])
278 mut t := &MockTransport{
279 inbound: inbound
280 }
281 mut c := new_h2_conn(t)
282 // A header value larger than the default 16 KiB max frame size forces the
283 // request header block to be split across HEADERS + CONTINUATION frames.
284 big := 'x'.repeat(20000)
285 resp := c.do(H2ClientRequest{
286 authority: 'h.example'
287 headers: [H2HeaderField{'x-big', big}]
288 })!
289 assert resp.status == 200
290
291 mut pos := h2_client_preface.len
292 mut headers_open := false
293 mut continuation_end := false
294 mut block := []u8{}
295 for pos < t.outbound.len {
296 frame, n := h2_read_frame(t.outbound[pos..])!
297 pos += n
298 match frame {
299 H2HeadersFrame {
300 if frame.stream_id == 1 {
301 assert !frame.end_headers // split, so END_HEADERS not on HEADERS
302 headers_open = true
303 block << frame.fragment
304 }
305 }
306 H2ContinuationFrame {
307 if frame.stream_id == 1 {
308 block << frame.fragment
309 if frame.end_headers {
310 continuation_end = true
311 }
312 }
313 }
314 else {}
315 }
316 }
317 assert headers_open
318 assert continuation_end
319 // The reassembled block must decode back to the original header.
320 mut dec := H2HpackDecoder{}
321 fields := dec.decode(block)!
322 assert fields.any(it.name == 'x-big' && it.value == big)
323}
324
325fn test_h2_conn_large_body_flow_control() {
326 // Body larger than the initial 64 KiB window: the client sends up to the
327 // window, waits, then resumes once the peer grows both the connection and
328 // stream windows.
329 body := 'a'.repeat(70000)
330 mut inbound := []u8{}
331 inbound << H2Frame(H2SettingsFrame{}).encode()
332 inbound << H2Frame(H2WindowUpdateFrame{
333 stream_id: 0
334 window_size_increment: 70000
335 }).encode()
336 inbound << H2Frame(H2WindowUpdateFrame{
337 stream_id: 1
338 window_size_increment: 70000
339 }).encode()
340 mut senc := H2HpackEncoder{}
341 inbound << H2Frame(H2HeadersFrame{
342 stream_id: 1
343 fragment: senc.encode([H2HeaderField{':status', '200'}])
344 end_headers: true
345 end_stream: true
346 }).encode()
347 mut t := &MockTransport{
348 inbound: inbound
349 }
350 mut c := new_h2_conn(t)
351 resp := c.do(H2ClientRequest{
352 method: 'POST'
353 authority: 'h.example'
354 path: '/upload'
355 body: body.bytes()
356 })!
357 assert resp.status == 200
358
359 // The client must have sent the entire body, ending with END_STREAM, and no
360 // single DATA frame may exceed the max frame size.
361 mut pos := h2_client_preface.len
362 mut sent := 0
363 mut ended := false
364 for pos < t.outbound.len {
365 frame, n := h2_read_frame(t.outbound[pos..])!
366 pos += n
367 if frame is H2DataFrame {
368 if frame.stream_id == 1 {
369 assert frame.data.len <= int(h2_default_max_frame_size)
370 sent += frame.data.len
371 if frame.end_stream {
372 ended = true
373 }
374 }
375 }
376 }
377 assert sent == body.len
378 assert ended
379}
380
381fn test_h2_fetch_glue_roundtrip() {
382 // Drive the full conversion glue (Request -> H2 -> Response) over the mock
383 // transport, without a socket.
384 inbound := build_server_stream([
385 H2HeaderField{':status', '200'},
386 H2HeaderField{'content-type', 'text/plain'},
387 ], [' world'.bytes()])
388 mut t := &MockTransport{
389 inbound: inbound
390 }
391 req := Request{
392 user_agent: 'v.http'
393 }
394 h2req := req.to_h2_request(.get, 'example.com', '/', '', new_header())
395 mut c := new_h2_conn(t)
396 h2resp := c.do(h2req)!
397 resp := h2_response_to_http(h2resp)
398 assert resp.status_code == 200
399 assert resp.version() == .v2_0
400 assert resp.body == ' world'
401 assert (resp.header.get_custom('content-type') or { '' }) == 'text/plain'
402}
403
404// build_streamed_response builds a server stream that delivers the response
405// body in fixed-size chunks, useful for streaming tests.
406fn build_streamed_response(status string, content_length string, chunks [][]u8) []u8 {
407 mut senc := H2HpackEncoder{}
408 mut fields := [H2HeaderField{':status', status}]
409 if content_length != '' {
410 fields << H2HeaderField{'content-length', content_length}
411 }
412 hdr := senc.encode(fields)
413 mut out := []u8{}
414 out << H2Frame(H2SettingsFrame{}).encode()
415 out << H2Frame(H2SettingsFrame{
416 ack: true
417 }).encode()
418 last_is_headers := chunks.len == 0
419 out << H2Frame(H2HeadersFrame{
420 stream_id: 1
421 fragment: hdr
422 end_headers: true
423 end_stream: last_is_headers
424 }).encode()
425 for i, chunk in chunks {
426 out << H2Frame(H2DataFrame{
427 stream_id: 1
428 data: chunk
429 end_stream: i == chunks.len - 1
430 }).encode()
431 }
432 return out
433}
434
435// ChunkCapture records on_data invocations via a reference captured by the
436// returned closure.
437struct ChunkCapture {
438mut:
439 chunks [][]u8
440 running []u64
441 expected []u64
442 status []int
443}
444
445fn make_capture_fn(cap &ChunkCapture) H2DataFn {
446 return fn [cap] (chunk []u8, body_so_far u64, body_expected u64, status int) ! {
447 unsafe {
448 cap.chunks << chunk.clone()
449 cap.running << body_so_far
450 cap.expected << body_expected
451 cap.status << status
452 }
453 }
454}
455
456fn test_h2_on_data_fires_per_chunk() {
457 mut cap := &ChunkCapture{}
458 inbound := build_streamed_response('200', '12', [
459 'foo'.bytes(),
460 'bar'.bytes(),
461 'baz quux'.bytes(),
462 ])
463 mut t := &MockTransport{
464 inbound: inbound
465 }
466 mut c := new_h2_conn(t)
467 resp := c.do(H2ClientRequest{
468 authority: 'h.example'
469 on_data: make_capture_fn(cap)
470 })!
471 assert resp.status == 200
472 assert resp.body.bytestr() == 'foobarbaz quux'
473 // Three DATA frames -> three callback invocations.
474 assert cap.chunks.len == 3
475 assert cap.chunks[0].bytestr() == 'foo'
476 assert cap.chunks[1].bytestr() == 'bar'
477 assert cap.chunks[2].bytestr() == 'baz quux'
478 // body_so_far is cumulative including the current chunk.
479 assert cap.running == [u64(3), u64(6), u64(14)]
480 // content-length is reported.
481 assert cap.expected == [u64(12), u64(12), u64(12)]
482 // Status was known by the first callback (headers arrived first).
483 assert cap.status == [200, 200, 200]
484}
485
486fn test_h2_stop_copying_limit_caps_body_but_keeps_callback() {
487 mut cap := &ChunkCapture{}
488 inbound := build_streamed_response('200', '', [
489 'AAAA'.bytes(),
490 'BBBB'.bytes(),
491 'CCCC'.bytes(),
492 ])
493 mut t := &MockTransport{
494 inbound: inbound
495 }
496 mut c := new_h2_conn(t)
497 resp := c.do(H2ClientRequest{
498 authority: 'h.example'
499 on_data: make_capture_fn(cap)
500 stop_copying_limit: 6
501 })!
502 // Body is capped at 6 bytes (4 from first chunk + 2 from second).
503 assert resp.body.bytestr() == 'AAAABB'
504 // All three chunks still produced callbacks.
505 assert cap.chunks.len == 3
506 assert cap.chunks[0].bytestr() == 'AAAA'
507 assert cap.chunks[1].bytestr() == 'BBBB'
508 assert cap.chunks[2].bytestr() == 'CCCC'
509 // body_so_far still reports the true cumulative size.
510 assert cap.running == [u64(4), u64(8), u64(12)]
511}
512
513fn test_h2_stop_receiving_limit_breaks_early() {
514 mut cap := &ChunkCapture{}
515 inbound := build_streamed_response('200', '', [
516 'XXXX'.bytes(),
517 'YYYY'.bytes(),
518 'ZZZZ'.bytes(), // should never be delivered
519 ])
520 mut t := &MockTransport{
521 inbound: inbound
522 }
523 mut c := new_h2_conn(t)
524 resp := c.do(H2ClientRequest{
525 authority: 'h.example'
526 on_data: make_capture_fn(cap)
527 stop_receiving_limit: 6
528 })!
529 // Loop breaks after the second DATA frame (8 bytes >= limit 6); body
530 // contains both chunks delivered so far.
531 assert resp.body.bytestr() == 'XXXXYYYY'
532 assert cap.chunks.len == 2
533 assert cap.chunks[1].bytestr() == 'YYYY'
534
535 // On early termination the client must send RST_STREAM(CANCEL) on the
536 // stream, and the connection must refuse further requests.
537 mut saw_cancel := false
538 mut pos := h2_client_preface.len
539 for pos < t.outbound.len {
540 frame, n := h2_read_frame(t.outbound[pos..])!
541 pos += n
542 if frame is H2RstStreamFrame {
543 if frame.stream_id == 1 && frame.error_code == u32(H2ErrorCode.cancel) {
544 saw_cancel = true
545 }
546 }
547 }
548 assert saw_cancel, 'expected RST_STREAM(CANCEL) on early termination'
549 c.do(H2ClientRequest{ authority: 'h.example' }) or {
550 assert err.msg().contains('no longer usable')
551 return
552 }
553 assert false, 'expected error on reuse after early termination'
554}
555