How to fix Error: HTTP/2 stream closed (cannot send on closed stream) in Node.js

Node.jsINTERMEDIATEMEDIUMUpdated Dec 9, 2025ยท 103 views

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.

Quick fix

Fixes ERR_HTTP2_STREAM_CLOSED

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);
  1. Check stream state before writing
  2. Handle stream close events
  3. Avoid multiple writes to the same stream
  4. Wrap write operations in try-catch
node.js โ€” error
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);
Error: HTTP/2 stream closed (cannot send on closed stream)

What this error means

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.

Typical symptoms

  • Error thrown when trying to write response headers or body in HTTP/2 request handler
  • Application crashes with "Error: HTTP/2 stream closed"
  • Intermittent failures in HTTP/2 client requests with stream operations
  • Server stops responding properly to certain HTTP/2 requests
  • Stream operations fail after receiving a server reset from remote peer
  • Multiple write attempts on the same HTTP/2 stream fail
  • Client connections hang or close unexpectedly with stream-related errors

Common causes

  1. Attempting to write to a stream after it has already been closed by the remote peer
  2. Race condition where stream is closed between checking if open and writing data
  3. HTTP/2 connection receives RST_STREAM frame from remote peer before local write completes
  4. Trying to write response headers multiple times on the same stream
  5. Writing response data after the stream has already ended normally
  6. Not properly handling stream close events before attempting operations
  7. Timeout causing stream to close while data is still being written
  8. HTTP/2 flow control violations causing stream to be reset

How to fix "Error: HTTP/2 stream closed (cannot send on closed stream)"

1Check stream state before writing

Always verify the stream is still writable before attempting to write data:

javascript
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

2Handle stream close events

Listen for stream closure events and prevent operations on closed streams:

javascript
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);
3Avoid multiple writes to the same stream

Ensure you only write headers once and complete the response properly:

javascript
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

4Wrap write operations in try-catch

Handle potential stream closure errors gracefully:

javascript
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);
5Use drain event for large writes

When writing large amounts of data, use the drain event to avoid buffer overflow:

javascript
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);
6Handle HTTP/2 reset frames

Be prepared for the remote peer to send RST_STREAM frames:

javascript
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);

Advanced notes

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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

bash
NODE_DEBUG=http2 node server.js

This will log detailed information about stream state changes, RST_STREAM frames, and other HTTP/2 protocol events.

Frequently asked questions

What does "ERR_HTTP2_STREAM_CLOSED" mean?
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. js throws this error.
How do I fix "ERR_HTTP2_STREAM_CLOSED"?
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.
What causes "ERR_HTTP2_STREAM_CLOSED"?
Attempting to write to a stream after it has already been closed by the remote peer Race condition where stream is closed between checking if open and writing data HTTP/2 connection receives RST_STREAM frame from remote peer before local write completes
Is the "ERR_HTTP2_STREAM_CLOSED" error serious?
It is a medium-severity error โ€” more of a blocker than a risk. The steps above resolve it.

Official resources & further reading