http2: async iteration reports premature close with buffered responses in v26.3.0+ and v24.20.0
Mantenedores costumam responder em até 1 dia
Ninguém assumiu esta issue ainda.
Avaliação
- Dificuldade
- 4/5
- Tempo estimado
- 3-5 dias
- Facilidade para iniciantes
- 65/100
- Tipo de issue
- Bug
- Clareza
- Claramente especificada
- Status de atividade
- Ativa
- Stack de tecnologia
- javascript, nodejs
- Domínio
- backend, networking
Direção de pesquisa
Execute repro-http2-premature-close.mjs com as versões do Node listadas e, em seguida, inspecione node:internal/streams/end-of-stream e node:internal/streams/readable em torno de eos() e createAsyncIterator. Compare o comportamento introduzido por #62394 com seu backport. Está concluído quando um stream HTTP/2 fechado normalmente, com dados armazenados em buffer, é totalmente consumido por for await sem ERR_STREAM_PREMATURE_CLOSE.
Escrita pelo modelo de indexação a partir do texto da issue.
Descrição
Version
v26.8.1 and v24.20.0 (official binaries). The regression starts between v26.2.0 and v26.3.0; v24.18.0 also passes. See the comparison below.
Platform
macOS, Darwin 25.6.0, arm64.
Subsystem
http2, stream (eos() and Readable async iteration)
What steps will reproduce the bug?
Save this as repro-http2-premature-close.mjs and run it with node repro-http2-premature-close.mjs. It uses only Node built-in modules, localhost, and an ephemeral port. No proxy or third-party dependency is needed.
// Run with an unmodified Node binary: node repro-http2-premature-close.mjs
// Uses only built-in modules, localhost, and an ephemeral port.
import assert from 'node:assert/strict';
import { once } from 'node:events';
import http2 from 'node:http2';
import { setImmediate } from 'node:timers/promises';
const server = http2.createServer();
server.on('stream', (stream) => {
stream.resume();
stream.on('end', () => {
stream.respond({ ':status': 200 }, { waitForTrailers: true });
stream.on('wantTrailers', () => stream.sendTrailers({ 'grpc-status': '0' }));
stream.end('complete response');
});
});
server.listen(0, '127.0.0.1');
await once(server, 'listening');
const session = http2.connect(`http://127.0.0.1:${server.address().port}`);
try {
await once(session, 'connect');
const request = session.request({ ':method': 'POST' });
const response = once(request, 'response');
request.end('request');
await response;
// Delay consuming the response until HTTP/2 reports a normal stream close.
// The complete response body is still buffered, and the stream is not destroyed.
const deadline = Date.now() + 2_000;
while (!request.closed && Date.now() < deadline) await setImmediate();
assert.equal(request.closed, true);
assert.equal(request.rstCode, 0);
assert.equal(request.destroyed, false);
assert.equal(request.readableLength, Buffer.byteLength('complete response'));
let body = '';
for await (const chunk of request) body += chunk.toString();
assert.equal(body, 'complete response');
console.log(`${process.version}: PASS`);
} finally {
session.destroy();
await once(session, 'close');
server.close();
await once(server, 'close');
}
How often does it reproduce? Is there a required condition?
The equivalent direct-connection test failed on all 20 attempts in each affected version listed below. The required condition is that iteration starts after the HTTP/2 stream closes normally, while the response body is still buffered.
Before reading, the reproduction asserts closed === true, rstCode === 0, destroyed === false, and that all 17 response bytes remain in readableLength. It does not destroy the stream before consuming it; session destruction is only in cleanup.
Without intentionally waiting for this state, the failure depends on scheduling. Downstream gRPC calls through proxy/Duplex transports exposed it, but the reproduction removes both the proxy and the gRPC library.
What is the expected behavior? Why is that the expected behavior?
for await consumes complete response successfully and finishes without an error. The process exits with status 0, as it does on v24.18.0 and v26.2.0. The HTTP/2 stream closed normally and its complete response is still readable.
What do you see instead?
for await throws ERR_STREAM_PREMATURE_CLOSE; the process exits with status 1. On v26.8.1:
Error [ERR_STREAM_PREMATURE_CLOSE]: Premature close
at getEosOnCloseError (node:internal/streams/end-of-stream:98:14)
at eos (node:internal/streams/end-of-stream:159:23)
at start (node:internal/streams/readable:1425:15)
at Object.next (node:internal/streams/readable:1574:23)
v24.20.0 fails with the same error through getEosOnCloseError / eos / createAsyncIterator.
Additional information
Version comparison for the equivalent direct-connection reproduction, delaying consumption until request.closed:
| Node version | Successful responses / attempts |
|---|---|
| 24.18.0 | 20 / 20 |
| 24.20.0 | 0 / 20 |
| 26.2.0 | 20 / 20 |
| 26.3.0 | 0 / 20 |
| 26.7.0 | 0 / 20 |
| 26.8.1 | 0 / 20 |
The single-case script above was also verified separately with v24.18.0 and v26.2.0 (exit 0), and with v24.20.0, v26.3.0 and v26.8.1 (exit 1).
Suspected regression mechanism: the version boundary and source comparison point to #62394, cbee0de1cb, released in v26.3.0. The same change was backported as 5658c631fa in v24.20.0.
Http2Stream.closed can be true after a normal protocol close while the readable body is still buffered. eos() now computes immediateResult before that buffer is consumed, then reports the captured premature-close error in a subsequent process.nextTick() callback. The previous implementation evaluated the close error on the next tick instead.
A diagnostic comparison on v26.8.1, keeping its async iterator unchanged and only recomputing the close result inside the next-tick callback, changes the direct-connection test from 0/20 to 20/20 successful responses. That is evidence for the timing regression, not a proposed production monkey patch.
Related reports checked: #63989 and #64098 concern HTTP/1.1 keep-alive / node-fetch. This reproduction uses core HTTP/2 with a normal protocol close, and also fails on v26.8.1 after those reports were resolved. #44866 is the older Web Stream termination hang addressed by #62394, rather than this new async-iteration failure.
- Linguagem predominante
- JavaScript
- Estrelas
- 122k
- Forks
- 37.4k
- Merge médio
- 4d 17h
- PRs com merge (30d)
- 300
Preparar o ambiente
Primeiros passos
- Leia a issue inteira e depois o guia de contribuição do projeto.
- Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
- Faça um fork do repositório e trabalhe em uma branch.
- Abra um pull request que referencie o número da issue.
Mais de nodejs/node
-
`test-fs-cp-async-socket` / `test-fs-cp-sync-copy-socket-error` failure on Darwin (path length)Abertatest
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 68/100
Mantenedores costumam responder em até 1 dia
-
doc
Dificuldade 1/5 Menos de uma hora Facilidade para iniciantes 90/100
Mantenedores costumam responder em até 1 dia
-
doc
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 65/100
Mantenedores costumam responder em até 1 dia
-
build
Dificuldade 1/5 Menos de uma hora Facilidade para iniciantes 88/100
nodejs/node#66076 · 2 comentários ·
Mantenedores costumam responder em até 1 dia
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 84/100
nodejs/node#65994 · 2 comentários · 2 reações ·
Mantenedores costumam responder em até 1 dia
Todas as issues de nodejs/node
Issues semelhantes
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 75/100
Mantenedores costumam responder em até 1 dia
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 86/100
Mantenedores costumam responder em até 1 dia
-
curriculum documentation quality
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
githubnext/gh-aw-workshop#3897 ·
Mantenedores costumam responder em até 2 dias
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 84/100
Mantenedores costumam responder em até 1 dia
-
agent/quality hive/hosted-available-lke648397-260827-5n31 quality testing
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 91/100
Mantenedores costumam responder em até 1 dia