vxx2 / vlib / net / http / h2_mux_conn_test.v
2123 lines · 2060 sloc · 62.44 KB · 89607c731290ee2c148c11541f73c40b23b00481
Raw
1// Hermetic tests for the multiplexed HTTP/2 client connection (h2_mux_conn.v):
2// concurrent interleaved streams, flow-control blocking, GOAWAY mid-flight,
3// per-stream cancellation that must not poison the connection, CONTINUATION
4// assembly, and waking every waiter when the connection dies. The peer is a
5// scripted in-process HTTP/2 server over an in-memory blocking pipe.
6module http
7
8import sync
9import time
10
11// MuxPipeBuf is a one-way in-memory FIFO with blocking, socket-like reads.
12struct MuxPipeBuf {
13mut:
14 mu &sync.Mutex = sync.new_mutex()
15 data []u8
16 closed bool
17}
18
19fn (mut p MuxPipeBuf) 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 MuxPipeBuf) 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
56fn (mut p MuxPipeBuf) close() {
57 p.mu.lock()
58 p.closed = true
59 p.mu.unlock()
60}
61
62// MuxPipeEnd is one half of a bidirectional pipe.
63@[heap]
64struct MuxPipeEnd {
65mut:
66 incoming &MuxPipeBuf
67 outgoing &MuxPipeBuf
68}
69
70fn (mut p MuxPipeEnd) read(mut buf []u8) !int {
71 return p.incoming.read(mut buf)!
72}
73
74fn (mut p MuxPipeEnd) write(buf []u8) !int {
75 return p.outgoing.write(buf)!
76}
77
78fn (mut p MuxPipeEnd) close_both() {
79 p.incoming.close()
80 p.outgoing.close()
81}
82
83fn new_mux_pipe() (&MuxPipeEnd, &MuxPipeEnd) {
84 mut a := &MuxPipeBuf{}
85 mut b := &MuxPipeBuf{}
86 client := &MuxPipeEnd{
87 incoming: b
88 outgoing: a
89 }
90 server := &MuxPipeEnd{
91 incoming: a
92 outgoing: b
93 }
94 return client, server
95}
96
97// new_test_mux_conn builds a mux conn over the client pipe end with a real
98// (non-nil) close_transport, satisfying new_h2_mux_conn's required-closer
99// contract. The closer closes the in-memory pipe, mirroring how a real transport
100// adapter would close its socket so teardown wakes the blocked reader.
101fn new_test_mux_conn(mut cend MuxPipeEnd) &H2MuxConn {
102 return new_h2_mux_conn(cend, fn [mut cend] () {
103 cend.close_both()
104 })
105}
106
107// MuxTestPeer is the scripted server side: it parses real frames off the pipe
108// (with its own HPACK state) and records what it saw for the test to assert.
109@[heap]
110struct MuxTestPeer {
111mut:
112 end &MuxPipeEnd
113 rbuf []u8
114 decoder H2HpackDecoder
115 encoder H2HpackEncoder
116 mu &sync.Mutex = sync.new_mutex()
117 // stream id -> request :path, in arrival order
118 paths map[u32]string
119 stream_ids []u32
120 failure string
121 data_total map[u32]u64 // DATA bytes received per stream
122 rst_streams []u32
123 conn_window_updates u64 // sum of WINDOW_UPDATE increments on stream 0
124}
125
126fn (mut p MuxTestPeer) fail(msg string) {
127 p.mu.lock()
128 if p.failure == '' {
129 p.failure = msg
130 }
131 p.mu.unlock()
132}
133
134fn (mut p MuxTestPeer) failure_msg() string {
135 p.mu.lock()
136 defer {
137 p.mu.unlock()
138 }
139 return p.failure
140}
141
142fn (mut p MuxTestPeer) read_exact(n int) ![]u8 {
143 for p.rbuf.len < n {
144 mut tmp := []u8{len: 4096}
145 got := p.end.read(mut tmp)!
146 if got <= 0 {
147 return error('peer: pipe closed')
148 }
149 p.rbuf << tmp[..got]
150 }
151 out := p.rbuf[..n].clone()
152 p.rbuf = p.rbuf[n..].clone()
153 return out
154}
155
156fn (mut p MuxTestPeer) read_preface() ! {
157 got := p.read_exact(h2_client_preface.len)!
158 if got.bytestr() != h2_client_preface {
159 return error('peer: bad connection preface')
160 }
161}
162
163fn (mut p MuxTestPeer) next_frame() !H2Frame {
164 head := p.read_exact(h2_frame_header_len)!
165 header := h2_parse_frame_header(head)!
166 payload := p.read_exact(int(header.length))!
167 return h2_parse_frame(header, payload)!
168}
169
170fn (mut p MuxTestPeer) write_frame(f H2Frame) ! {
171 p.end.write(f.encode())!
172}
173
174// pump consumes one client frame, updating the peer's records. HEADERS blocks
175// are decoded (tracking :path); WINDOW_UPDATE / SETTINGS / DATA / RST are
176// tallied. Returns the frame.
177fn (mut p MuxTestPeer) pump() !H2Frame {
178 f := p.next_frame()!
179 match f {
180 H2SettingsFrame {
181 if !f.ack {
182 p.write_frame(H2SettingsFrame{
183 ack: true
184 })!
185 }
186 }
187 H2HeadersFrame {
188 mut fragment := f.fragment.clone()
189 if !f.end_headers {
190 for {
191 cont := p.next_frame()!
192 if cont is H2ContinuationFrame {
193 fragment << cont.fragment
194 if cont.end_headers {
195 break
196 }
197 } else {
198 return error('peer: expected CONTINUATION')
199 }
200 }
201 }
202 fields := p.decoder.decode(fragment)!
203 mut path := ''
204 for fld in fields {
205 if fld.name == ':path' {
206 path = fld.value
207 }
208 }
209 p.mu.lock()
210 p.paths[f.stream_id] = path
211 p.stream_ids << f.stream_id
212 p.mu.unlock()
213 }
214 H2DataFrame {
215 p.mu.lock()
216 p.data_total[f.stream_id] = (p.data_total[f.stream_id] or { u64(0) }) + u64(f.data.len)
217 p.mu.unlock()
218 }
219 H2RstStreamFrame {
220 p.mu.lock()
221 p.rst_streams << f.stream_id
222 p.mu.unlock()
223 }
224 H2WindowUpdateFrame {
225 if f.stream_id == 0 {
226 p.mu.lock()
227 p.conn_window_updates += u64(f.window_size_increment)
228 p.mu.unlock()
229 }
230 }
231 else {}
232 }
233
234 return f
235}
236
237// wait_for_headers pumps until `n` request header blocks have arrived,
238// returning the stream ids in arrival order.
239fn (mut p MuxTestPeer) wait_for_headers(n int) ![]u32 {
240 for {
241 p.mu.lock()
242 if p.stream_ids.len >= n {
243 ids := p.stream_ids.clone()
244 p.mu.unlock()
245 return ids
246 }
247 p.mu.unlock()
248 p.pump()!
249 }
250 return []u32{}
251}
252
253// respond_headers sends a 200 response header block for `stream_id`.
254fn (mut p MuxTestPeer) respond_headers(stream_id u32, end_stream bool) ! {
255 block := p.encoder.encode([H2HeaderField{':status', '200'},
256 H2HeaderField{'content-type', 'text/plain'}])
257 p.write_frame(H2HeadersFrame{
258 stream_id: stream_id
259 fragment: block
260 end_headers: true
261 end_stream: end_stream
262 })!
263}
264
265// --- worker plumbing ----------------------------------------------------------
266
267@[heap]
268struct MuxResults {
269mut:
270 mu &sync.Mutex = sync.new_mutex()
271 bodies map[string]string
272 statuses map[string]int
273 errs map[string]string
274 codes map[string]int
275}
276
277fn (mut r MuxResults) set_ok(path string, resp H2ClientResponse) {
278 r.mu.lock()
279 r.bodies[path] = resp.body.bytestr()
280 r.statuses[path] = resp.status
281 r.mu.unlock()
282}
283
284fn (mut r MuxResults) set_err(path string, e IError) {
285 r.mu.lock()
286 r.errs[path] = e.msg()
287 r.codes[path] = e.code()
288 r.mu.unlock()
289}
290
291fn mux_worker(mut c H2MuxConn, req H2ClientRequest, mut out MuxResults) {
292 resp := c.do(req) or {
293 out.set_err(req.path, err)
294 return
295 }
296 out.set_ok(req.path, resp)
297}
298
299// --- tests --------------------------------------------------------------------
300
301// Three concurrent GETs on one connection; the peer interleaves their DATA
302// frames. Each requester must receive exactly its own body.
303fn test_mux_concurrent_interleaved_streams() {
304 mut cend, mut pend := new_mux_pipe()
305 mut conn := new_test_mux_conn(mut cend)
306 mut peer := &MuxTestPeer{
307 end: pend
308 }
309 mut out := &MuxResults{}
310 mut workers := []thread{}
311 for i in 0 .. 3 {
312 workers << spawn mux_worker(mut conn, H2ClientRequest{ authority: 't', path: '/w${i}' }, mut out)
313 }
314 peer_thread := spawn fn (mut peer MuxTestPeer) {
315 peer.read_preface() or {
316 peer.fail('preface: ${err.msg()}')
317 return
318 }
319 ids := peer.wait_for_headers(3) or {
320 peer.fail('headers: ${err.msg()}')
321 return
322 }
323 for id in ids {
324 peer.respond_headers(id, false) or {
325 peer.fail('respond: ${err.msg()}')
326 return
327 }
328 }
329 // Interleave the bodies: one chunk per stream per round.
330 for round in 0 .. 2 {
331 for id in ids {
332 peer.mu.lock()
333 path := peer.paths[id] or { '' }
334 peer.mu.unlock()
335 peer.write_frame(H2DataFrame{
336 stream_id: id
337 data: '${path}#${round};'.bytes()
338 end_stream: round == 1
339 }) or {
340 peer.fail('data: ${err.msg()}')
341 return
342 }
343 }
344 }
345 }(mut peer)
346 workers.wait()
347 peer_thread.wait()
348 assert peer.failure_msg() == ''
349 assert out.errs.len == 0, 'worker errors: ${out.errs}'
350 for i in 0 .. 3 {
351 assert out.statuses['/w${i}'] == 200
352 assert out.bodies['/w${i}'] == '/w${i}#0;/w${i}#1;'
353 }
354 cend.close_both()
355}
356
357// A large request body must block on the 65535-byte connection/stream send
358// windows and resume when the peer grants WINDOW_UPDATEs.
359fn test_mux_flow_control_blocks_and_resumes() {
360 mut cend, mut pend := new_mux_pipe()
361 mut conn := new_test_mux_conn(mut cend)
362 mut peer := &MuxTestPeer{
363 end: pend
364 }
365 mut out := &MuxResults{}
366 body_len := 100000
367 mut workers := []thread{}
368 workers << spawn mux_worker(mut conn, H2ClientRequest{
369 method: 'POST'
370 authority: 't'
371 path: '/up'
372 body: []u8{len: body_len, init: `B`}
373 }, mut out)
374 peer_thread := spawn fn [body_len] (mut peer MuxTestPeer) {
375 peer.read_preface() or {
376 peer.fail('preface: ${err.msg()}')
377 return
378 }
379 ids := peer.wait_for_headers(1) or {
380 peer.fail('headers: ${err.msg()}')
381 return
382 }
383 id := ids[0]
384 // Drain DATA until the client exhausts the 65535-byte windows.
385 for {
386 peer.mu.lock()
387 got := peer.data_total[id] or { u64(0) }
388 peer.mu.unlock()
389 if got >= u64(h2_default_initial_window) {
390 break
391 }
392 peer.pump() or {
393 peer.fail('pump: ${err.msg()}')
394 return
395 }
396 }
397 peer.mu.lock()
398 at_block := peer.data_total[id] or { u64(0) }
399 peer.mu.unlock()
400 if at_block != u64(h2_default_initial_window) {
401 peer.fail('expected the client to stop at exactly 65535 sent bytes, got ${at_block}')
402 return
403 }
404 // Grant room on both windows; the client must finish the body.
405 peer.write_frame(H2WindowUpdateFrame{
406 stream_id: 0
407 window_size_increment: u32(body_len)
408 }) or {
409 peer.fail('wu0: ${err.msg()}')
410 return
411 }
412 peer.write_frame(H2WindowUpdateFrame{
413 stream_id: id
414 window_size_increment: u32(body_len)
415 }) or {
416 peer.fail('wu: ${err.msg()}')
417 return
418 }
419 for {
420 peer.mu.lock()
421 got := peer.data_total[id] or { u64(0) }
422 peer.mu.unlock()
423 if got >= u64(body_len) {
424 break
425 }
426 peer.pump() or {
427 peer.fail('pump2: ${err.msg()}')
428 return
429 }
430 }
431 peer.respond_headers(id, true) or {
432 peer.fail('respond: ${err.msg()}')
433 return
434 }
435 }(mut peer)
436 workers.wait()
437 peer_thread.wait()
438 assert peer.failure_msg() == ''
439 assert out.errs.len == 0, 'worker errors: ${out.errs}'
440 assert out.statuses['/up'] == 200
441 cend.close_both()
442}
443
444// A peer that RST_STREAMs an upload after the client has exhausted its send
445// windows must wake the body sender blocked on flow-control credit, so do()
446// returns the reset error instead of hanging forever.
447fn test_mux_rst_wakes_blocked_body_sender() {
448 mut cend, mut pend := new_mux_pipe()
449 mut conn := new_test_mux_conn(mut cend)
450 mut peer := &MuxTestPeer{
451 end: pend
452 }
453 mut out := &MuxResults{}
454 body_len := 100000
455 mut workers := []thread{}
456 workers << spawn mux_worker(mut conn, H2ClientRequest{
457 method: 'POST'
458 authority: 't'
459 path: '/up'
460 body: []u8{len: body_len, init: `B`}
461 }, mut out)
462 peer_thread := spawn fn (mut peer MuxTestPeer) {
463 peer.read_preface() or {
464 peer.fail('preface: ${err.msg()}')
465 return
466 }
467 ids := peer.wait_for_headers(1) or {
468 peer.fail('headers: ${err.msg()}')
469 return
470 }
471 id := ids[0]
472 // Let the client send until both windows are exhausted; it then blocks
473 // in send_body_on_stream waiting for a WINDOW_UPDATE that never comes.
474 for {
475 peer.mu.lock()
476 got := peer.data_total[id] or { u64(0) }
477 peer.mu.unlock()
478 if got >= u64(h2_default_initial_window) {
479 break
480 }
481 peer.pump() or {
482 peer.fail('pump: ${err.msg()}')
483 return
484 }
485 }
486 // Reset the stream instead of granting credit.
487 peer.write_frame(H2RstStreamFrame{
488 stream_id: id
489 error_code: u32(H2ErrorCode.cancel)
490 }) or {
491 peer.fail('rst: ${err.msg()}')
492 return
493 }
494 }(mut peer)
495 workers.wait()
496 peer_thread.wait()
497 assert peer.failure_msg() == ''
498 upload_err := out.errs['/up'] or { '' }
499 assert upload_err != '', 'expected the reset upload to fail, not hang'
500 assert upload_err.contains('reset'), 'expected a reset error, got: ${upload_err}'
501 cend.close_both()
502}
503
504// An early final response that carries a body ends with END_STREAM on the DATA
505// frame, not on HEADERS. A body sender blocked on flow control must still be
506// woken (by on_response_data), abandon the upload with RST_STREAM, and have the
507// response delivered — not hang waiting for a WINDOW_UPDATE that never comes.
508fn test_mux_early_final_response_with_body_wakes_blocked_upload() {
509 mut cend, mut pend := new_mux_pipe()
510 mut conn := new_test_mux_conn(mut cend)
511 mut peer := &MuxTestPeer{
512 end: pend
513 }
514 mut out := &MuxResults{}
515 body_len := 100000
516 mut workers := []thread{}
517 workers << spawn mux_worker(mut conn, H2ClientRequest{
518 method: 'POST'
519 authority: 't'
520 path: '/up'
521 body: []u8{len: body_len, init: `B`}
522 }, mut out)
523 peer_thread := spawn fn (mut peer MuxTestPeer) {
524 peer.read_preface() or {
525 peer.fail('preface: ${err.msg()}')
526 return
527 }
528 ids := peer.wait_for_headers(1) or {
529 peer.fail('headers: ${err.msg()}')
530 return
531 }
532 id := ids[0]
533 // Let the client exhaust both send windows; it then blocks in
534 // send_body_on_stream waiting for a WINDOW_UPDATE that never comes.
535 for {
536 peer.mu.lock()
537 got := peer.data_total[id] or { u64(0) }
538 peer.mu.unlock()
539 if got >= u64(h2_default_initial_window) {
540 break
541 }
542 peer.pump() or {
543 peer.fail('pump: ${err.msg()}')
544 return
545 }
546 }
547 // Send a complete final response WITH a body, so END_STREAM lands on the
548 // DATA frame rather than HEADERS. No WINDOW_UPDATE is ever granted.
549 block := peer.encoder.encode([H2HeaderField{':status', '413'},
550 H2HeaderField{'content-type', 'text/plain'}])
551 peer.write_frame(H2HeadersFrame{
552 stream_id: id
553 fragment: block
554 end_headers: true
555 end_stream: false
556 }) or {
557 peer.fail('resp headers: ${err.msg()}')
558 return
559 }
560 peer.write_frame(H2DataFrame{
561 stream_id: id
562 data: 'rejected'.bytes()
563 end_stream: true
564 }) or {
565 peer.fail('resp data: ${err.msg()}')
566 return
567 }
568 // The client must wake, abandon the upload, and close its half with
569 // RST_STREAM; pump until we observe it (or the pipe closes).
570 for {
571 f := peer.pump() or { return }
572 if f is H2RstStreamFrame {
573 return
574 }
575 }
576 }(mut peer)
577 workers.wait()
578 peer_thread.wait()
579 assert peer.failure_msg() == ''
580 upload_err := out.errs['/up'] or { '' }
581 assert upload_err == '', 'early final response must not error the request, got: ${upload_err}'
582 assert out.statuses['/up'] or { 0 } == 413, 'expected the 413 response to be delivered'
583 assert out.bodies['/up'] or { '' } == 'rejected', 'expected the response body to be delivered'
584 peer.mu.lock()
585 saw_rst := u32(1) in peer.rst_streams
586 peer.mu.unlock()
587 assert saw_rst, 'client must RST_STREAM to close its abandoned upload half'
588 cend.close_both()
589}
590
591// RFC 9113 §8.3.1: a response HEADERS block without a valid :status is malformed.
592// The client must reset the stream (PROTOCOL_ERROR) and surface an error to the
593// requester rather than treating it as a 1xx interim and waiting forever.
594fn test_mux_response_missing_status_resets_stream() {
595 mut cend, mut pend := new_mux_pipe()
596 mut conn := new_test_mux_conn(mut cend)
597 mut peer := &MuxTestPeer{
598 end: pend
599 }
600 mut out := &MuxResults{}
601 mut workers := []thread{}
602 workers << spawn mux_worker(mut conn, H2ClientRequest{
603 method: 'GET'
604 authority: 't'
605 path: '/get'
606 }, mut out)
607 peer_thread := spawn fn (mut peer MuxTestPeer) {
608 peer.read_preface() or {
609 peer.fail('preface: ${err.msg()}')
610 return
611 }
612 ids := peer.wait_for_headers(1) or {
613 peer.fail('headers: ${err.msg()}')
614 return
615 }
616 id := ids[0]
617 // A response header block with no :status pseudo-header (malformed); the
618 // stream is left open so the client cannot fall back to "stream closed".
619 block := peer.encoder.encode([H2HeaderField{'content-type', 'text/plain'}])
620 peer.write_frame(H2HeadersFrame{
621 stream_id: id
622 fragment: block
623 end_headers: true
624 end_stream: false
625 }) or {
626 peer.fail('resp headers: ${err.msg()}')
627 return
628 }
629 // The client must reset the stream; pump until we observe the RST.
630 for {
631 f := peer.pump() or { return }
632 if f is H2RstStreamFrame {
633 return
634 }
635 }
636 }(mut peer)
637 workers.wait()
638 peer_thread.wait()
639 assert peer.failure_msg() == ''
640 get_err := out.errs['/get'] or { '' }
641 assert get_err != '', 'expected an error for a response missing :status, not a hang'
642 assert get_err.contains('status'), 'expected a :status protocol error, got: ${get_err}'
643 peer.mu.lock()
644 saw_rst := u32(1) in peer.rst_streams
645 peer.mu.unlock()
646 assert saw_rst, 'client must RST_STREAM a malformed (no :status) response'
647 cend.close_both()
648}
649
650// string.int() is lenient ('200 OK' -> 200), so a non-three-digit :status must
651// be rejected by an explicit digit check rather than accepted as a 200 success.
652fn test_mux_response_malformed_status_resets_stream() {
653 mut cend, mut pend := new_mux_pipe()
654 mut conn := new_test_mux_conn(mut cend)
655 mut peer := &MuxTestPeer{
656 end: pend
657 }
658 mut out := &MuxResults{}
659 mut workers := []thread{}
660 workers << spawn mux_worker(mut conn, H2ClientRequest{
661 method: 'GET'
662 authority: 't'
663 path: '/get'
664 }, mut out)
665 peer_thread := spawn fn (mut peer MuxTestPeer) {
666 peer.read_preface() or {
667 peer.fail('preface: ${err.msg()}')
668 return
669 }
670 ids := peer.wait_for_headers(1) or {
671 peer.fail('headers: ${err.msg()}')
672 return
673 }
674 id := ids[0]
675 // A :status that int() would parse leniently to 200, but is malformed.
676 block := peer.encoder.encode([H2HeaderField{':status', '200 OK'},
677 H2HeaderField{'content-type', 'text/plain'}])
678 peer.write_frame(H2HeadersFrame{
679 stream_id: id
680 fragment: block
681 end_headers: true
682 end_stream: false
683 }) or {
684 peer.fail('resp headers: ${err.msg()}')
685 return
686 }
687 for {
688 f := peer.pump() or { return }
689 if f is H2RstStreamFrame {
690 return
691 }
692 }
693 }(mut peer)
694 workers.wait()
695 peer_thread.wait()
696 assert peer.failure_msg() == ''
697 get_err := out.errs['/get'] or { '' }
698 assert get_err != '', 'expected an error for a malformed :status, not a success'
699 assert get_err.contains('status'), 'expected a :status protocol error, got: ${get_err}'
700 delivered := out.statuses['/get'] or { -1 }
701 assert delivered == -1, 'a malformed :status must not be delivered as a success (got ${delivered})'
702 cend.close_both()
703}
704
705// A stream reset with REFUSED_STREAM means the server did not process the
706// request, so it must surface a retryable error (RFC 7540 8.1.4) — even for a
707// non-idempotent method — so the pool can replay it on a fresh connection. A
708// reset with any other code (e.g. CANCEL) must stay non-retryable.
709fn test_mux_refused_stream_reset_is_retryable() {
710 mut cend, mut pend := new_mux_pipe()
711 mut conn := new_test_mux_conn(mut cend)
712 mut peer := &MuxTestPeer{
713 end: pend
714 }
715 mut out := &MuxResults{}
716 mut workers := []thread{}
717 workers << spawn mux_worker(mut conn, H2ClientRequest{
718 method: 'POST'
719 authority: 't'
720 path: '/refused'
721 body: 'x'.bytes()
722 }, mut out)
723 peer_thread := spawn fn (mut peer MuxTestPeer) {
724 peer.read_preface() or {
725 peer.fail('preface: ${err.msg()}')
726 return
727 }
728 ids := peer.wait_for_headers(1) or {
729 peer.fail('headers: ${err.msg()}')
730 return
731 }
732 peer.write_frame(H2RstStreamFrame{
733 stream_id: ids[0]
734 error_code: u32(H2ErrorCode.refused_stream)
735 }) or {
736 peer.fail('rst: ${err.msg()}')
737 return
738 }
739 }(mut peer)
740 workers.wait()
741 peer_thread.wait()
742 assert peer.failure_msg() == ''
743 assert out.errs['/refused'] or { '' } != '', 'expected the refused POST to fail'
744 assert out.codes['/refused'] or { 0 } == h2_err_retryable_code, 'REFUSED_STREAM reset must be retryable, got code ${out.codes['/refused']}'
745 cend.close_both()
746}
747
748// Padding in a DATA frame counts toward flow control (RFC 7540 6.9.1) even
749// though it is never delivered to the app. The client must credit the full
750// received payload (pad-length byte + data + padding) back via WINDOW_UPDATE,
751// or the connection receive window leaks and eventually stalls.
752fn test_mux_padded_data_credits_full_flow_control() {
753 mut cend, mut pend := new_mux_pipe()
754 mut conn := new_test_mux_conn(mut cend)
755 mut peer := &MuxTestPeer{
756 end: pend
757 }
758 mut out := &MuxResults{}
759 data := 'hi'.bytes()
760 pad := []u8{len: 100}
761 payload_len := 1 + data.len + pad.len // pad-length byte + data + padding
762 mut workers := []thread{}
763 workers << spawn mux_worker(mut conn, H2ClientRequest{ authority: 't', path: '/p' }, mut out)
764 peer_thread := spawn fn [data, pad, payload_len] (mut peer MuxTestPeer) {
765 peer.read_preface() or {
766 peer.fail('preface: ${err.msg()}')
767 return
768 }
769 ids := peer.wait_for_headers(1) or {
770 peer.fail('headers: ${err.msg()}')
771 return
772 }
773 peer.respond_headers(ids[0], false) or {
774 peer.fail('respond: ${err.msg()}')
775 return
776 }
777 mut payload := [u8(pad.len)]
778 payload << data
779 payload << pad
780 raw := h2_frame_bytes(h2_frame_data, h2_flag_padded | h2_flag_end_stream, ids[0], payload)
781 peer.end.write(raw) or {
782 peer.fail('data: ${err.msg()}')
783 return
784 }
785 // Pump until the connection-level WINDOW_UPDATEs sum to the full padded
786 // payload; with the fix this terminates (data + padding both credited).
787 for {
788 peer.mu.lock()
789 got := peer.conn_window_updates
790 peer.mu.unlock()
791 if got >= u64(payload_len) {
792 break
793 }
794 peer.pump() or {
795 peer.fail('pump: ${err.msg()}')
796 return
797 }
798 }
799 }(mut peer)
800 workers.wait()
801 peer_thread.wait()
802 assert peer.failure_msg() == ''
803 assert out.statuses['/p'] == 200
804 assert out.bodies['/p'] == 'hi'
805 assert peer.conn_window_updates == u64(payload_len), 'padding must count toward connection flow control'
806 cend.close_both()
807}
808
809// A received PUSH_PROMISE must fail the connection: we advertise ENABLE_PUSH=0,
810// so it is a PROTOCOL_ERROR (RFC 7540 6.6 / 8.2). The peer also sends a valid
811// response — with the guard the request fails; without it the push is ignored
812// and the request would wrongly succeed.
813fn test_mux_push_promise_fails_connection() {
814 mut cend, mut pend := new_mux_pipe()
815 mut conn := new_test_mux_conn(mut cend)
816 mut peer := &MuxTestPeer{
817 end: pend
818 }
819 mut out := &MuxResults{}
820 mut workers := []thread{}
821 workers << spawn mux_worker(mut conn, H2ClientRequest{ authority: 't', path: '/pp' }, mut out)
822 peer_thread := spawn fn (mut peer MuxTestPeer) {
823 peer.read_preface() or {
824 peer.fail('preface: ${err.msg()}')
825 return
826 }
827 ids := peer.wait_for_headers(1) or {
828 peer.fail('headers: ${err.msg()}')
829 return
830 }
831 block := peer.encoder.encode([H2HeaderField{':status', '200'}])
832 peer.write_frame(H2PushPromiseFrame{
833 stream_id: ids[0]
834 promised_stream_id: 2
835 fragment: block
836 end_headers: true
837 }) or {
838 peer.fail('push: ${err.msg()}')
839 return
840 }
841 peer.respond_headers(ids[0], true) or {}
842 }(mut peer)
843 workers.wait()
844 peer_thread.wait()
845 assert peer.failure_msg() == ''
846 assert out.statuses['/pp'] or { 0 } != 200, 'a PUSH_PROMISE must fail the connection, not be ignored'
847 assert out.errs['/pp'] or { '' } != ''
848 cend.close_both()
849}
850
851// An invalid SETTINGS_ENABLE_PUSH value (not 0/1) must fail the connection
852// (RFC 7540 6.5.2). The peer sends ENABLE_PUSH=2 then a valid response; the
853// request must fail rather than succeed.
854fn test_mux_invalid_enable_push_fails_connection() {
855 mut cend, mut pend := new_mux_pipe()
856 mut conn := new_test_mux_conn(mut cend)
857 mut peer := &MuxTestPeer{
858 end: pend
859 }
860 mut out := &MuxResults{}
861 mut workers := []thread{}
862 workers << spawn mux_worker(mut conn, H2ClientRequest{ authority: 't', path: '/ep' }, mut out)
863 peer_thread := spawn fn (mut peer MuxTestPeer) {
864 peer.read_preface() or {
865 peer.fail('preface: ${err.msg()}')
866 return
867 }
868 peer.write_frame(H2SettingsFrame{
869 settings: [
870 H2Setting{
871 id: h2_settings_enable_push
872 value: 2
873 },
874 ]
875 }) or {
876 peer.fail('settings: ${err.msg()}')
877 return
878 }
879 ids := peer.wait_for_headers(1) or {
880 peer.fail('headers: ${err.msg()}')
881 return
882 }
883 peer.respond_headers(ids[0], true) or {}
884 }(mut peer)
885 workers.wait()
886 peer_thread.wait()
887 assert peer.failure_msg() == ''
888 assert out.statuses['/ep'] or { 0 } != 200, 'an invalid ENABLE_PUSH must fail the connection'
889 assert out.errs['/ep'] or { '' } != ''
890 cend.close_both()
891}
892
893// A stream-level WINDOW_UPDATE with a zero increment is a stream error (RFC
894// 7540 6.9): that one stream must be RST and failed, but the connection and
895// other concurrent streams must survive.
896fn test_mux_zero_window_update_resets_only_that_stream() {
897 mut cend, mut pend := new_mux_pipe()
898 mut conn := new_test_mux_conn(mut cend)
899 mut peer := &MuxTestPeer{
900 end: pend
901 }
902 mut out := &MuxResults{}
903 mut wa := []thread{}
904 wa << spawn mux_worker(mut conn, H2ClientRequest{ authority: 't', path: '/a' }, mut out)
905 peer_thread := spawn fn (mut peer MuxTestPeer) {
906 peer.read_preface() or {
907 peer.fail('preface: ${err.msg()}')
908 return
909 }
910 ids := peer.wait_for_headers(2) or {
911 peer.fail('headers: ${err.msg()}')
912 return
913 }
914 mut a := u32(0)
915 mut b := u32(0)
916 peer.mu.lock()
917 for id in ids {
918 if peer.paths[id] or { '' } == '/a' {
919 a = id
920 } else {
921 b = id
922 }
923 }
924 peer.mu.unlock()
925 // Illegal zero-increment WINDOW_UPDATE on stream A: A must be reset.
926 peer.write_frame(H2WindowUpdateFrame{
927 stream_id: a
928 window_size_increment: 0
929 }) or {
930 peer.fail('wu: ${err.msg()}')
931 return
932 }
933 // B is served to completion; the connection must remain usable.
934 peer.respond_headers(b, false) or {
935 peer.fail('respond b: ${err.msg()}')
936 return
937 }
938 peer.write_frame(H2DataFrame{
939 stream_id: b
940 data: 'b-ok'.bytes()
941 end_stream: true
942 }) or {
943 peer.fail('data b: ${err.msg()}')
944 return
945 }
946 }(mut peer)
947 time.sleep(50 * time.millisecond)
948 mut wb := []thread{}
949 wb << spawn mux_worker(mut conn, H2ClientRequest{ authority: 't', path: '/b' }, mut out)
950 wa.wait()
951 wb.wait()
952 peer_thread.wait()
953 assert peer.failure_msg() == ''
954 assert out.errs['/a'] or { '' } != '', 'the zero-increment stream must fail'
955 assert out.statuses['/b'] or { 0 } == 200, 'the other stream and the connection must survive'
956 assert out.bodies['/b'] == 'b-ok'
957 cend.close_both()
958}
959
960// GOAWAY with last_stream_id between two active streams: the lower id
961// completes, the higher fails with a retryable error, and new requests are
962// refused (also retryable).
963fn test_mux_goaway_mid_flight() {
964 mut cend, mut pend := new_mux_pipe()
965 mut conn := new_test_mux_conn(mut cend)
966 mut peer := &MuxTestPeer{
967 end: pend
968 }
969 mut out := &MuxResults{}
970 mut w1 := []thread{}
971 w1 << spawn mux_worker(mut conn, H2ClientRequest{ authority: 't', path: '/g0' }, mut out)
972 peer_thread := spawn fn (mut peer MuxTestPeer) {
973 peer.read_preface() or {
974 peer.fail('preface: ${err.msg()}')
975 return
976 }
977 // First stream arrives, then wait for the second before answering, so
978 // both are unambiguously in flight when GOAWAY is sent.
979 ids := peer.wait_for_headers(2) or {
980 peer.fail('headers: ${err.msg()}')
981 return
982 }
983 mut sorted := ids.clone()
984 sorted.sort()
985 low := sorted[0]
986 high := sorted[1]
987 peer.write_frame(H2GoawayFrame{
988 last_stream_id: low
989 error_code: u32(H2ErrorCode.no_error)
990 }) or {
991 peer.fail('goaway: ${err.msg()}')
992 return
993 }
994 // The processed stream still completes.
995 peer.respond_headers(low, false) or {
996 peer.fail('respond: ${err.msg()}')
997 return
998 }
999 peer.write_frame(H2DataFrame{
1000 stream_id: low
1001 data: 'survivor'.bytes()
1002 end_stream: true
1003 }) or {
1004 peer.fail('data: ${err.msg()}')
1005 return
1006 }
1007 _ = high
1008 }(mut peer)
1009 // Make sure the first stream is registered before starting the second, so
1010 // the id order is deterministic.
1011 time.sleep(50 * time.millisecond)
1012 mut w2 := []thread{}
1013 w2 << spawn mux_worker(mut conn, H2ClientRequest{ authority: 't', path: '/g1' }, mut out)
1014 w1.wait()
1015 w2.wait()
1016 peer_thread.wait()
1017 assert peer.failure_msg() == ''
1018 assert out.statuses['/g0'] == 200
1019 assert out.bodies['/g0'] == 'survivor'
1020 assert out.errs['/g1'] or { '' } != ''
1021 assert out.codes['/g1'] or { 0 } == h2_err_retryable_code
1022 // New requests on a GOAWAYed connection are refused as retryable.
1023 conn.do(H2ClientRequest{ authority: 't', path: '/late' }) or {
1024 assert err.code() == h2_err_retryable_code
1025 cend.close_both()
1026 return
1027 }
1028 assert false, 'a request on a GOAWAYed connection must fail'
1029}
1030
1031// Cancelling one stream (stop_receiving_limit) must not poison the
1032// connection: the peer's late DATA for the cancelled stream is absorbed, and
1033// a second stream completes afterwards.
1034fn test_mux_cancel_one_stream_other_lives() {
1035 mut cend, mut pend := new_mux_pipe()
1036 mut conn := new_test_mux_conn(mut cend)
1037 mut peer := &MuxTestPeer{
1038 end: pend
1039 }
1040 mut out := &MuxResults{}
1041 mut wa := []thread{}
1042 wa << spawn mux_worker(mut conn, H2ClientRequest{
1043 authority: 't'
1044 path: '/cancelme'
1045 stop_receiving_limit: 5
1046 }, mut out)
1047 peer_thread := spawn fn (mut peer MuxTestPeer) {
1048 peer.read_preface() or {
1049 peer.fail('preface: ${err.msg()}')
1050 return
1051 }
1052 ids := peer.wait_for_headers(2) or {
1053 peer.fail('headers: ${err.msg()}')
1054 return
1055 }
1056 mut a := u32(0)
1057 mut b := u32(0)
1058 peer.mu.lock()
1059 for id in ids {
1060 if peer.paths[id] or { '' } == '/cancelme' {
1061 a = id
1062 } else {
1063 b = id
1064 }
1065 }
1066 peer.mu.unlock()
1067 // Over-deliver on stream A so the client cancels it.
1068 peer.respond_headers(a, false) or {
1069 peer.fail('respond a: ${err.msg()}')
1070 return
1071 }
1072 peer.write_frame(H2DataFrame{
1073 stream_id: a
1074 data: 'aaaaaaaaaa'.bytes()
1075 }) or {
1076 peer.fail('data a: ${err.msg()}')
1077 return
1078 }
1079 // Wait for the client's RST_STREAM(A).
1080 for {
1081 peer.mu.lock()
1082 rst := a in peer.rst_streams
1083 peer.mu.unlock()
1084 if rst {
1085 break
1086 }
1087 peer.pump() or {
1088 peer.fail('pump: ${err.msg()}')
1089 return
1090 }
1091 }
1092 // Late DATA for the cancelled stream: the client must absorb it and
1093 // keep the connection healthy.
1094 peer.write_frame(H2DataFrame{
1095 stream_id: a
1096 data: 'late-data!'.bytes()
1097 }) or {
1098 peer.fail('late data: ${err.msg()}')
1099 return
1100 }
1101 // Now serve stream B to completion.
1102 peer.respond_headers(b, false) or {
1103 peer.fail('respond b: ${err.msg()}')
1104 return
1105 }
1106 peer.write_frame(H2DataFrame{
1107 stream_id: b
1108 data: 'b-survives'.bytes()
1109 end_stream: true
1110 }) or {
1111 peer.fail('data b: ${err.msg()}')
1112 return
1113 }
1114 }(mut peer)
1115 time.sleep(50 * time.millisecond)
1116 mut wb := []thread{}
1117 wb << spawn mux_worker(mut conn, H2ClientRequest{ authority: 't', path: '/lives' }, mut out)
1118 wa.wait()
1119 wb.wait()
1120 peer_thread.wait()
1121 assert peer.failure_msg() == ''
1122 // The cancelled stream returns its truncated body without an error.
1123 assert out.errs['/cancelme'] or { '' } == ''
1124 assert out.bodies['/cancelme'] == 'aaaaaaaaaa'
1125 // The other stream is unaffected — the connection was not poisoned. (This
1126 // exercises the deregister-before-RST ordering in cancel_stream: the late
1127 // DATA for the cancelled stream is absorbed by the unknown-stream backstop,
1128 // which credits the connection window so other streams keep flowing.)
1129 assert out.errs['/lives'] or { '' } == ''
1130 assert out.bodies['/lives'] == 'b-survives'
1131 cend.close_both()
1132}
1133
1134// Cancelling a stream (stop_receiving_limit) must credit the connection window
1135// for ALL DATA received on it — including chunks queued before the cancel that
1136// are discarded undrained — or the connection window leaks on every early
1137// cancellation. The peer sends three 10-byte chunks; whatever the client does
1138// not deliver it must still credit back, so the connection WINDOW_UPDATEs sum
1139// to the full 30 bytes sent.
1140fn test_mux_cancel_credits_all_received_data() {
1141 mut cend, mut pend := new_mux_pipe()
1142 mut conn := new_test_mux_conn(mut cend)
1143 mut peer := &MuxTestPeer{
1144 end: pend
1145 }
1146 mut out := &MuxResults{}
1147 total := 30
1148 mut workers := []thread{}
1149 workers << spawn mux_worker(mut conn, H2ClientRequest{
1150 authority: 't'
1151 path: '/c'
1152 stop_receiving_limit: 5
1153 }, mut out)
1154 peer_thread := spawn fn [total] (mut peer MuxTestPeer) {
1155 peer.read_preface() or {
1156 peer.fail('preface: ${err.msg()}')
1157 return
1158 }
1159 ids := peer.wait_for_headers(1) or {
1160 peer.fail('headers: ${err.msg()}')
1161 return
1162 }
1163 peer.respond_headers(ids[0], false) or {
1164 peer.fail('respond: ${err.msg()}')
1165 return
1166 }
1167 for _ in 0 .. 3 {
1168 peer.write_frame(H2DataFrame{
1169 stream_id: ids[0]
1170 data: 'xxxxxxxxxx'.bytes()
1171 }) or {
1172 peer.fail('data: ${err.msg()}')
1173 return
1174 }
1175 }
1176 // Pump until every sent DATA byte has been credited back on the
1177 // connection window; with the fix this terminates regardless of how the
1178 // chunks raced against the cancel (drained, queued-then-discarded, or
1179 // late via the backstop).
1180 for {
1181 peer.mu.lock()
1182 got := peer.conn_window_updates
1183 peer.mu.unlock()
1184 if got >= u64(total) {
1185 break
1186 }
1187 peer.pump() or {
1188 peer.fail('pump: ${err.msg()}')
1189 return
1190 }
1191 }
1192 }(mut peer)
1193 workers.wait()
1194 peer_thread.wait()
1195 assert peer.failure_msg() == ''
1196 assert peer.conn_window_updates == u64(total), 'cancel must credit every received DATA byte'
1197 cend.close_both()
1198}
1199
1200// A peer SETTINGS_HEADER_TABLE_SIZE only bounds the dynamic table our encoder
1201// may use for request headers; it must not shrink the table we advertised for
1202// decoding the server's responses. Applying it to the response decoder would
1203// break valid dynamic references in later responses, so the decoder limit must
1204// stay at its advertised default.
1205fn test_mux_peer_header_table_size_keeps_decoder_limit() {
1206 mut cend, mut pend := new_mux_pipe()
1207 mut conn := new_test_mux_conn(mut cend)
1208 mut peer := &MuxTestPeer{
1209 end: pend
1210 }
1211 mut out := &MuxResults{}
1212 mut workers := []thread{}
1213 workers << spawn mux_worker(mut conn, H2ClientRequest{ authority: 't', path: '/h' }, mut out)
1214 peer_thread := spawn fn (mut peer MuxTestPeer) {
1215 peer.read_preface() or {
1216 peer.fail('preface: ${err.msg()}')
1217 return
1218 }
1219 // The peer limits the table it uses to DECODE our request headers to 0.
1220 peer.write_frame(H2SettingsFrame{
1221 settings: [
1222 H2Setting{
1223 id: h2_settings_header_table_size
1224 value: 0
1225 },
1226 ]
1227 }) or {
1228 peer.fail('settings: ${err.msg()}')
1229 return
1230 }
1231 ids := peer.wait_for_headers(1) or {
1232 peer.fail('headers: ${err.msg()}')
1233 return
1234 }
1235 id := ids[0]
1236 peer.respond_headers(id, false) or {
1237 peer.fail('respond: ${err.msg()}')
1238 return
1239 }
1240 peer.write_frame(H2DataFrame{
1241 stream_id: id
1242 data: 'ok'.bytes()
1243 end_stream: true
1244 }) or {
1245 peer.fail('data: ${err.msg()}')
1246 return
1247 }
1248 }(mut peer)
1249 workers.wait()
1250 peer_thread.wait()
1251 assert peer.failure_msg() == ''
1252 assert out.statuses['/h'] == 200
1253 // The response round-trip guarantees the reader processed the peer SETTINGS
1254 // first; our response decoder must still be at the advertised default.
1255 assert conn.decoder.max_dynamic_size == h2_hpack_default_table_size
1256 cend.close_both()
1257}
1258
1259// A POST whose upload is flow-control blocked when GOAWAY repudiates its stream
1260// (id above last_stream_id) must surface a retryable error, not a plain one, so
1261// the pool can replay a request the server never processed.
1262fn test_mux_goaway_preserves_retryable_for_blocked_upload() {
1263 mut cend, mut pend := new_mux_pipe()
1264 mut conn := new_test_mux_conn(mut cend)
1265 mut peer := &MuxTestPeer{
1266 end: pend
1267 }
1268 mut out := &MuxResults{}
1269 body_len := 100000
1270 mut workers := []thread{}
1271 workers << spawn mux_worker(mut conn, H2ClientRequest{
1272 method: 'POST'
1273 authority: 't'
1274 path: '/up'
1275 body: []u8{len: body_len, init: `B`}
1276 }, mut out)
1277 peer_thread := spawn fn (mut peer MuxTestPeer) {
1278 peer.read_preface() or {
1279 peer.fail('preface: ${err.msg()}')
1280 return
1281 }
1282 ids := peer.wait_for_headers(1) or {
1283 peer.fail('headers: ${err.msg()}')
1284 return
1285 }
1286 id := ids[0]
1287 // Let the upload exhaust both windows so the sender blocks in
1288 // send_body_on_stream waiting for a WINDOW_UPDATE.
1289 for {
1290 peer.mu.lock()
1291 got := peer.data_total[id] or { u64(0) }
1292 peer.mu.unlock()
1293 if got >= u64(h2_default_initial_window) {
1294 break
1295 }
1296 peer.pump() or {
1297 peer.fail('pump: ${err.msg()}')
1298 return
1299 }
1300 }
1301 // GOAWAY repudiating the in-flight stream (last_stream_id 0 < id), with
1302 // no connection error: the stream is retryable and the blocked sender
1303 // must wake with that classification preserved.
1304 peer.write_frame(H2GoawayFrame{
1305 last_stream_id: 0
1306 error_code: u32(H2ErrorCode.no_error)
1307 }) or {
1308 peer.fail('goaway: ${err.msg()}')
1309 return
1310 }
1311 }(mut peer)
1312 workers.wait()
1313 peer_thread.wait()
1314 assert peer.failure_msg() == ''
1315 assert out.errs['/up'] or { '' } != '', 'expected the repudiated upload to fail, not hang'
1316 assert out.codes['/up'] or { 0 } == h2_err_retryable_code, 'upload error was not retryable: ${out.errs['/up']}'
1317 cend.close_both()
1318}
1319
1320// An out-of-range peer SETTINGS_MAX_FRAME_SIZE (here 0) must fail the connection
1321// (RFC 7540 6.5.2), not be accepted — a zero frame size would make the send
1322// path's chunk step 0 and hang it in a zero-length-frame loop. The peer also
1323// sends a valid response afterwards: with the guard the request fails on the
1324// bad SETTINGS (processed first); without it the request would wrongly succeed.
1325fn test_mux_invalid_max_frame_size_fails_connection() {
1326 mut cend, mut pend := new_mux_pipe()
1327 mut conn := new_test_mux_conn(mut cend)
1328 mut peer := &MuxTestPeer{
1329 end: pend
1330 }
1331 mut out := &MuxResults{}
1332 mut workers := []thread{}
1333 workers << spawn mux_worker(mut conn, H2ClientRequest{ authority: 't', path: '/x' }, mut out)
1334 peer_thread := spawn fn (mut peer MuxTestPeer) {
1335 peer.read_preface() or {
1336 peer.fail('preface: ${err.msg()}')
1337 return
1338 }
1339 peer.write_frame(H2SettingsFrame{
1340 settings: [
1341 H2Setting{
1342 id: h2_settings_max_frame_size
1343 value: 0
1344 },
1345 ]
1346 }) or {
1347 peer.fail('settings: ${err.msg()}')
1348 return
1349 }
1350 ids := peer.wait_for_headers(1) or {
1351 peer.fail('headers: ${err.msg()}')
1352 return
1353 }
1354 // A perfectly valid response — the client must still have failed the
1355 // connection on the illegal SETTINGS processed before it.
1356 peer.respond_headers(ids[0], false) or {}
1357 peer.write_frame(H2DataFrame{
1358 stream_id: ids[0]
1359 data: 'nope'.bytes()
1360 end_stream: true
1361 }) or {}
1362 }(mut peer)
1363 workers.wait()
1364 peer_thread.wait()
1365 assert peer.failure_msg() == ''
1366 assert out.statuses['/x'] or { 0 } != 200, 'request must not succeed on an illegal SETTINGS_MAX_FRAME_SIZE'
1367 err_msg := out.errs['/x'] or { '' }
1368 assert err_msg != '', 'expected the connection to fail, not hang or succeed'
1369 assert err_msg.contains('MAX_FRAME_SIZE'), 'unexpected error: ${err_msg}'
1370 cend.close_both()
1371}
1372
1373// A response header block split across HEADERS + CONTINUATION is reassembled.
1374fn test_mux_continuation_assembly() {
1375 mut cend, mut pend := new_mux_pipe()
1376 mut conn := new_test_mux_conn(mut cend)
1377 mut peer := &MuxTestPeer{
1378 end: pend
1379 }
1380 mut out := &MuxResults{}
1381 mut workers := []thread{}
1382 workers << spawn mux_worker(mut conn, H2ClientRequest{ authority: 't', path: '/cont' }, mut out)
1383 peer_thread := spawn fn (mut peer MuxTestPeer) {
1384 peer.read_preface() or {
1385 peer.fail('preface: ${err.msg()}')
1386 return
1387 }
1388 ids := peer.wait_for_headers(1) or {
1389 peer.fail('headers: ${err.msg()}')
1390 return
1391 }
1392 id := ids[0]
1393 block := peer.encoder.encode([H2HeaderField{':status', '200'},
1394 H2HeaderField{'x-long-header', 'v'.repeat(64)}])
1395 half := block.len / 2
1396 peer.write_frame(H2HeadersFrame{
1397 stream_id: id
1398 fragment: block[..half]
1399 end_headers: false
1400 }) or {
1401 peer.fail('headers: ${err.msg()}')
1402 return
1403 }
1404 peer.write_frame(H2ContinuationFrame{
1405 stream_id: id
1406 fragment: block[half..]
1407 end_headers: true
1408 }) or {
1409 peer.fail('cont: ${err.msg()}')
1410 return
1411 }
1412 peer.write_frame(H2DataFrame{
1413 stream_id: id
1414 data: 'ok'.bytes()
1415 end_stream: true
1416 }) or {
1417 peer.fail('data: ${err.msg()}')
1418 return
1419 }
1420 }(mut peer)
1421 workers.wait()
1422 peer_thread.wait()
1423 assert peer.failure_msg() == ''
1424 assert out.errs.len == 0, 'worker errors: ${out.errs}'
1425 assert out.statuses['/cont'] == 200
1426 assert out.bodies['/cont'] == 'ok'
1427 cend.close_both()
1428}
1429
1430// When the connection dies with several streams in flight, every waiter must
1431// wake with an error (no hangs).
1432fn test_mux_conn_death_wakes_all_waiters() {
1433 mut cend, mut pend := new_mux_pipe()
1434 mut conn := new_test_mux_conn(mut cend)
1435 mut peer := &MuxTestPeer{
1436 end: pend
1437 }
1438 mut out := &MuxResults{}
1439 mut workers := []thread{}
1440 for i in 0 .. 4 {
1441 workers << spawn mux_worker(mut conn, H2ClientRequest{ authority: 't', path: '/d${i}' }, mut out)
1442 }
1443 peer_thread := spawn fn (mut peer MuxTestPeer) {
1444 peer.read_preface() or {
1445 peer.fail('preface: ${err.msg()}')
1446 return
1447 }
1448 peer.wait_for_headers(4) or {
1449 peer.fail('headers: ${err.msg()}')
1450 return
1451 }
1452 // Kill the connection with all four streams mid-flight.
1453 peer.end.close_both()
1454 }(mut peer)
1455 workers.wait()
1456 peer_thread.wait()
1457 assert peer.failure_msg() == ''
1458 assert out.errs.len == 4, 'all four waiters must fail, got: ${out.errs}'
1459 // A subsequent request on the dead connection fails fast and retryably.
1460 conn.do(H2ClientRequest{ authority: 't', path: '/postmortem' }) or {
1461 assert err.code() == h2_err_retryable_code
1462 return
1463 }
1464 assert false, 'a request on a dead connection must fail'
1465}
1466
1467// When the connection preface write fails, note_write_failure must be called so
1468// the connection is torn down and the pool stops admitting new work. Both the
1469// failing request and any immediately subsequent one must return retryable errors.
1470fn test_mux_preface_write_failure_tears_down() {
1471 mut cend, mut pend := new_mux_pipe()
1472 // Close the write side so the very first transport write (the preface) fails.
1473 cend.outgoing.close()
1474 mut conn := new_test_mux_conn(mut cend)
1475 mut out := &MuxResults{}
1476 mut w1 := []thread{}
1477 w1 << spawn mux_worker(mut conn, H2ClientRequest{ authority: 't', path: '/x' }, mut out)
1478 w1.wait()
1479 assert out.errs['/x'] or { '' } != '', 'request on broken transport must fail'
1480 assert out.codes['/x'] or { 0 } == h2_err_retryable_code, 'preface write failure must be retryable'
1481 // note_write_failure must have set shutting_down; the second request must
1482 // fail retryably rather than be admitted to the dead connection.
1483 mut out2 := &MuxResults{}
1484 mut w2 := []thread{}
1485 w2 << spawn mux_worker(mut conn, H2ClientRequest{ authority: 't', path: '/y' }, mut out2)
1486 w2.wait()
1487 assert out2.errs['/y'] or { '' } != '', 'second request on torn-down connection must fail'
1488 assert out2.codes['/y'] or { 0 } == h2_err_retryable_code, 'second request must also be retryable'
1489 cend.incoming.close() // unblock the reader thread so it exits cleanly
1490 pend.close_both()
1491}
1492
1493// Applying a positive SETTINGS_INITIAL_WINDOW_SIZE delta to a stream that
1494// already has extra credit from a WINDOW_UPDATE can overflow the stream's send
1495// window above 2^31-1, which is a connection FLOW_CONTROL_ERROR (RFC 7540
1496// §6.9.2). The connection must be failed, not silently keep the invalid window.
1497fn test_mux_settings_initial_window_delta_overflow_fails_connection() {
1498 mut cend, mut pend := new_mux_pipe()
1499 mut conn := new_test_mux_conn(mut cend)
1500 mut peer := &MuxTestPeer{
1501 end: pend
1502 }
1503 mut out := &MuxResults{}
1504 mut workers := []thread{}
1505 // A simple GET keeps the stream registered in c.streams (waiting for a
1506 // response that never arrives), so the SETTINGS delta has a stream to hit.
1507 workers << spawn mux_worker(mut conn, H2ClientRequest{ authority: 't', path: '/ov' }, mut out)
1508 peer_thread := spawn fn (mut peer MuxTestPeer) {
1509 peer.read_preface() or {
1510 peer.fail('preface: ${err.msg()}')
1511 return
1512 }
1513 peer.wait_for_headers(1) or {
1514 peer.fail('headers: ${err.msg()}')
1515 return
1516 }
1517 // Raise the stream's send window close to 2^31-1 via WINDOW_UPDATE,
1518 // then raise SETTINGS_INITIAL_WINDOW_SIZE so the delta pushes the window
1519 // over the limit. Stream send_window ≈ 65535 + 0x7fff_0000 = 0x7fff_ffff;
1520 // delta = 0x7fff_ffff - 65535 = 0x7fff_0000 → new window ≈ 0xfffe_0000
1521 // which exceeds 0x7fff_ffff → FLOW_CONTROL_ERROR.
1522 peer.write_frame(H2WindowUpdateFrame{
1523 stream_id: 1
1524 window_size_increment: u32(0x7fff_0000)
1525 }) or {
1526 peer.fail('wu: ${err.msg()}')
1527 return
1528 }
1529 peer.write_frame(H2SettingsFrame{
1530 settings: [H2Setting{h2_settings_initial_window_size, u32(0x7fff_ffff)}]
1531 }) or {
1532 peer.fail('settings: ${err.msg()}')
1533 return
1534 }
1535 }(mut peer)
1536 workers.wait()
1537 peer_thread.wait()
1538 assert peer.failure_msg() == ''
1539 err_msg := out.errs['/ov'] or { '' }
1540 assert err_msg != '', 'expected a connection FLOW_CONTROL_ERROR, got success or hang'
1541 assert err_msg.contains('FLOW_CONTROL_ERROR'), 'unexpected error: ${err_msg}'
1542 cend.close_both()
1543}
1544
1545// A server that ignores our advertised receive window and sends more DATA than
1546// it allows must trigger a FLOW_CONTROL_ERROR: the connection must be failed,
1547// not silently buffer unbounded data. on_data blocks the requester from draining
1548// (and sending WINDOW_UPDATE) while the peer floods the connection window.
1549fn test_mux_peer_exceeding_recv_window_fails_connection() {
1550 mut cend, mut pend := new_mux_pipe()
1551 mut conn := new_test_mux_conn(mut cend)
1552 mut peer := &MuxTestPeer{
1553 end: pend
1554 }
1555 mut out := &MuxResults{}
1556 // Deplete the tracked connection-level receive window to 1 byte so the
1557 // very first DATA frame (2 bytes) immediately overflows it and triggers
1558 // FLOW_CONTROL_ERROR. Doing this before any goroutine sends frames means
1559 // no WINDOW_UPDATE is ever sent that could re-fill the window first, so
1560 // the result is fully deterministic with no concurrent timing race.
1561 conn.recv_wmu.lock()
1562 conn.conn_recv_window = 1
1563 conn.recv_wmu.unlock()
1564 mut workers := []thread{}
1565 workers << spawn mux_worker(mut conn, H2ClientRequest{
1566 authority: 't'
1567 path: '/flood'
1568 }, mut out)
1569 peer_thread := spawn fn (mut peer MuxTestPeer) {
1570 peer.read_preface() or {
1571 peer.fail('preface: ${err.msg()}')
1572 return
1573 }
1574 ids := peer.wait_for_headers(1) or {
1575 peer.fail('headers: ${err.msg()}')
1576 return
1577 }
1578 peer.respond_headers(ids[0], false) or {
1579 peer.fail('respond: ${err.msg()}')
1580 return
1581 }
1582 // 2-byte DATA frame: connection window is 1 byte, so this exceeds it
1583 // and must trigger FLOW_CONTROL_ERROR on the client.
1584 peer.write_frame(H2DataFrame{
1585 stream_id: ids[0]
1586 data: [u8(0), 0]
1587 }) or { peer.fail('data: ${err.msg()}') }
1588 }(mut peer)
1589 workers.wait()
1590 peer_thread.wait()
1591 assert peer.failure_msg() == ''
1592 err_msg := out.errs['/flood'] or { '' }
1593 assert err_msg != '', 'expected FLOW_CONTROL_ERROR, got success or hang'
1594 assert err_msg.contains('FLOW_CONTROL_ERROR'), 'unexpected error: ${err_msg}'
1595 cend.close_both()
1596}
1597
1598// A STREAM-level receive-window violation (RFC 7540 §6.9.1) must reset only that
1599// stream, not tear down the whole connection: the offending request fails, but
1600// the connection stays alive (closed stays false) so other streams are unharmed.
1601fn test_mux_stream_window_violation_resets_only_that_stream() {
1602 mut cend, mut pend := new_mux_pipe()
1603 mut conn := new_test_mux_conn(mut cend)
1604 mut peer := &MuxTestPeer{
1605 end: pend
1606 }
1607 mut out := &MuxResults{}
1608 // Inflate the connection window so the oversized DATA trips ONLY the
1609 // stream-level window check, not the connection-level one.
1610 conn.recv_wmu.lock()
1611 conn.conn_recv_window = 10_000_000
1612 conn.recv_wmu.unlock()
1613 mut workers := []thread{}
1614 workers << spawn mux_worker(mut conn, H2ClientRequest{
1615 authority: 't'
1616 path: '/flood'
1617 }, mut out)
1618 peer_thread := spawn fn (mut peer MuxTestPeer, mut conn H2MuxConn) {
1619 peer.read_preface() or {
1620 peer.fail('preface: ${err.msg()}')
1621 return
1622 }
1623 ids := peer.wait_for_headers(1) or {
1624 peer.fail('headers: ${err.msg()}')
1625 return
1626 }
1627 id := ids[0]
1628 peer.respond_headers(id, false) or {
1629 peer.fail('respond: ${err.msg()}')
1630 return
1631 }
1632 // White-box: shrink just this stream's receive window so the next DATA
1633 // frame exceeds it while the connection window stays valid. No DATA has
1634 // arrived yet, so the requester has not credited the window back.
1635 conn.smu.lock()
1636 if mut s := conn.streams[id] {
1637 s.mu.lock()
1638 s.recv_window = 1
1639 s.mu.unlock()
1640 }
1641 conn.smu.unlock()
1642 peer.write_frame(H2DataFrame{
1643 stream_id: id
1644 data: [u8(0), 0]
1645 }) or {
1646 peer.fail('data: ${err.msg()}')
1647 return
1648 }
1649 // The client must RST only this stream; pump until we see it.
1650 for {
1651 f := peer.pump() or { return }
1652 if f is H2RstStreamFrame {
1653 return
1654 }
1655 }
1656 }(mut peer, mut conn)
1657 workers.wait()
1658 peer_thread.wait()
1659 assert peer.failure_msg() == ''
1660 err_msg := out.errs['/flood'] or { '' }
1661 assert err_msg != '', 'stream-window violation must error the request'
1662 assert err_msg.contains('FLOW_CONTROL_ERROR') || err_msg.contains('receive window'), 'unexpected error: ${err_msg}'
1663 // The connection must survive — closed is set only when the reader exits on a
1664 // connection-level failure, which a stream reset must not cause.
1665 conn.smu.lock()
1666 closed := conn.closed
1667 conn.smu.unlock()
1668 assert !closed, 'a stream-level flow-control violation must NOT close the connection'
1669 cend.close_both()
1670}
1671
1672// DATA after END_STREAM on a fully "closed" stream (we also sent our END_STREAM)
1673// is a connection error (RFC 7540 §5.1 MUST).
1674fn test_mux_data_after_end_stream_on_closed_stream_fails_connection() {
1675 mut cend, mut pend := new_mux_pipe()
1676 mut conn := new_test_mux_conn(mut cend)
1677 mut peer := &MuxTestPeer{
1678 end: pend
1679 }
1680 // White-box: register a stream in the "closed" state — both the peer (ended)
1681 // and we (send_closed) have finished sending.
1682 mut s := new_h2_mux_stream()
1683 s.id = 1
1684 s.headers_done = true
1685 s.ended = true
1686 s.send_closed = true
1687 conn.smu.lock()
1688 conn.streams[1] = s
1689 conn.smu.unlock()
1690 // A DATA frame on the closed stream must fail the whole connection.
1691 peer.write_frame(H2DataFrame{ stream_id: 1, data: [u8(0)] }) or {
1692 assert false, 'write: ${err.msg()}'
1693 }
1694 mut closed := false
1695 for _ in 0 .. 2000 {
1696 conn.smu.lock()
1697 closed = conn.closed
1698 conn.smu.unlock()
1699 if closed {
1700 break
1701 }
1702 time.sleep(time.millisecond)
1703 }
1704 assert closed, 'DATA after END_STREAM on a closed stream must fail the connection'
1705 cend.close_both()
1706}
1707
1708// DATA after END_STREAM on a "half-closed (remote)" stream (the peer ended but
1709// we have NOT closed our send side) is a STREAM error: RST that stream, keep the
1710// connection alive (RFC 7540 §5.1).
1711fn test_mux_data_after_end_stream_half_closed_resets_only_stream() {
1712 mut cend, mut pend := new_mux_pipe()
1713 mut conn := new_test_mux_conn(mut cend)
1714 mut peer := &MuxTestPeer{
1715 end: pend
1716 }
1717 // White-box: register a stream where the peer has ended but our send side is
1718 // still open (send_closed = false) — "half-closed (remote)".
1719 mut s := new_h2_mux_stream()
1720 s.id = 1
1721 s.headers_done = true
1722 s.ended = true
1723 s.send_closed = false
1724 conn.smu.lock()
1725 conn.streams[1] = s
1726 conn.smu.unlock()
1727 peer.write_frame(H2DataFrame{ stream_id: 1, data: [u8(0)] }) or {
1728 assert false, 'write: ${err.msg()}'
1729 }
1730 // The client must RST just this stream; pump until we see it.
1731 mut saw_rst := false
1732 for _ in 0 .. 50 {
1733 f := peer.pump() or { break }
1734 if f is H2RstStreamFrame {
1735 saw_rst = true
1736 break
1737 }
1738 }
1739 assert saw_rst, 'half-closed-remote DATA-after-END_STREAM must RST the stream'
1740 conn.smu.lock()
1741 closed := conn.closed
1742 conn.smu.unlock()
1743 assert !closed, 'a stream-level reset must NOT close the connection'
1744 cend.close_both()
1745}
1746
1747// A peer that floods control frames before the client preface (the connection
1748// stays pre-handshake until the first request) must not grow the deferred-ACK
1749// buffers without bound — the connection is failed once the cap is exceeded.
1750fn test_mux_preface_control_flood_fails_connection() {
1751 mut cend, mut pend := new_mux_pipe()
1752 mut conn := new_test_mux_conn(mut cend)
1753 mut peer := &MuxTestPeer{
1754 end: pend
1755 }
1756 // No request is made, so the lazy client preface is never sent; every PING
1757 // is deferred. Send more than the cap allows.
1758 for _ in 0 .. (h2_max_pending_preface_acks + 5) {
1759 peer.write_frame(H2PingFrame{ data: [u8(1), 2, 3, 4, 5, 6, 7, 8] }) or {
1760 assert false, 'write: ${err.msg()}'
1761 }
1762 }
1763 mut closed := false
1764 for _ in 0 .. 2000 {
1765 conn.smu.lock()
1766 closed = conn.closed
1767 conn.smu.unlock()
1768 if closed {
1769 break
1770 }
1771 time.sleep(time.millisecond)
1772 }
1773 assert closed, 'a pre-preface control-frame flood must fail the connection'
1774 cend.close_both()
1775}
1776
1777// A malformed Content-Length (string.u64() would leniently parse '12junk' -> 12)
1778// makes the response malformed; it must be rejected, not accepted as a success.
1779fn test_mux_malformed_content_length_resets_stream() {
1780 mut cend, mut pend := new_mux_pipe()
1781 mut conn := new_test_mux_conn(mut cend)
1782 mut peer := &MuxTestPeer{
1783 end: pend
1784 }
1785 mut out := &MuxResults{}
1786 mut workers := []thread{}
1787 workers << spawn mux_worker(mut conn, H2ClientRequest{ authority: 't', path: '/x' }, mut out)
1788 peer_thread := spawn fn (mut peer MuxTestPeer) {
1789 peer.read_preface() or {
1790 peer.fail('preface: ${err.msg()}')
1791 return
1792 }
1793 ids := peer.wait_for_headers(1) or {
1794 peer.fail('headers: ${err.msg()}')
1795 return
1796 }
1797 block := peer.encoder.encode([H2HeaderField{':status', '200'},
1798 H2HeaderField{'content-length', '12junk'}])
1799 peer.write_frame(H2HeadersFrame{
1800 stream_id: ids[0]
1801 fragment: block
1802 end_headers: true
1803 end_stream: false
1804 }) or {
1805 peer.fail('resp: ${err.msg()}')
1806 return
1807 }
1808 for {
1809 f := peer.pump() or { return }
1810 if f is H2RstStreamFrame {
1811 return
1812 }
1813 }
1814 }(mut peer)
1815 workers.wait()
1816 peer_thread.wait()
1817 assert peer.failure_msg() == ''
1818 err_msg := out.errs['/x'] or { '' }
1819 assert err_msg != '', 'malformed Content-Length must error the request, not succeed'
1820 assert err_msg.to_lower().contains('content-length'), 'unexpected error: ${err_msg}'
1821 cend.close_both()
1822}
1823
1824// RFC 7540 §8.1: a response must begin with HEADERS. A DATA frame before any
1825// response HEADERS is malformed — reset that stream, but keep the connection.
1826fn test_mux_data_before_headers_resets_stream() {
1827 mut cend, mut pend := new_mux_pipe()
1828 mut conn := new_test_mux_conn(mut cend)
1829 mut peer := &MuxTestPeer{
1830 end: pend
1831 }
1832 mut out := &MuxResults{}
1833 mut workers := []thread{}
1834 workers << spawn mux_worker(mut conn, H2ClientRequest{ authority: 't', path: '/x' }, mut out)
1835 peer_thread := spawn fn (mut peer MuxTestPeer) {
1836 peer.read_preface() or {
1837 peer.fail('preface: ${err.msg()}')
1838 return
1839 }
1840 ids := peer.wait_for_headers(1) or {
1841 peer.fail('headers: ${err.msg()}')
1842 return
1843 }
1844 // DATA with no preceding response HEADERS (malformed).
1845 peer.write_frame(H2DataFrame{
1846 stream_id: ids[0]
1847 data: 'oops'.bytes()
1848 }) or {
1849 peer.fail('data: ${err.msg()}')
1850 return
1851 }
1852 for {
1853 f := peer.pump() or { return }
1854 if f is H2RstStreamFrame {
1855 return
1856 }
1857 }
1858 }(mut peer)
1859 workers.wait()
1860 peer_thread.wait()
1861 assert peer.failure_msg() == ''
1862 err_msg := out.errs['/x'] or { '' }
1863 assert err_msg != '', 'DATA before HEADERS must error the request'
1864 conn.smu.lock()
1865 closed := conn.closed
1866 conn.smu.unlock()
1867 assert !closed, 'DATA before HEADERS is a stream error; the connection must survive'
1868 cend.close_both()
1869}
1870
1871// Releasing the last reference on an idle connection must wake the background
1872// reader through the required close_transport callback. With no requests and no
1873// inbound frames the reader is blocked in transport.read(); only closing the
1874// transport can unblock it (the H2Transport interface has no close()). This
1875// exercises the teardown -> closer -> reader-exit chain WITHOUT the test closing
1876// the pipe manually, guarding the Codex P2 fix (discussion_r3475761576): a nil
1877// closer is now rejected, so teardown can always close the transport. (Note: the
1878// nil-closer leak itself is reproduced statically by detect_mux_requires_closer.sh,
1879// since a nil closer now panics and cannot be exercised at runtime.)
1880fn test_mux_release_wakes_reader_via_closer() {
1881 mut cend, _ := new_mux_pipe()
1882 mut conn := new_test_mux_conn(mut cend)
1883 // No requests, no frames: the reader is blocked in transport.read().
1884 conn.shutdown_when_idle()
1885 conn.release() // last ref -> teardown_transport -> close_transport closes the pipe
1886 // The reader observes the closed transport and exits via fail_conn(closed=true).
1887 mut woke := false
1888 for _ in 0 .. 200 {
1889 conn.smu.lock()
1890 woke = conn.closed
1891 conn.smu.unlock()
1892 if woke {
1893 break
1894 }
1895 time.sleep(5 * time.millisecond)
1896 }
1897 assert woke, 'reader did not exit after release(): close_transport failed to wake it'
1898}
1899
1900// A trailing HEADERS block (trailers) must surface its non-pseudo fields in the
1901// response headers, matching the synchronous H2Conn.read_response. Regression for
1902// Codex P2 (discussion_r3477245531): the mux path HPACK-decoded the trailer block
1903// but dropped every field, only marking the stream ended — so callers lost
1904// grpc-status, digest, etc. even though the stream completed normally.
1905fn test_mux_response_trailers_preserved() {
1906 mut cend, mut pend := new_mux_pipe()
1907 mut conn := new_test_mux_conn(mut cend)
1908 mut peer := &MuxTestPeer{
1909 end: pend
1910 }
1911 peer_thread := spawn fn (mut peer MuxTestPeer) {
1912 peer.read_preface() or {
1913 peer.fail('preface: ${err.msg()}')
1914 return
1915 }
1916 ids := peer.wait_for_headers(1) or {
1917 peer.fail('headers: ${err.msg()}')
1918 return
1919 }
1920 id := ids[0]
1921 // Final response headers (no END_STREAM: a body and trailers follow).
1922 peer.respond_headers(id, false) or {
1923 peer.fail('respond: ${err.msg()}')
1924 return
1925 }
1926 peer.write_frame(H2DataFrame{
1927 stream_id: id
1928 data: 'hi'.bytes()
1929 end_stream: false
1930 }) or {
1931 peer.fail('data: ${err.msg()}')
1932 return
1933 }
1934 // A trailing HEADERS block carrying END_STREAM, with valid (non-pseudo)
1935 // trailer fields that must be preserved. (Pseudo-headers in trailers are
1936 // malformed per §8.1 and are covered by the rejection test below.)
1937 trailer_block := peer.encoder.encode([H2HeaderField{'grpc-status', '0'},
1938 H2HeaderField{'grpc-message', 'ok'}])
1939 peer.write_frame(H2HeadersFrame{
1940 stream_id: id
1941 fragment: trailer_block
1942 end_headers: true
1943 end_stream: true
1944 }) or {
1945 peer.fail('trailers: ${err.msg()}')
1946 return
1947 }
1948 }(mut peer)
1949 resp := conn.do(H2ClientRequest{ authority: 't', path: '/g' }) or {
1950 assert false, 'request failed: ${err}'
1951 return
1952 }
1953 peer_thread.wait()
1954 assert peer.failure_msg() == ''
1955 assert resp.status == 200
1956 assert resp.body.bytestr() == 'hi'
1957 mut grpc_status := ''
1958 mut grpc_message := ''
1959 mut pseudo_in_headers := false
1960 for h in resp.headers {
1961 match h.name {
1962 'grpc-status' { grpc_status = h.value }
1963 'grpc-message' { grpc_message = h.value }
1964 else {}
1965 }
1966
1967 if h.name.starts_with(':') {
1968 pseudo_in_headers = true
1969 }
1970 }
1971 assert grpc_status == '0', 'trailer grpc-status missing from response headers: ${resp.headers}'
1972 assert grpc_message == 'ok', 'trailer grpc-message missing from response headers: ${resp.headers}'
1973 assert !pseudo_in_headers, 'pseudo-header field leaked into trailers: ${resp.headers}'
1974 cend.close_both()
1975}
1976
1977// mux_response_rejected runs one request whose response carries `resp_fields` and
1978// asserts the request fails with an error containing `want` (the stream was reset
1979// as malformed), rather than delivering the response.
1980fn mux_response_rejected(resp_fields []H2HeaderField, want string) {
1981 mut cend, mut pend := new_mux_pipe()
1982 mut conn := new_test_mux_conn(mut cend)
1983 mut peer := &MuxTestPeer{
1984 end: pend
1985 }
1986 peer_thread := spawn fn (mut peer MuxTestPeer, resp_fields []H2HeaderField) {
1987 peer.read_preface() or {
1988 peer.fail('preface: ${err.msg()}')
1989 return
1990 }
1991 ids := peer.wait_for_headers(1) or {
1992 peer.fail('headers: ${err.msg()}')
1993 return
1994 }
1995 block := peer.encoder.encode(resp_fields)
1996 peer.write_frame(H2HeadersFrame{
1997 stream_id: ids[0]
1998 fragment: block
1999 end_headers: true
2000 end_stream: true
2001 }) or {
2002 peer.fail('resp: ${err.msg()}')
2003 return
2004 }
2005 // Drain the client's RST_STREAM until the pipe closes.
2006 for {
2007 peer.pump() or { return }
2008 }
2009 }(mut peer, resp_fields)
2010 mut got := ''
2011 if resp := conn.do(H2ClientRequest{ authority: 't', path: '/x' }) {
2012 got = '<<accepted: status=${resp.status} headers=${resp.headers}>>'
2013 } else {
2014 got = err.msg()
2015 }
2016 cend.close_both() // unblock the peer's pump loop so its thread exits
2017 peer_thread.wait()
2018 assert peer.failure_msg() == ''
2019 assert got.contains(want), 'expected "${want}" in the rejection, got: ${got}'
2020}
2021
2022// RFC 9113 §8.2: a response carrying a malformed field — a connection-specific
2023// header (transfer-encoding/connection/...) or an uppercase field name — is
2024// malformed and must be rejected, not delivered to the caller. Regression for
2025// the proactive RFC-conformance audit (gap G1): both client paths previously
2026// appended received fields with only :status / content-length validation.
2027fn test_mux_response_rejects_malformed_fields() {
2028 mux_response_rejected([H2HeaderField{':status', '200'},
2029 H2HeaderField{'transfer-encoding', 'chunked'}], 'transfer-encoding')
2030 mux_response_rejected([H2HeaderField{':status', '200'},
2031 H2HeaderField{'Content-Type', 'text/plain'}], 'uppercase')
2032 // An undefined response pseudo-header is also malformed (§8.3.1).
2033 mux_response_rejected([H2HeaderField{':status', '200'}, H2HeaderField{':custom', 'x'}],
2034 'pseudo-header')
2035}
2036
2037// RFC 9113 §8.1: a trailing HEADERS block carrying a pseudo-header (or any
2038// malformed field) is malformed and must reset the stream, not be dropped.
2039fn test_mux_trailers_reject_pseudo() {
2040 mut cend, mut pend := new_mux_pipe()
2041 mut conn := new_test_mux_conn(mut cend)
2042 mut peer := &MuxTestPeer{
2043 end: pend
2044 }
2045 peer_thread := spawn fn (mut peer MuxTestPeer) {
2046 peer.read_preface() or {
2047 peer.fail('preface: ${err.msg()}')
2048 return
2049 }
2050 ids := peer.wait_for_headers(1) or {
2051 peer.fail('headers: ${err.msg()}')
2052 return
2053 }
2054 id := ids[0]
2055 peer.respond_headers(id, false) or {
2056 peer.fail('respond: ${err.msg()}')
2057 return
2058 }
2059 // Trailers must not contain a pseudo-header.
2060 block := peer.encoder.encode([H2HeaderField{':status', '200'}])
2061 peer.write_frame(H2HeadersFrame{
2062 stream_id: id
2063 fragment: block
2064 end_headers: true
2065 end_stream: true
2066 }) or {
2067 peer.fail('trailers: ${err.msg()}')
2068 return
2069 }
2070 for {
2071 peer.pump() or { return }
2072 }
2073 }(mut peer)
2074 mut got := ''
2075 if resp := conn.do(H2ClientRequest{ authority: 't', path: '/g' }) {
2076 got = '<<accepted: ${resp.headers}>>'
2077 } else {
2078 got = err.msg()
2079 }
2080 cend.close_both()
2081 peer_thread.wait()
2082 assert peer.failure_msg() == ''
2083 assert got.contains('malformed trailers'), 'trailers pseudo-header not rejected: ${got}'
2084}
2085
2086// RFC 9113 §6.5.2: the client honors the peer's advisory
2087// SETTINGS_MAX_HEADER_LIST_SIZE and refuses an over-limit request locally rather
2088// than emitting it. Conformance gap G4. The peer sends no SETTINGS here, so the
2089// reader never writes peer_max_header_list_size — setting it directly before the
2090// request is race-free.
2091fn test_mux_request_respects_peer_max_header_list_size() {
2092 mut cend, mut pend := new_mux_pipe()
2093 mut conn := new_test_mux_conn(mut cend)
2094 conn.peer_max_header_list_size = 40 // tiny: even the pseudo-headers exceed it
2095 mut peer := &MuxTestPeer{
2096 end: pend
2097 }
2098 peer_thread := spawn fn (mut peer MuxTestPeer) {
2099 peer.read_preface() or {
2100 peer.fail('preface: ${err.msg()}')
2101 return
2102 }
2103 for {
2104 peer.pump() or { return }
2105 }
2106 }(mut peer)
2107 mut got := ''
2108 if _ := conn.do(H2ClientRequest{
2109 method: 'GET'
2110 scheme: 'https'
2111 authority: 'example.com'
2112 path: '/a-fairly-long-path-to-exceed-the-limit'
2113 })
2114 {
2115 got = '<<accepted>>'
2116 } else {
2117 got = err.msg()
2118 }
2119 cend.close_both()
2120 peer_thread.wait()
2121 assert peer.failure_msg() == ''
2122 assert got.contains('MAX_HEADER_LIST_SIZE'), 'over-limit request not rejected: ${got}'
2123}
2124