Skip to content

fix(NODE-7892): reject an invalid Date in EJSON.stringify instead of emitting $numberLong NaN - #942

Open
PerryLink wants to merge 1 commit into
mongodb:mainfrom
PerryLink:fix/ejson-invalid-date
Open

PerryLink wants to merge 1 commit into
mongodb:mainfrom
PerryLink:fix/ejson-invalid-date

Conversation

@PerryLink

Copy link
Copy Markdown

EJSON.stringify accepts a Date whose time value is not finite and writes it as a $numberLong that holds the string "NaN". That is not valid Extended JSON, and this library's own parser refuses it, so the two halves of the codec disagree about what can be represented:

EJSON.stringify({ d: new Date(NaN) })   // {"d":{"$date":{"$numberLong":"NaN"}}}

EJSON.parse('{"d":{"$date":{"$numberLong":"NaN"}}}')
// BSONError: $numberLong string "NaN" is in an invalid format

The legacy modes are worse: they reach getISOString, and toISOString() throws a RangeError out of the implementation rather than a BSONError.

Why

serializeValue decides between an ISO string and a numeric timestamp from whether the time value falls in 1970-9999:

const dateNum = value.getTime(),
  inRange = dateNum > -1 && dateNum < 253402300800000;

if (options.legacy) {
  return options.relaxed && inRange
    ? { $date: value.getTime() }
    : { $date: getISOString(value) };
}
return options.relaxed && inRange
  ? { $date: getISOString(value) }
  : { $date: { $numberLong: value.getTime().toString() } };

NaN is not in that range, so an invalid Date takes the numeric branch and the string comes from NaN.toString(). The range check is doing two jobs: choosing a representation, and implicitly deciding whether the date is representable at all.

The change

An invalid Date is rejected before the mode is consulted, so the default, relaxed, relaxed: false, legacy and legacy, relaxed: false paths all report the same thing:

if (!Number.isFinite(dateNum)) {
  throw new BSONError(`Cannot serialize an invalid Date to EJSON: ${value.toString()}`);
}

BSONError is what the parser already throws for the document this used to produce, and what the rest of the repository throws for values it cannot represent.

Tests

Two cases in test/node/extended_json.test.ts, next to the existing date cases, check that all five option combinations reject an invalid Date, and that valid dates still serialize to exactly the strings the neighbouring tests expect:

$ npx mocha test/node -g "date is invalid"
    √ rejects it instead of emitting a $numberLong it cannot parse back
    √ still serializes valid dates unchanged
  2 passing

Without the change the first one fails:

AssertionError: expected [Function] to throw BSONError

Full node suite with the change: 4601 passing, 36 pending, 1 failing. The one failure is Code … when it contains multiline code, which also fails on the unmodified checkout: it compares a fixture containing \r\n against an expected string with \n, so it is a line-ending artifact of my Windows checkout rather than something this change touches. I verified that by stashing the source change and running the case on its own.

Out of scope

BSON.serialize({ d: new Date(NaN) }) does not throw; it writes a date field with a NaN payload that reads back as new Date(0). That is a separate asymmetry in the BSON path, so it is left alone here.

There is no NODE ticket for this. If you would like one, point me at it and I will rename the commit to match.

…umberLong "NaN"

EJSON.stringify accepted a Date whose time value is not finite and wrote it as a $numberLong
holding the string "NaN", which is not valid Extended JSON and which this library's own parser
refuses:

  EJSON.stringify({ d: new Date(NaN) })   // {"d":{"$date":{"$numberLong":"NaN"}}}
  EJSON.parse('{"d":{"$date":{"$numberLong":"NaN"}}}')
  // BSONError: $numberLong string "NaN" is in an invalid format

so the two halves of the codec disagreed about what can be represented. The legacy modes were
worse: they reached toISOString and threw a RangeError out of the implementation.

serializeValue reaches the numeric branch for any date outside 1970-9999, and NaN is outside it,
so the string came from getTime().toString(). Reject a non-finite time value before the mode is
consulted, which covers the default, relaxed and legacy paths in one place.

Two cases in test/node/extended_json.test.ts check that all five option combinations reject an
invalid Date and that valid dates still serialize exactly as the neighbouring tests expect.
@PerryLink
PerryLink requested a review from a team as a code owner October 9, 2026 09:35
@PavelSafronov PavelSafronov changed the title fix: reject an invalid Date in EJSON.stringify instead of emitting $numberLong NaN fix(NODE-7892): reject an invalid Date in EJSON.stringify instead of emitting $numberLong NaN Oct 9, 2026
@PavelSafronov PavelSafronov added tracked-in-jira There is a ticket in Mongo's Jira instance tracking this issue/PR External Submission labels Oct 9, 2026
@PavelSafronov

Copy link
Copy Markdown
Contributor

@PerryLink , thanks for the detailed PR! This is being tracked in NODE-7892 and the team will prioritize it in our next triage session.
No need to rename the commit: we squash-merge, so the only thing that has to be correct is the PR title, and I've already updated it.

This branch has not been deployed

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

Labels

External Submission tracked-in-jira There is a ticket in Mongo's Jira instance tracking this issue/PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants