{"record":{"id":"75fecb5ea120b49d","repo":"sipeed/picoclaw","slug":"invalid-json-v-75fecb","errorCode":null,"errorMessage":"invalid JSON: %v","messagePattern":"invalid JSON: (.+?)","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"web/backend/api/oauth.go","lineNumber":187,"sourceCode":"\t\t\"providers\": providersResp,\n\t})\n}\n\nfunc (h *Handler) handleOAuthLogin(w http.ResponseWriter, r *http.Request) {\n\tbody, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))\n\tif err != nil {\n\t\thttp.Error(w, \"failed to read request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tdefer r.Body.Close()\n\n\tvar req struct {\n\t\tProvider string `json:\"provider\"`\n\t\tMethod   string `json:\"method\"`\n\t\tToken    string `json:\"token\"`\n\t}\n\tif err = json.Unmarshal(body, &req); err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"invalid JSON: %v\", err), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tprovider, err := normalizeOAuthProvider(req.Provider)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tmethod := strings.ToLower(strings.TrimSpace(req.Method))\n\tif !isOAuthMethodSupported(provider, method) {\n\t\thttp.Error(\n\t\t\tw,\n\t\t\tfmt.Sprintf(\"unsupported login method %q for provider %q\", method, provider),\n\t\t\thttp.StatusBadRequest,\n\t\t)\n\t\treturn\n\t}","sourceCodeStart":169,"sourceCodeEnd":205,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/web/backend/api/oauth.go#L169-L205","documentation":"Returned by POST /api/oauth/login when json.Unmarshal of the body into {provider, method, token} fails; %v names the exact JSON defect (syntax, wrong types, trailing data). All three fields are strings; extra fields are ignored, so the failure is always malformed JSON rather than schema mismatch.","triggerScenarios":"POST /api/oauth/login with unquoted keys, {\"method\": 5} (number vs string), double-encoded JSON, or a body like 'provider=openai' (form-encoded without JSON conversion).","commonSituations":"Sending FormData/x-www-form-urlencoded directly; JSON.stringify applied twice; hand-built curl with shell-mangled quotes; token containing an unescaped quote breaking a template-built string.","solutions":["Send JSON.stringify({provider: 'anthropic', method: 'token', token: tsk}) with Content-Type: application/json","Use the %v offset — it points at the offending byte in the body","If migrating from a form client, convert FormData to a plain object before stringify"],"exampleFix":"// before\nbody: new FormData(loginForm) // sends multipart, not JSON\n\n// after\nbody: JSON.stringify({\n  provider: 'anthropic',\n  method: 'token',\n  token: tokenInput.value,\n})","handlingStrategy":"validation","validationCode":"function buildLoginPayload(provider: string, method: string, token: string): string {\n  for (const [k, v] of Object.entries({ provider, method, token })) {\n    if (typeof v !== 'string') throw new Error(`${k} must be a string`);\n  }\n  return JSON.stringify({ provider, method, token });\n}","typeGuard":"function isOAuthLoginBody(v: unknown): v is { provider: string; method: string; token: string } {\n  return typeof v === 'object' && v !== null\n    && typeof (v as any).provider === 'string'\n    && typeof (v as any).method === 'string'\n    && typeof (v as any).token === 'string';\n}","tryCatchPattern":"try {\n  const res = await fetch('/api/oauth/login', {...});\n  if (res.status === 400 && (await res.text()).startsWith('invalid JSON')) {\n    /* %v points at the bad byte — JSON.parse locally to find and fix it */\n  }\n} catch (e) { /* network */ }","preventionTips":["Never send FormData to this endpoint — convert to a plain object first","Apply JSON.stringify exactly once","All three fields are strings; method must be one of browser/device_code/token per provider"],"tags":["http","json","validation","oauth","login"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}