This error occurs when you attempt to send data on an HTTP/2 stream that has already been closed or ended. The stream closure can happen due to receiving a remote reset, the stream ending normally, or attempting operations after the stream has been destroyed. This commonly occurs with HTTP/2 connections when proper stream lifecycle management is not followed.
The "HTTP/2 stream closed" error indicates that your Node.js application tried to write data, headers, or other information to an HTTP/2 stream that is no longer active. HTTP/2 uses multiplexed streams within a single connection, and each stream has a lifecycle: created, active, and closed. When you attempt to send on a closed stream, Node.js throws this error. This differs from HTTP/1.1 connection management. In HTTP/2, individual streams can close while the underlying connection remains open. Common causes include: - The remote peer sent a RST_STREAM frame closing the stream - You attempted a second write operation on a stream that was already being written to - The request/response cycle completed and the stream was closed before you tried to write - The HTTP/2 connection itself was reset or closed The error usually manifests in server scenarios where you're writing response data or in client scenarios where you're sending request data on multiplexed streams.
Always verify the stream is still writable before attempting to write data:
const http2 = require('http2');
const server = http2.createSecureServer((req, res) => {
// Check if stream is still writable
if (!res.writableEnded) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.write(JSON.stringify({ status: 'ok' }));
res.end();
} else {
console.log('Stream already closed, cannot write response');
}
});
server.listen(443);Key properties to check:
- stream.writableEnded - true if stream has ended
- stream.writableLength - bytes of data buffered but not written
- stream.destroyed - true if stream has been destroyed
Listen for stream closure events and prevent operations on closed streams:
const http2 = require('http2');
const server = http2.createSecureServer((req, res) => {
let canWrite = true;
// Mark stream as closed when it closes
res.on('close', () => {
console.log('Stream closed');
canWrite = false;
});
res.on('error', (err) => {
console.error('Stream error:', err);
canWrite = false;
});
// Only write if stream is still open
setTimeout(() => {
if (canWrite && !res.writableEnded) {
res.writeHead(200);
res.end('Response');
} else {
console.log('Cannot write, stream is closed');
}
}, 100);
});
server.listen(443);Ensure you only write headers once and complete the response properly:
const http2 = require('http2');
const server = http2.createSecureServer((req, res) => {
// WRONG: Writing headers twice
// res.writeHead(200);
// res.writeHead(404); // Error! Headers already sent
// CORRECT: Write headers once, then write data
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write('Hello, ');
res.write('World!');
res.end(); // Complete the stream properly
});
server.listen(443);Always follow this pattern:
1. writeHead() - once, at the beginning
2. write() - zero or more times for body data
3. end() - once, to close the stream
Handle potential stream closure errors gracefully:
const http2 = require('http2');
const server = http2.createSecureServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
const sendData = (data) => {
try {
if (!res.writableEnded && !res.destroyed) {
res.write(JSON.stringify(data));
}
} catch (err) {
if (err.code === 'ERR_HTTP2_STREAM_CLOSED') {
console.log('Stream was closed, cannot send data');
} else {
console.error('Write error:', err);
}
}
};
// Safe to call multiple times
sendData({ message: 'First message' });
setTimeout(() => {
sendData({ message: 'Second message' });
}, 1000);
setTimeout(() => {
if (!res.writableEnded) {
res.end();
}
}, 2000);
});
server.listen(443);When writing large amounts of data, use the drain event to avoid buffer overflow:
const http2 = require('http2');
const server = http2.createSecureServer((req, res) => {
res.writeHead(200);
// Simulate writing large data
let canWrite = true;
const write = (data) => {
if (!canWrite) {
console.log('Write buffer full, waiting for drain');
return;
}
try {
// write returns false if buffer is full
canWrite = res.write(data);
} catch (err) {
console.error('Write error:', err);
canWrite = false;
}
};
// Resume writing when buffer is drained
res.on('drain', () => {
console.log('Buffer drained, can write more');
canWrite = true;
// Resume sending data...
});
// Close when done
res.on('close', () => {
console.log('Stream closed');
});
// Write initial chunk
write('Starting data...\n');
write('More data...\n');
});
server.listen(443);Be prepared for the remote peer to send RST_STREAM frames:
const http2 = require('http2');
const server = http2.createSecureServer((req, res) => {
// Listen for reset from client
req.on('error', (err) => {
if (err.code === 'ERR_HTTP2_STREAM_RESET') {
console.log('Client reset the stream');
} else {
console.error('Request error:', err);
}
});
res.on('error', (err) => {
if (err.code === 'ERR_HTTP2_STREAM_CLOSED') {
console.log('Cannot write, stream is closed');
} else {
console.error('Response error:', err);
}
});
// Safe to attempt write, errors are caught
try {
res.writeHead(200);
res.write('Data');
res.end();
} catch (err) {
console.log('Write failed:', err.code);
}
});
server.listen(443);Understanding HTTP/2 Stream Lifecycle
HTTP/2 streams go through several states:
- idle - Stream created but not yet active
- open - Stream is active, can send/receive data
- half-closed (local) - Local end closed (sent END_STREAM), but can still receive
- half-closed (remote) - Remote end closed (received END_STREAM), but can still send
- closed - Both sides closed, no more operations
Attempting operations in the wrong state causes ERR_HTTP2_STREAM_CLOSED.
HTTP/2 Flow Control
HTTP/2 implements flow control which can cause stream resets:
const http2 = require('http2');
const client = http2.connect('https://example.com');
const req = client.request({ ':path': '/' });
// Monitor flow control
console.log('Initial window size:', client.state.localWindowSize);
console.log('Stream window size:', req.state.localWindowSize);
req.on('data', (chunk) => {
console.log('Received:', chunk.length, 'bytes');
});If you write more data than the flow control window allows, the stream may be reset.
Multiplexing Considerations
With HTTP/2 multiplexing, multiple streams share a single connection. One stream's issues don't necessarily affect others:
const http2 = require('http2');
const server = http2.createSecureServer((req, res) => {
// Each request gets its own stream
// If one stream closes, others remain open
res.on('close', () => {
console.log('This stream closed');
// Other streams on same connection are unaffected
});
res.writeHead(200);
res.end('OK');
});
server.listen(443);Testing Stream States
Create tests to verify proper stream handling:
const http2 = require('http2');
const assert = require('assert');
const server = http2.createSecureServer((req, res) => {
res.writeHead(200);
res.end('OK');
});
server.listen(0, () => {
const client = http2.connect(`https://localhost:${server.address().port}`);
const req = client.request();
req.on('response', () => {
// Safe to read response
});
req.on('end', () => {
// Stream is closing
// Writing now would cause ERR_HTTP2_STREAM_CLOSED
});
client.close();
server.close();
});Client-Side Stream Management
When making HTTP/2 client requests, manage the request stream carefully:
const http2 = require('http2');
const client = http2.connect('https://example.com');
const req = client.request({
':method': 'POST',
':path': '/api/upload',
'content-type': 'application/json'
});
// Write request body
req.write(JSON.stringify({ data: 'value' }));
// Must end request to send it
req.end();
// Listen for response
req.on('response', (headers) => {
console.log('Response headers:', headers);
});
// Errors on this stream don't affect others
req.on('error', (err) => {
console.error('Request error:', err);
});Debugging Stream Issues
Enable HTTP/2 debugging to see stream lifecycle events:
NODE_DEBUG=http2 node server.jsThis will log detailed information about stream state changes, RST_STREAM frames, and other HTTP/2 protocol events.
Error: Listener already called (once event already fired)
EventEmitter listener already called with once()
Error: EACCES: permission denied, open '/root/file.txt'
EACCES: permission denied
Error: Invalid encoding specified (stream encoding not supported)
How to fix Invalid encoding error in Node.js readable streams
Error: EINVAL: invalid argument, open
EINVAL: invalid argument, open
TypeError: readableLength must be a positive integer (stream config)
TypeError: readableLength must be a positive integer in Node.js streams