Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http2 server wait infinitely to close #57611

Open
pandeykushagra51 opened this issue Mar 25, 2025 · 2 comments · May be fixed by #57586
Open

http2 server wait infinitely to close #57611

pandeykushagra51 opened this issue Mar 25, 2025 · 2 comments · May be fixed by #57586
Labels
http2 Issues or PRs related to the http2 subsystem.

Comments

@pandeykushagra51
Copy link

Version

v23.1.0

Platform

Windows, macOS, Linux

Subsystem

http2

What steps will reproduce the bug?

Create an HTTP/2 server and call

server.close() 

while there are active client connections.

server.js

// server.js
const http2 = require('http2');
const { readFileSync } = require('fs');

// Create an HTTP/2 server
const server = http2.createSecureServer({
    key: readFileSync('localhost-privkey.pem'),
    cert: readFileSync('localhost-cert.pem'),
    sessionTimeout: 10,
});
const timer = 10000;

// Handle new connections
server.on('stream', (stream, headers) => {
  const path = headers[':path'];
  console.log(`Request received for ${path} on stream ${stream.id}`);
  stream.respond({
    'content-type': 'text/html',
    ':status': 200
  });
  stream.write(`<h1>Hello from stream ${stream.id}</h1>`);
});

server.on('session', (session) => {
  console.log('New session established');
  const interval = setInterval(() => {
    if (session.closed || session.destroyed) {
      clearInterval(interval);
    }
  }, 500);
  
  session.on('close', () => {
      clearInterval(interval);
  });
});

const PORT = 8443;
server.listen(PORT, () => {
  console.log(`Server started on port ${PORT}`);
  console.log(`Server will send GOAWAY after ${timer/1000} seconds`);
});


setTimeout(() => {
    server.close(() => console.log('__________SERVER CLOSED__________'));
    console.log('__________SERVER CLOSE IS FIRED___________');
}, timer);

client.js

const http2 = require('http2');

let client = null;
let requestCount = 0;
let receivedGoaway = false;

// Create connection
function connect() {
  receivedGoaway = false;
  client = http2.connect('https://localhost:8443', {
    rejectUnauthorized: false
  });
  
  client.on('error', err => console.error('Connection error:', err));
  client.on('close', () => console.log('Connection closed'));
  client.on('goaway', () => {
    console.log('GOAWAY received');
    receivedGoaway = true;
    
    // Reconnect after goaway
    setTimeout(() => {
      connect();
      sendRequest();
    }, 1000);
  });
}

// Send a single request
function sendRequest() {
  // Skip if connection got GOAWAY
  if (receivedGoaway) return;
  
  requestCount++;
  console.log(`Opening Stream/Request #${requestCount}`);
  
  // Create request
  const req = client.request({ 
    ':path': `/req-${requestCount}`,
    ':method': 'POST'
  });
  
  // Simple handlers
  req.on('response', headers => console.log(`Response: ${headers[':status']}`));
  req.on('data', chunk => console.log(`Data: ${chunk.toString().trim()}`));
  req.on('end', () => console.log(`Stream #${requestCount} complete`));
  req.on('error', err => console.error(`Stream error:`, err.message));
  
  // Write and end
  req.write(`Data for request ${requestCount}`);
  req.end(`Final data for ${requestCount}`);
  
  if (!receivedGoaway) {
    setTimeout(sendRequest, 1000);
  }
}

// Start client
connect();
sendRequest();

localhost-privkey.pem

