You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
perry-ext-http: HTTP/2 res.write() backpressure never emits 'drain' (response stalls) #11158
HTTP/2 compat res.write() backpressure stalls the response: 'drain' never fires
In perry-ext-http, turnloop_route::send_body returns a single bool that means two different things depending on the transport:
HTTP/1.1 (turnloop_serve::send_body): false means the stream is gone.
HTTP/2 (turnloop_h2::h2_send_body): false means the bytes were queued but the stream is above its watermark, i.e. ordinary backpressure.
response.rs's stream_write_with_cb treats false as "not streaming" and returns None. For HTTP/2 backpressure, res.write() then:
appends the chunk (which was already queued and sent) to buffered_body, and
never sets needs_drain.
A producer that waits for 'drain' after write() returns false (every pipe()-style writer) never resumes.
This code predates #11144; the same turnloop branch is at b75e6a6eb. CodeRabbit found it while reviewing #11144.
Repro (Linux, perrymaster, Node 26.5.1). Perry built from #11144 head c1f36c25b; the path is unchanged from main.
importhttp2from'node:http2';constCHUNK='x'.repeat(64*1024);constN=32;constserver: any=http2.createServer((_req: any,res: any)=>{leti=0;letdrains=0;constpump=()=>{while(i<N){i++;if(!res.write(CHUNK)){res.once('drain',()=>{drains++;pump();});return;}}res.end();console.log(`server: wrote all ${N}, drained=${drains>0}`);};pump();});server.listen(0,'127.0.0.1',()=>{constclient=http2.connect(`http://127.0.0.1:${server.address().port}`);constreq=client.request({':path': '/'});lettotal=0;req.on('data',(c: any)=>{total+=c.length;});req.on('end',()=>{console.log(`client: bytes=${total} expected=${N*CHUNK.length}`);client.close();server.close();});req.end();setTimeout(()=>{console.log(`TIMEOUT client bytes=${total}`);process.exit(0);},5000).unref();});
== node
server: wrote all 32, drained=true
client: bytes=2097152 expected=2097152
== perry
TIMEOUT client bytes=0
With 16 chunks and write callbacks instead of 'drain', Perry matches Node (anyFalse=true callbacks=16 bytes=1048576). So the stall is specific to the 'drain' wait.
Fix direction: make turnloop_route::send_body return a tri-state (sent / backpressured / gone). HTTP/2 backpressure should then take the needs_drain = true branch, as HTTP/1.1's queued-bytes check does.
Activity