{"record":{"id":"e9142c9a6c422eac","repo":"musistudio/claude-code-router","slug":"provider-payload-must-be-a-json-object","errorCode":null,"errorMessage":"Provider payload must be a JSON object.","messagePattern":"Provider payload must be a JSON object\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/core/src/contracts/deep-link.ts","lineNumber":420,"sourceCode":"        return JSON.parse(paramValue);\n      } catch {\n        return paramValue;\n      }\n    }\n  }\n  return undefined;\n}\n\nfunction readPayloadRecord(params: URLSearchParams): Record<string, unknown> | undefined {\n  const value = firstStringParam(params, [\"payload\"]);\n  if (!value) {\n    return undefined;\n  }\n\n  const jsonText = value.trim().startsWith(\"{\") ? value : decodeBase64Url(value);\n  const parsed = JSON.parse(jsonText) as unknown;\n  if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n    throw new Error(\"Provider payload must be a JSON object.\");\n  }\n  return parsed as Record<string, unknown>;\n}\n\nfunction decodeBase64Url(value: string): string {\n  const padded = value.replace(/-/g, \"+\").replace(/_/g, \"/\").padEnd(Math.ceil(value.length / 4) * 4, \"=\");\n  const binary = typeof atob === \"function\"\n    ? atob(padded)\n    : Buffer.from(padded, \"base64\").toString(\"binary\");\n  const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));\n  return new TextDecoder().decode(bytes);\n}\n\nfunction firstStringParam(params: URLSearchParams, names: string[]): string | undefined {\n  for (const name of names) {\n    const value = params.get(name);\n    if (typeof value === \"string\" && value.trim()) {\n      return value.trim();","sourceCodeStart":402,"sourceCodeEnd":438,"githubUrl":"https://github.com/musistudio/claude-code-router/blob/99f24806c6a2c660b16e53e95211c517448a6c90/packages/core/src/contracts/deep-link.ts#L402-L438","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nccr://provider/install?payload=W1sibmFtZSJdXQ (decodes to [[\"name\"]])\n// after\nccr://provider/install?payload=eyJuYW1lIjoiYWNtZSJ9 (decodes to {\"name\":\"acme\"})","handlingStrategy":"type-guard","validationCode":"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;","typeGuard":"const isPayloadRecord = (v: unknown): v is Record<string, unknown> =>\n  !!v && typeof v === \"object\" && !Array.isArray(v);","tryCatchPattern":"try { parseProviderDeepLinkPayload(url); } catch (e) { if (e instanceof Error && e.message === \"Provider payload must be a JSON object.\") return showPayloadHelp(); throw e; }","preventionTips":["Encode payloads as base64url(JSON.stringify(obj))","Never wrap payload JSON in arrays or extra quotes"],"tags":["deep-link","payload","json","validation"],"backgroundTag":"payload-decode-invalid-json","analyzedSha":"99f24806c6a2c660b16e53e95211c517448a6c90","analyzedAt":"2026-08-27T04:11:01.184Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}