node icon indicating copy to clipboard operation
node copied to clipboard

http: resume kept-alive when no body allowed

Open mweberxyz opened this issue 11 months ago • 23 comments

In accordance with https://www.rfc-editor.org/rfc/rfc9112#name-message-body-length: HEAD, 1xx, 204, and 304 responses cannot contain a message body.

If a connection will be kept-alive, resume the socket during parsing so that it may be returned to the free pool, even if the caller has not consumed the known-expected-empty response.

Fixes nodejs/node#47228

Note: The original source of the issue reporter's issue was due to failure to consume the response body of a 3xx request, which this change will not resolve. As an option, 3xx with a Content-Length of 0 could receive the same auto-resume treatment. I am hesitant to propose that change, as some HTTP servers may return a message body (ie <a href=here>Moved Here</a>), which would then make the caller's responsibility to consume or resume dependent on the particular remote server's behavior.

mweberxyz avatar Apr 02 '24 17:04 mweberxyz

Review requested:

  • [ ] @nodejs/http
  • [ ] @nodejs/net

nodejs-github-bot avatar Apr 02 '24 17:04 nodejs-github-bot

Setting to draft, went to fix review nit and add tests and I am not convinced issue is fully resolved.

mweberxyz avatar Apr 05 '24 15:04 mweberxyz

@ShogunPanda I added tests and discovered that my approach putting the logic in in an else if following the !shouldKeepAlive condition will work for 204 and 304s, but not for HEAD, because at this point shouldKeepAlive is set to false for HEAD requests -- this is coming from llhttp.

mweberxyz avatar Apr 05 '24 17:04 mweberxyz

This is potentially unsafe. Some servers don't properly end HEAD requests and there is no way to detect it. Such a case would potentially corrupt following responses from the same connection. Which is why in undici we always reset the connection after a HEAD request, unless explicitly told otherwise (reset: false option).

ronag avatar Apr 09 '24 10:04 ronag

@ronag re-using a socket following a HEAD request is the existing logic.

I checked with the following replication:

const http = require("http");

const noKeepalive = process.argv.at(-1) === "no-keepalive";

const opts = {};

if (noKeepalive) {
  opts.agent = new http.Agent({ keepAlive: false });
}

console.log({
  version: process.version,
  keepAlive: !noKeepalive,
});

http.get("http://www.example.com", { method: "HEAD", ...opts }, (res) => {
  res.resume();
  const headSocket = res.socket;
  setTimeout(() => {
    http.get("http://www.example.com", { method: "GET", ...opts }, (res2) => {
      console.log({ sameSocket: headSocket === res2.socket });
    });
  }, 500);
});

gives:

% node index.js keepalive
{ version: 'v20.12.1', keepAlive: true }
{ sameSocket: true }

and

% node index.js no-keepalive
{ version: 'v20.12.1', keepAlive: false }
{ sameSocket: false }

Should I take a look at that first before returning to this PR?

mweberxyz avatar Apr 09 '24 16:04 mweberxyz

What does this PR do then?

ronag avatar Apr 09 '24 16:04 ronag

I think the problem might apply to all expected empty responses. I need to check that for undici as well.

ronag avatar Apr 09 '24 16:04 ronag

What does this PR do then?

Makes it so the res.resume() following a HEAD request, a request that returns a 204, or a request that returns a 304 is implicit.

mweberxyz avatar Apr 09 '24 16:04 mweberxyz

Makes it so the res.resume() following a HEAD request, a request that returns a 204, or a request that returns a 304 is implicit.

Sorry, I don't understand.

ronag avatar Apr 09 '24 16:04 ronag

FWIW what lead me here is a package that was using node:https and not consuming the response on a request that returned a 302, which prevented node from exiting until the keepAlive timed out. I can't fix that because a 302 could return a response, so it should be up to the caller to avoid keeping node from exiting, but figured in these three cases where an http request could not possibly return a response, per spec, we can safely do this automatically.

Will push changes for lint errors shortly.

mweberxyz avatar Apr 09 '24 16:04 mweberxyz

In a twist of irony, my test appears to have left a socket open. Will take a look and push fix.

mweberxyz avatar Apr 09 '24 18:04 mweberxyz

Ready for ci: the server in the test needed to be unref'd.

mweberxyz avatar Apr 09 '24 20:04 mweberxyz

I can't replicate that final test-asan failure locally - let me know if there's anything else I need to do to get this ready to land 👍

mweberxyz avatar Apr 10 '24 15:04 mweberxyz

CI: https://ci.nodejs.org/job/node-test-pull-request/58243/

nodejs-github-bot avatar Apr 10 '24 18:04 nodejs-github-bot

CI: https://ci.nodejs.org/job/node-test-pull-request/58320/

nodejs-github-bot avatar Apr 12 '24 13:04 nodejs-github-bot

CI: https://ci.nodejs.org/job/node-test-pull-request/58594/

nodejs-github-bot avatar Apr 22 '24 13:04 nodejs-github-bot

CI: https://ci.nodejs.org/job/node-test-pull-request/58600/

nodejs-github-bot avatar Apr 22 '24 14:04 nodejs-github-bot

CI: https://ci.nodejs.org/job/node-test-pull-request/58672/

nodejs-github-bot avatar Apr 24 '24 15:04 nodejs-github-bot

I don't understand the CI failures. Merging main in to keep up-to-date.

mweberxyz avatar Apr 24 '24 22:04 mweberxyz

CI: https://ci.nodejs.org/job/node-test-pull-request/58748/

nodejs-github-bot avatar Apr 27 '24 14:04 nodejs-github-bot

CI: https://ci.nodejs.org/job/node-test-pull-request/58763/

nodejs-github-bot avatar Apr 27 '24 21:04 nodejs-github-bot

CI: https://ci.nodejs.org/job/node-test-pull-request/58775/

nodejs-github-bot avatar Apr 28 '24 09:04 nodejs-github-bot