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
http2: large queued request body prevents receiving an early response #66739
Darwin miki.local 25.6.0 Darwin Kernel Version 25.6.0: Sat Jul 11 15:27:04 PDT 2026; root:xnu-12377.161.13~4/RELEASE_ARM64_T6050 arm64
Subsystem
http2
What steps will reproduce the bug?
Save the following code into a file and run it. For the 16MB request with the default session limit, node reports ERR_HTTP2_STREAM_ERROR: Stream closed with error code NGHTTP2_ENHANCE_YOUR_CALM rather than the 413.
// A client that queues a large request body cannot read an early response.// Cleartext HTTP/2 (h2c), no dependencies: `node repro.mjs`.importhttp2from'node:http2';constserver=http2.createServer();constsessions=newSet();server.on('session',(session)=>sessions.add(session));server.on('stream',(stream)=>{stream.on('error',()=>{});// the client may reset the stream// Reject without reading the request body, as a server does for a 401 or 413.stream.respond({':status': 413});stream.end('payload too large');});awaitnewPromise((resolve)=>server.listen(0,resolve));constorigin=`http://localhost:${server.address().port}`;constpost=(bodyBytes,sessionOptions={})=>newPromise((resolve)=>{constsession=http2.connect(origin,sessionOptions);constreq=session.request({':method': 'POST',':path': '/'});req.on('response',(headers)=>resolve(`status ${headers[':status']}`));req.on('error',(err)=>resolve(`${err.code}: ${err.message}`));req.on('close',()=>session.destroy());req.end(Buffer.alloc(bodyBytes));// the whole body in one write});constMiB=1024*1024;console.log(`node ${process.version}`);console.log(' 8 MiB body: ',awaitpost(8*MiB));console.log('16 MiB body: ',awaitpost(16*MiB));console.log('16 MiB, maxSessionMemory 64 MB:',awaitpost(16*MiB,{maxSessionMemory: 64}));for(constsessionofsessions)session.destroy();server.close();
How often does it reproduce? Is there a required condition?
To reproduce, more than maxSessionMemory worth of request-body data must be queued when the response arrives.
What is the expected behavior? Why is that the expected behavior?
I'd expect consistent propagation of the server's 413 responses
What do you see instead?
Opaque stream error (ERR_HTTP2_STREAM_ERROR: Stream closed with error code NGHTTP2_ENHANCE_YOUR_CALM) and no response
It looks intentional rather than a regression. The IsAvailableSessionMemory check in Http2Stream::AddHeader came in with maxSessionMemory itself (#17967, 882e7ef354). The description says that existing streams still receiving headers get terminated with ENHANCE_YOUR_CALM.
But the docs only say that existing streams may cause the limit to be exceeded and new ones are rejected.
There is no mention that header receipt on existing streams gets cut off.
So either the docs need updating or the behavior does.
FWIW, on current main (e2d55227) the same repro gives rstCode 2 (INTERNAL_ERROR) instead of 11 so it won't match the report.
nodejs/http2 Could you clarify whether this limit should prevent receiving response headers on an existing stream?
I don't think it should in this specific case. The docs description doesn't match this behaviour (it says the limit just affects new streams, as @GBra-4-669 mentioned). In this scenario, the block isn't really providing any useful protection imo - i.e. I don't think there'd be a way to abuse this to retain significant memory unexpectedly.
The memory limit is here primarily for servers, to stop a client using up unlimited & disproportionate memory with inbound streams & requests. On the client side, there are some related risks, but they're much more limited - for these response headers specifically, memory usage is tightly bounded by the requests you send intentionally. I can't see a good argument for blocking this.
That said, this is a delicate and security-sensitive code, so I might've missed something, it will need some careful discussion, and I think everybody related to both security & HTTP/2 is swamped at the moment! I agree it would be good to improve this though.
I think there's probably a safe route through by disabling blocking on memory usage here, exclusively for client-side + locally initiated streams (i.e. request streams, not push streams opened by the server). 1xx response headers (which the server can send unlimited times) need a little thought, but I don't think we actually retain them so that shouldn't be a problem.
Notably, I think this wouldn't happen if the request body was sent in a backpressure-sensitive way. I think request.end(data) is reasonable code and we do need to make that work, but in any scenario where you are piping data, or doing incremental write(data) calls, if you correctly observe backpressure you won't hit this - you should automatically stop writing and wait for more window before reaching the buffer limits, and then everything will work as expected. That's not a perfect solution, and as I say I do think you should expect the above code to work, but it might be a workaround in the meantime, and it does mean this won't hit in practice in many existing cases.
Activity
inoway46 commentedon Oct 15, 2026
Reproduced with a local main build (macOS x64, 38aa1f3), which reports
NGHTTP2_INTERNAL_ERROR.Queued outbound data exceeds the session memory limit, so
AddHeader()rejects response headers. The reset is deferred during receive processing, and nghttp2 queuesINTERNAL_ERRORfor the callback failure.On this build, increasing
maxSessionMemoryto 64 or streaming with backpressure allows the 413 response through without errors.@nodejs/http2 Could you clarify whether this limit should prevent receiving response headers on an existing stream?
GBra-4-669 commentedon Oct 18, 2026
It looks intentional rather than a regression. The
IsAvailableSessionMemorycheck inHttp2Stream::AddHeadercame in withmaxSessionMemoryitself (#17967,882e7ef354). The description says that existing streams still receiving headers get terminated withENHANCE_YOUR_CALM.But the docs only say that existing streams may cause the limit to be exceeded and new ones are rejected.
There is no mention that header receipt on existing streams gets cut off.
So either the docs need updating or the behavior does.
FWIW, on current main (
e2d55227) the same repro givesrstCode2 (INTERNAL_ERROR) instead of 11 so it won't match the report.pimterry commentedon Oct 22, 2026
I don't think it should in this specific case. The docs description doesn't match this behaviour (it says the limit just affects new streams, as @GBra-4-669 mentioned). In this scenario, the block isn't really providing any useful protection imo - i.e. I don't think there'd be a way to abuse this to retain significant memory unexpectedly.
The memory limit is here primarily for servers, to stop a client using up unlimited & disproportionate memory with inbound streams & requests. On the client side, there are some related risks, but they're much more limited - for these response headers specifically, memory usage is tightly bounded by the requests you send intentionally. I can't see a good argument for blocking this.
That said, this is a delicate and security-sensitive code, so I might've missed something, it will need some careful discussion, and I think everybody related to both security & HTTP/2 is swamped at the moment! I agree it would be good to improve this though.
I think there's probably a safe route through by disabling blocking on memory usage here, exclusively for client-side + locally initiated streams (i.e. request streams, not push streams opened by the server). 1xx response headers (which the server can send unlimited times) need a little thought, but I don't think we actually retain them so that shouldn't be a problem.
Notably, I think this wouldn't happen if the request body was sent in a backpressure-sensitive way. I think
request.end(data)is reasonable code and we do need to make that work, but in any scenario where you are piping data, or doing incrementalwrite(data)calls, if you correctly observe backpressure you won't hit this - you should automatically stop writing and wait for more window before reaching the buffer limits, and then everything will work as expected. That's not a perfect solution, and as I say I do think you should expect the above code to work, but it might be a workaround in the meantime, and it does mean this won't hit in practice in many existing cases.