-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJxbqj5m3795uf
Y88Dgq101Edgm+ezvcSkbvgUh0dzJUqIkXHHgM19TBajSBh97vFgeF67qols796T
EogDviqgWFIlJZtSxFdK/5m7Flyw11I8BSajHiEJqNQDI5FZ3c2Vo37onU5wuVXS
MhBzZu+dL/6ILn3/7bDtf02XMjQfZUzukmXSmbATRH129bC29PiyWbpsyNsjz97U
MaAg0NfbWOGBF6LhuWy7Wevpky2bWQS0iqPilrnDleX45CFhvYXNhsW04964sFU7
d2hR4sHJeAaMRWLOqGiW5r+6lZ974qOQNW60gQcLZkYb+yT/CeSxVkLOQrKUZBSd
Gv7k8EH9AgMBAAECggEAQb/5Lees92qcf7gagV5X/7Yc/FJRkrTIG309whLDKbaB
LzeWCBjk3WuqBNM9h+wRJAqVdDoCRkl0EiD7yZxyb4XYXtp+Xt40Q/A+vERxN/8y
gPR8vrLWuTsWu18fwlUQ5S498LcyFHGQkcpWqgRlGKfHHcFotblS6lGNizOE4KWh
2bKErgFhtzMX2yy5fxIMKVL8mRmdaHxv7xwC/I3Txr+qpdKL0CryOE10RuoeR07X
Cgd4000EZjhYifuQLXhxebsEiKeBRP5Ir1bxHVpqukBMvgFaO/5Pq3Llij55Adjx
xnqJ5LeMc+/hz/V/BqlOwwzS3bVnHBzslFjYOpJm0QKBgQDpyYLKONmpozlHQuf2
IUTHBS/iBAaVEQsXFMZwNUoKuNY/Q0+lMrcVj0/Zz2kwHzcQEvQsbHnQJbulHAn7
xLl6B7LfrB+57MZ0iMuXQ0pWQa4rQkr9HHQZVM4k9BhTr4RrFbbKME8jVxEt5Yto
6JxbJGhRdHKk4ZoMf42AjzOJ9wKBgQDc8YLllGctzAAZT5HFwmHawJfRQ4YdlEHH
Rg9vfmJ/+lXtsf7TN1faXqEmt0OtM3n2QETe/4NJkmJAc4HAPJUzTf9D7CPf+9Hu
WA1KpvuscggCuTHfFkR48+iMO61BDcuMikQj7QrMBhTerPmNbmkPnJ0gKqzx6u/n
NQfaQp82qwKBgCqoQBsR4HVZePwNszFvxJLj2WbOAT11zKY7fjG/J3FpZH5Kk9+6
rzlZ2uINPE4xg+SE8NSiPl/CYsivowqzTHx9px+00l0kXNmqlCtXddrjRRpTkEc6
x9xPYwXPHBk471pyrdWalvYFzvam6ZcMymq+6+Hg84IuP+OD6pfIiUjlAoGBAMZI
+PCQ/whazLvqbSjOCoQH1Dg6IWLqax4sAi5NfpjcUcHAvLFSOcYApx/X2STXzdzy
UNzQ80JT3Vl6UDf2JvCkTzLl+kxJ8120KDmXIsbgj8/h5KARm+HxBALWi+aWOtcm
P4D+e3IfAxvUoSIMtL0OEPWNVyjFcAhPz3xRzhGBAoGAZM4R21Y7DXywjTLO4uiK
B8jNTwRSVIbP+8hwHfZl4sul9p0VlVeVuU7AaRYi7oPLRcbaMlZawU5q7mK2+BrG
L0gcP987UIReDYUs4rxm0qYTkBhBKg2xio9IFUXPLbo3/60ZOWHL2xk6PT9Fqg8s
8Hzp3UYAnwR/trff5u0ra5o=
-----END PRIVATE KEY-----

localhost-cert.pem

