1+ // Flags: --experimental-quic --experimental-stream-iter --no-warnings
2+
3+ // Test: Quic maxstreamdata updates on http/3
4+ // Client sends a body that precisely fills the window size,
5+ // and verifies that it is data transfer is not stalled.
6+
7+ import { hasQuic , skip , mustCall } from '../common/index.mjs' ;
8+ import assert from 'node:assert' ;
9+ import { readFile } from 'node:fs/promises' ;
10+ import { setTimeout as sleep } from 'node:timers/promises' ;
11+
12+ if ( ! hasQuic ) {
13+ skip ( 'QUIC is not enabled' ) ;
14+ }
15+ const { listen, connect } = await import ( 'node:quic' ) ;
16+ const { createPrivateKey } = await import ( 'node:crypto' ) ;
17+ const { drainableProtocol } = await import ( 'stream/iter' ) ;
18+
19+ const keys = 'test/fixtures/keys' ;
20+ const key = createPrivateKey ( await readFile ( `${ keys } /agent1-key.pem` ) ) ;
21+ const cert = await readFile ( `${ keys } /agent1-cert.pem` ) ;
22+
23+ const WINDOW = 4096 ;
24+ // Fills the window exactly: HTTP/3 spends 11 of those bytes on framing (8 for
25+ // the HEADERS frame below, 3 for the DATA frame header). The send buffer then
26+ // empties at the same moment the window reaches zero, leaving nothing in
27+ // flight to ack. Any other size leaves bytes queued, and the ack for those
28+ // wakes the writer instead, hiding the bug.
29+ const BODY = WINDOW - 11 ;
30+
31+ let letServerRead ;
32+ const serverMayRead = new Promise ( ( resolve ) => { letServerRead = resolve ; } ) ;
33+
34+ const endpoint = await listen ( ( session ) => {
35+ session . onstream = async ( stream ) => {
36+ await serverMayRead ;
37+ for await ( const _ of stream ) { /* reading extends the window */ }
38+ } ;
39+ } , {
40+ sni : { '*' : { keys : [ key ] , certs : [ cert ] } } ,
41+ transportParams : {
42+ initialMaxStreamDataBidiRemote : WINDOW ,
43+ initialMaxData : 1024 * 1024 ,
44+ } ,
45+ onheaders ( ) { this . sendHeaders ( { ':status' : '200' } ) ; } ,
46+ } ) ;
47+
48+ const session = await connect ( endpoint . address , {
49+ servername : 'localhost' ,
50+ verifyPeer : 'manual' ,
51+ } ) ;
52+ await session . opened ;
53+
54+ // Budget well above the window, so the window is what stops the writer.
55+ const stream = await session . createBidirectionalStream ( { budget : 1024 * 1024 } ) ;
56+ stream . sendHeaders ( {
57+ ':method' : 'POST' ,
58+ ':path' : '/' ,
59+ ':scheme' : 'https' ,
60+ ':authority' : 'localhost' ,
61+ } , { terminal : false } ) ;
62+
63+ const writer = stream . writer ;
64+ writer . writeSync ( new Uint8Array ( BODY ) ) ;
65+
66+ // Long enough for every byte to be acked. The peer acks as data arrives,
67+ // whether or not its application has read any of it, so by now the window is
68+ // exhausted, the send buffer is empty, and no further ACK can arrive.
69+ await sleep ( 500 ) ;
70+
71+ const watchdog = setTimeout ( ( ) => {
72+ console . error ( 'STALLED: no drain after MAX_STREAM_DATA' ) ;
73+ process . exit ( 1 ) ;
74+ } , 5000 ) ;
75+
76+ letServerRead ( ) ; // extend the window, with no ack attached
77+ await writer [ drainableProtocol ] ( ) ;
78+
79+ clearTimeout ( watchdog ) ;
80+ process . exit ( 0 ) ;
0 commit comments