Repro
console.log(JSON.stringify(new URLSearchParams({}).toString()));
console.log(JSON.stringify(new URLSearchParams({ a: "1", b: "2" }).toString()));
console.log(JSON.stringify(new URLSearchParams("a=1").toString()));
bun main.ts:
perry main.ts:
"[object Object]"
"[object Object]"
"[object Object]"
Every input shape — empty record, populated record, query string — produces "[object Object]" from .toString(). Looks like .toString() is falling through to Object.prototype.toString.
Note: per #561 / the related fetch.rs work, URLSearchParams.set(k, v) and .get(k) work in isolation. The bug is specifically .toString() (and likely the constructor's input parsing — even strings come back as "[object Object]").
Why this matters
Discovered while running @bradenmacdonald/s3-lite-client post #572/#573/#574 fix (#551). The package's buildRequestOptions does:
const queryAsString = typeof options.query === "object"
? new URLSearchParams(options.query).toString().replace("+", "%20")
: options.query;
In a presigned-URL flow, queryAsString becomes "[object Object]" — which then gets injected verbatim into the URL path before AWS SigV4 canonicalization. The resulting presigned URL has a stray query parameter ?[object Object]=&... (visible in the issue's repro).
Beyond s3-lite, URLSearchParams is used by every HTTP client / form-encoder / redirect-URL builder in the JS ecosystem.
Acceptance
The repro at the top prints "" / "a=1&b=2" / "a=1" matching bun. Plus regressions:
- Iterating:
for (const [k, v] of params) yields the entries
params.has(k), params.delete(k), params.append(k, v) all working against the parsed form
URLSearchParams(URLSearchParams) copy-construct
- Special chars:
URLSearchParams({ a: "x y" }).toString() → "a=x+y" (+ for space, not %20)
Refs #551
Repro
bun main.ts:perry main.ts:Every input shape — empty record, populated record, query string — produces
"[object Object]"from.toString(). Looks like.toString()is falling through toObject.prototype.toString.Note: per #561 / the related fetch.rs work,
URLSearchParams.set(k, v)and.get(k)work in isolation. The bug is specifically.toString()(and likely the constructor's input parsing — even strings come back as"[object Object]").Why this matters
Discovered while running
@bradenmacdonald/s3-lite-clientpost #572/#573/#574 fix (#551). The package'sbuildRequestOptionsdoes:In a presigned-URL flow,
queryAsStringbecomes"[object Object]"— which then gets injected verbatim into the URL path before AWS SigV4 canonicalization. The resulting presigned URL has a stray query parameter?[object Object]=&...(visible in the issue's repro).Beyond s3-lite,
URLSearchParamsis used by every HTTP client / form-encoder / redirect-URL builder in the JS ecosystem.Acceptance
The repro at the top prints
""/"a=1&b=2"/"a=1"matching bun. Plus regressions:for (const [k, v] of params)yields the entriesparams.has(k),params.delete(k),params.append(k, v)all working against the parsed formURLSearchParams(URLSearchParams)copy-constructURLSearchParams({ a: "x y" }).toString()→"a=x+y"(+for space, not%20)Refs #551