toJSON is the method JSON.stringify invokes automatically, and in FetchHttpClientResponse it is both async and body-consuming:
async toJSON() {
if (!this._res.headers.get("content-type")?.includes("application/json")) return null;
const rawBody = await this._res.text();
The SDK has normally already read that body (return { data: await res.toJSON() }), so any later stringify of an object graph containing the response calls .text() a second time and gets TypeError: Body is unusable: Body has already been read.
Because toJSON is async, that surfaces as a rejected promise JSON.stringify discards — an unhandled rejection, with no useful stack to trace it back from:
TypeError: Body is unusable: Body has already been read
at _Response.text (node:internal/deps/undici/undici:6821)
at FetchHttpClientResponse.toJSON (@workos-inc/node)
at stringify (<anonymous>)
Where this bites
Anything that serializes values reachable from a WorkOS call. We hit it through Next.js 16's dev error overlay, which re-fires on every poll of routes that call WorkOS, so the log fills continuously.
The same shape applies to error reporters. Sentry's normalize() calls toJSON() on any value that has one, inside a try/catch that cannot help here — an async toJSON rejects rather than throwing synchronously, so the catch never runs:
if (valueWithToJSON && typeof valueWithToJSON.toJSON === "function") {
try { const jsonValue = valueWithToJSON.toJSON(); }
catch {}
}
Reproduce
Make any SDK call, then JSON.stringify({ res }) on the resulting FetchHttpClientResponse.
Versions
Reproduced on @workos-inc/node 10.7.0, still present in 10.10.0 (checked against a clean install). Node 24.13.
Suggested fix
Rename the body-consuming method (e.g. parseBody()) so JSON.stringify no longer triggers it, and/or cache the body text on first read so subsequent reads resolve instead of throwing. Either would make the object safe to serialize.
toJSONis the methodJSON.stringifyinvokes automatically, and inFetchHttpClientResponseit is both async and body-consuming:The SDK has normally already read that body (
return { data: await res.toJSON() }), so any later stringify of an object graph containing the response calls.text()a second time and getsTypeError: Body is unusable: Body has already been read.Because
toJSONis async, that surfaces as a rejected promiseJSON.stringifydiscards — an unhandled rejection, with no useful stack to trace it back from:Where this bites
Anything that serializes values reachable from a WorkOS call. We hit it through Next.js 16's dev error overlay, which re-fires on every poll of routes that call WorkOS, so the log fills continuously.
The same shape applies to error reporters. Sentry's
normalize()callstoJSON()on any value that has one, inside atry/catchthat cannot help here — an asynctoJSONrejects rather than throwing synchronously, so thecatchnever runs:Reproduce
Make any SDK call, then
JSON.stringify({ res })on the resultingFetchHttpClientResponse.Versions
Reproduced on
@workos-inc/node10.7.0, still present in 10.10.0 (checked against a clean install). Node 24.13.Suggested fix
Rename the body-consuming method (e.g.
parseBody()) soJSON.stringifyno longer triggers it, and/or cache the body text on first read so subsequent reads resolve instead of throwing. Either would make the object safe to serialize.