Skip to content

fix: correct isJSONSerializable to handle null without throwing#572

Open
EduardF1 wants to merge 1 commit into
unjs:mainfrom
EduardF1:fix/isJSONSerializable-null
Open

fix: correct isJSONSerializable to handle null without throwing#572
EduardF1 wants to merge 1 commit into
unjs:mainfrom
EduardF1:fix/isJSONSerializable-null

Conversation

@EduardF1
Copy link
Copy Markdown

@EduardF1 EduardF1 commented May 4, 2026

Problem

isJSONSerializable(null) throws a TypeError and incorrectly returns
alse.

Root cause

// src/utils.ts
const t = typeof value;
if (t === "string" || t === "number" || t === "boolean" || t === null) {
  return true;
}
// ...
if (value.buffer) { // <-- throws TypeError when value is null

Two bugs on the same line:

  1. Dead code: ypeof value never returns "null" — it returns "object" for
    ull. So === null can never be rue.
  2. Throws on null: Since the early-return check is never triggered for
    ull, execution continues to
    alue.buffer, which throws TypeError: Cannot read properties of null (reading 'buffer').

Verified:
ull is valid JSON (JSON.stringify(null) → "null"), so the function should return rue for it.

Fix

Replace === null with
alue === null:

// Before
if (t === "string" || t === "number" || t === "boolean" || t === null) {

// After
if (t === "string" || t === "number" || t === "boolean" || value === null) {

This correctly checks the value itself rather than its ypeof result, so
ull hits the early
eturn true before reaching
alue.buffer.

Fixes #571

@magion33
Copy link
Copy Markdown

magion33 commented May 5, 2026

@EduardF1 I'm the creator of the issue and this is exactly what fix I had in mind. LGTM

Note: Another partially related thing is that the body serialization logic of the fetch here

    if (context.options.body && isPayloadMethod(context.options.method)) {
      if (isJSONSerializable(context.options.body)) {
         ...

doesn't allow to have a body of "" (empty string), or 0, or null. All of those are technically valid JSON-serializable values and should be allowed imo.

@EduardF1
Copy link
Copy Markdown
Author

EduardF1 commented May 5, 2026

@magion33 thanks for the LGTM, and good catch on the body-serialization gap. Looking at https://github.com/unjs/ofetch/blob/main/src/fetch.ts#L130:

if (context.options.body && isPayloadMethod(context.options.method)) {

The leading context.options.body && is the part that drops "", 0, and null before isJSONSerializable is even consulted. Replacing it with an explicit `!== undefined` check (and handling those primitives in the stringify branch) is the right fix, but it's a separate concern from #571 — different file, different surface area, different test coverage.

I'd prefer to keep this PR narrowly focused on the dead-code/throws-on-null bug so it's easy for maintainers to review and merge, and then land the body-serialization fix as a follow-up that references this one. Happy to open that follow-up PR right after this merges (or in parallel if a maintainer signals it's fine to bundle).

@magion33
Copy link
Copy Markdown

magion33 commented May 5, 2026

@EduardF1 Yes, I agree. It's a different topic, just slightly related.

@EduardF1
Copy link
Copy Markdown
Author

EduardF1 commented May 8, 2026

Friendly nudge: the issue creator (@magion33) confirmed the fix matches what they had in mind. Diff is two lines in utils.ts (drop the unreachable === null branch, return false on null) plus a unit test for the throwing-on-null path. cc @pi0 if you have a moment.

@EduardF1
Copy link
Copy Markdown
Author

EduardF1 commented May 15, 2026

Checking in — @magion33 (issue reporter) confirmed the fix. Note that duplicate PRs #577 and #578 targeting the same #571 issue were opened and closed this week; this PR (#572) is the original, has test coverage, and keeps the scope narrow. @pi0 would appreciate a final sign-off when you have a moment!

@EduardF1
Copy link
Copy Markdown
Author

Friendly week-21 nudge. Just checking if there is anything I can adjust on this one. The change is two lines in utils.ts so isJSONSerializable(null) returns false instead of throwing, plus a unit test for the throwing-on-null path. @magion33 (the issue reporter on #571) already confirmed the fix matches their intent. Cc @pi0 whenever you have a moment to take a look. Thanks!

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.

isJSONSerializable bug

2 participants