Skip to content

fix(eio-client): honor Max-Age=0 and unparseable expiry attributes - #5585

Open
simpleqt wants to merge 1 commit into
socketio:mainfrom
simpleqt:fix/cookie-jar-max-age-zero
Open

simpleqt wants to merge 1 commit into
socketio:mainfrom
simpleqt:fix/cookie-jar-max-age-zero

Conversation

@simpleqt

@simpleqt simpleqt commented Nov 3, 2026

Copy link
Copy Markdown

fix(eio-client): honor Max-Age=0 and unparseable expiry attributes

Root cause

CookieJar (used by the Node HTTP long-polling and WebSocket transports to replay cookies on
subsequent requests) treated a cookie as expired only once the clock had moved strictly past its
expiry time, and stored Invalid Date whenever an expiry attribute could not be parsed.

packages/engine.io-client/lib/globals.node.ts:

// before
if (cookie.expires?.getTime() < now) {   // line 88
  this._cookies.delete(name);
}
// before
case "Max-Age":
  const expiration = new Date();
  expiration.setUTCSeconds(expiration.getUTCSeconds() + parseInt(value, 10));
  cookie.expires = expiration;

Two consequences:

  1. Max-Age=0 did not delete anything. Zero is the canonical way for a server to ask a client to
    discard a cookie (RFC 6265, section 5.2.2: "If delta-seconds is less than or equal to zero (0),
    let expiry-time be the earliest representable date and time."
    ). It produced expires equal to
    "now", and because the check was a strict <, the cookie survived as long as the current
    millisecond had not yet passed. A session cookie that the server had just revoked was therefore
    replayed on the next polling request. Max-Age=0 sent to overwrite a previously stored value
    had the same problem.
  2. Unparseable attributes never expired. parseInt("abc") is NaN, and new Date("not-a-date")
    is an Invalid Date. Both yield getTime() === NaN, and every comparison with NaN is false,
    so the cookie stayed in the jar forever — a malformed deletion request silently became a
    permanently replayed cookie.

Reproduction

const { CookieJar } = require("engine.io-client/build/cjs/globals.node.js");

const jar = new CookieJar();
jar.parseCookies(["foo=bar; Max-Age=0"]);
console.log([...jar.cookies]); // before: [["foo", {...}]]  after: []

Fix

  • Max-Age of zero or less, and any unparseable Max-Age, now set the expiry to new Date(0).
  • An unparseable Expires date now also sets new Date(0).
  • The expiry check is now inclusive (<=) and explicit about the "no expiry" case, so a cookie is
    expired as soon as its expiry time is reached.

new Date(0) is used as the "earliest representable date and time" the RFC asks for: it is
unambiguously in the past for every supported runtime and keeps the check a single comparison.
MySQL-free, no API change, strict: false in the package tsconfig, and tsc passes with no
errors.

Test evidence

Added a cookie expiry suite to packages/engine.io-client/test/node.js, covering Max-Age=0, a
negative Max-Age, an unparseable Max-Age, an unparseable Expires, a future Max-Age, a plain
session cookie, and Max-Age=0 used as an overwrite of a previously stored cookie.

Red, before the fix (3 of the 5 new assertions fail; the other 2 passed beforehand):

cookie expiry
  1) should treat Max-Age=0 as an expired cookie:
     Error: expected [ [ 'foo', { name: 'foo', value: 'bar', expires: ... } ] ] to sort of equal []
  2) should not keep a cookie whose Max-Age cannot be parsed:
     Error: expected [ [ 'foo', ... ] ] to sort of equal []
  3) should not keep a cookie whose Expires date cannot be parsed:
     Error: expected [ [ 'foo', { ..., expires: Invalid Date } ] ] to sort of equal []

Green, after the fix:

$ mocha --require test/support/hooks.js --grep "cookie" test/index.js
  node.js
    √ should send cookies with withCredentials: true
    √ should not send cookies with withCredentials: false
  cookie parsing
    √ should parse a simple set-cookie header
    √ should parse a complex set-cookie header
    √ should parse a weird but valid cookie
  cookie expiry
    √ should treat Max-Age=0 as an expired cookie
    √ should treat a negative Max-Age as an expired cookie
    √ should not keep a cookie whose Max-Age cannot be parsed
    √ should not keep a cookie whose Expires date cannot be parsed
    √ should keep a cookie with a Max-Age in the future
  10 passing

Full Node suite, no regressions:

$ mocha --require test/support/hooks.js test/index.js
  88 passing
  1 pending
  0 failing

Formatting

prettier --check (the repo's pinned ^3.3.2) passes on both changed files.

CookieJar treated a cookie as expired only once the clock had moved strictly past its expiry time, so Max-Age=0 (RFC 6265 section 5.2.2, the canonical deletion request) produced expires=now and the cookie was replayed on the next request. parseInt/Date turning a malformed attribute into NaN/Invalid Date made that cookie outlive every comparison, so it was never removed either. Set the expiry to the earliest representable date for Max-Age <= 0 and for values that cannot be parsed, and make the expiry check inclusive.

Signed-off-by: simpleqt <2015900818@qq.com>
Copilot AI balanced review requested due to automatic review settings November 3, 2026 19:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants