Repository: nodejs/node
Pull request: http: fix keep-alive not timing out after post-request empty line

Fixes: https://github.com/nodejs/node/issues/58140

As shown in the test code, sending an empty line after a request can result in a state where the keep-alive timer is reset, but neither requestTimeout nor keepAliveTimeout is active.
Unless a custom timeout is implemented, this allows the client to hold the socket indefinitely.

Modified behavior so that data like an empty line, which does not indicate the start of an HTTP message, no longer resets the keep-alive timeout.


### FYI
Here is the behavior of other HTTP servers:

nginx: When client_header_timeout is set, sending an empty line after a request triggers a 408 timeout response once the timeout period expires.

Apache: When RequestReadTimeout header=5-10,MinRate=500 is configured, the connection times out after the specified duration, but no error code is sent.

However, since the timing for resetting the keep-alive timeout is not clearly defined in RFC 9112 or similar specifications, I believe this change is appropriate.

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: lib/_http_server.js
patch: @@ -918,8 +918,6 @@ function socketOnError(e) {
 }
 
 function onParserExecuteCommon(server, socket, parser, state, ret, d) {
-  resetSocketTimeout(server, socket, state);
-
   if (ret instanceof Error) {
     prepareError(ret, parser, d);
     debug('parse error', ret);

filename: test/parallel/test-http-keep-alive-empty-line.mjs
patch: @@ -0,0 +1,50 @@
+import * as common from '../common/index.mjs';
+import assert from 'node:assert';
+import { createServer } from 'node:http';
+import { connect } from 'node:net';
+
+const server = createServer({
+  connectionsCheckingInterval: 100,
+  headersTimeout: 100,
+  keepAliveTimeout: 300
+}, (req, res) => {
+  res.writeHead(404);
+  res.end();
+
+  req.socket.on('close', common.mustCall(() => {
+    server.close();
+  }));
+});
+
+server.listen(0, () => {
+  const client = connect({
+    host: 'localhost',
+    port: server.address().port,
+  }, () => {
+    client.write(
+      'GET / HTTP/1.1\r\n' +
+          'Host: localhost:3000\r\n' +
+          'Content-Length: 0\r\n' +
+          '\r\n'
+    );
+
+    setTimeout(() => {
+      client.write('\r\n');
+    }, 100);
+
+    let responseBuffer = '';
+
+    client.on('data', (chunk) => {
+      responseBuffer += chunk.toString();
+
+      // Check if we've received the full header (ending with \r\n\r\n)
+      if (responseBuffer.includes('\r\n\r\n')) {
+        const statusLine = responseBuffer.split('\r\n')[0];
+        const status = statusLine.split(' ')[1];
+        assert.strictEqual(status, '404');
+        client.end();
+      }
+    });
+    client.on('end', common.mustCall());
+  });
+});