musistudio/claude-code-router · error · Error
Provider payload must be a JSON object.
Error message
Provider payload must be a JSON object.
What it means
readPayloadRecord decodes the payload= deep-link parameter (raw JSON if it starts with '{', otherwise base64url) and requires the decoded value to be a non-null, non-array object. Arrays, strings, numbers, or null after JSON.parse trigger this error.
Source
Thrown at packages/core/src/contracts/deep-link.ts:420
return JSON.parse(paramValue);
} catch {
return paramValue;
}
}
}
return undefined;
}
function readPayloadRecord(params: URLSearchParams): Record<string, unknown> | undefined {
const value = firstStringParam(params, ["payload"]);
if (!value) {
return undefined;
}
const jsonText = value.trim().startsWith("{") ? value : decodeBase64Url(value);
const parsed = JSON.parse(jsonText) as unknown;
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("Provider payload must be a JSON object.");
}
return parsed as Record<string, unknown>;
}
function decodeBase64Url(value: string): string {
const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
const binary = typeof atob === "function"
? atob(padded)
: Buffer.from(padded, "base64").toString("binary");
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
function firstStringParam(params: URLSearchParams, names: string[]): string | undefined {
for (const name of names) {
const value = params.get(name);
if (typeof value === "string" && value.trim()) {
return value.trim();View on GitHub (pinned to 99f24806c6)
Solutions
- Make payload a JSON object: {...} (base64url of an object if not inline)
- Don't wrap in an array or quotes; encode with base64url(JSON.stringify(obj)) only when not starting with '{'
- Test decoding locally: JSON.parse(Buffer.from(payload,'base64url').toString()) before shipping the link
Example fix
// before
ccr://provider/install?payload=W1sibmFtZSJdXQ (decodes to [["name"]])
// after
ccr://provider/install?payload=eyJuYW1lIjoiYWNtZSJ9 (decodes to {"name":"acme"}) Defensive patterns
Strategy: type-guard
Validate before calling
const text = param.startsWith("{") ? param : Buffer.from(param, "base64url").toString(); const v = JSON.parse(text); if (typeof v !== "object" || v === null || Array.isArray(v)) return; Type guard
const isPayloadRecord = (v: unknown): v is Record<string, unknown> => !!v && typeof v === "object" && !Array.isArray(v);
Try / catch
try { parseProviderDeepLinkPayload(url); } catch (e) { if (e instanceof Error && e.message === "Provider payload must be a JSON object.") return showPayloadHelp(); throw e; } Prevention
- Encode payloads as base64url(JSON.stringify(obj))
- Never wrap payload JSON in arrays or extra quotes
When it happens
Trigger: A payload= param whose decoded JSON is an array (e.g. [{...}]) or a scalar ("abc", 42, null), or a base64url blob that decodes to non-object JSON.
Common situations: Encoding a list of providers instead of a single record; double-encoding leaving a quoted JSON string; hand-crafted base64 that decodes to malformed JSON shape.
Related errors
- Chrome login import payload must be an object.
- Provider link is too long.
- Unsupported link protocol.
- Unsupported CCR link target.
- Provider manifest must be a JSON object.
AI-assisted analysis of musistudio/claude-code-router@99f24806c6 (2026-08-27).
Data as JSON: /api/errors/e9142c9a6c422eac.
Report an issue: GitHub.