-----BEGIN CERTIFICATE-----
MIICpDCCAYwCCQDOOPaQDlpHfDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
b2NhbGhvc3QwHhcNMjUwMzE1MTIzMzQ5WhcNMjUwNDE0MTIzMzQ5WjAUMRIwEAYD
VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJ
xbqj5m3795ufY88Dgq101Edgm+ezvcSkbvgUh0dzJUqIkXHHgM19TBajSBh97vFg
eF67qols796TEogDviqgWFIlJZtSxFdK/5m7Flyw11I8BSajHiEJqNQDI5FZ3c2V
o37onU5wuVXSMhBzZu+dL/6ILn3/7bDtf02XMjQfZUzukmXSmbATRH129bC29Piy
WbpsyNsjz97UMaAg0NfbWOGBF6LhuWy7Wevpky2bWQS0iqPilrnDleX45CFhvYXN
hsW04964sFU7d2hR4sHJeAaMRWLOqGiW5r+6lZ974qOQNW60gQcLZkYb+yT/CeSx
VkLOQrKUZBSdGv7k8EH9AgMBAAEwDQYJKoZIhvcNAQELBQADggEBACuudMA5tC3I
sqQ1S81Hm8noqzx+09zPD05gaX86iXyU/aK/3qutJOTlNLpg0XQ+NW5UVNBluJPd
b0ECB/nj8I7LaxUQM+tWovn4/Ul5ih8I6bk+mZpxKf6rL7EbItsUhOVnZAMEqhnb
yBcdD0bonaVCJNIcVmOCfltDXtnrNG1fBQGOD8OXDCj87uWWaaw5iHwDSHH071PT
7dGDvNJ56lxxlzec4HqRobXtvq+h+zD1K4azvTbJ66sa7EFGsth7GtQxHdx5LsyI
RD2rCFhH7S99ildeP4GmA0ixSE7UOobcAERDj8m/6pezpjcSwWjFrdWHroLpWSY/
tzTferlXrBI=
-----END CERTIFICATE-----

Steps to reproduce:

  1. Keep all of the above file(server.js, client.js, localhost-privkey.pem, localhost-cert.pem) in same folder.
  2. Run
node server.js
  1. Run below script with 10s of running server.js
node client.js

server.close is fired after 10s of server start and if it do not find any active request it automatically closes this is why client need to start within 10s of starting server

How often does it reproduce? Is there a required condition?

This reproduce always when server have fired close call but it have active client connections.

What is the expected behavior? Why is that the expected behavior?

According to the Node.js documentation:
server.close()

stops the server from accepting new connections and closes all connections connected to this server which are not sending a request or waiting for a response.

The server should:

  • Stop accepting new connections (currently works)
  • Close all existing HTTP/2 sessions gracefully by sending GOAWAY frames (not working)
  • Allow in-flight requests to complete (currently works)
  • Prevent new streams (requests) on existing connections (not working)
  • Terminate cleanly after all active streams finish (not working)

The server should not wait for client to close instead server should close connection once it have processes all active stream/request.

What do you see instead?

Currently:

  • The server stops accepting new connections
  • But existing connections remain open indefinitely
  • New streams (requests) can be initiated on existing connections
  • The server's close callback may never be called if clients maintain open connections
  • Start of server shutdown can starve indefinitely, leading to processes that cannot terminate cleanly and causing resource leaks

Additional information

As per http2 docs, if there is need of connection close, server should send goaway frame and ensure gracefull session close by processing all active request(or stream) and discarding any new request.
Refer paragraph 5 of http2 link

@pandeykushagra51
Copy link
Author

pandeykushagra51 commented Mar 25, 2025

I have attached a PR which ensure that server will stop accepting any request and it will close automatically once it is done processing all active request.

@pandeykushagra51 pandeykushagra51 changed the title http2 server wait to close infinitely http2 server wait infinitely to close Mar 25, 2025
@bjohansebas bjohansebas added the http2 Issues or PRs related to the http2 subsystem. label Mar 25, 2025
@pandeykushagra51
Copy link
Author

pandeykushagra51 commented Mar 26, 2025

@bjohansebas could you please check the PR attached.
It is minimal changes (only 20 line of code) and I have added strong tests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
http2 Issues or PRs related to the http2 subsystem.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants