{"record":{"id":"dcc988ec21e7b227","repo":"sipeed/picoclaw","slug":"invalid-json-v-dcc988","errorCode":null,"errorMessage":"Invalid JSON: %v","messagePattern":"Invalid JSON: (.+?)","errorType":"http","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"web/backend/api/models.go","lineNumber":323,"sourceCode":"// handleAddModel appends a new model configuration entry.\n//\n//\tPOST /api/models\nfunc (h *Handler) handleAddModel(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\ttype custom struct {\n\t\tconfig.ModelConfig\n\t\tAPIKey string `json:\"api_key\"`\n\t}\n\n\tvar mc custom\n\tif err = json.Unmarshal(body, &mc); err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Invalid JSON: %v\", err), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tnormalizeIncomingModelConfig(&mc.ModelConfig)\n\n\tif err = validateIncomingModelConfig(&mc.ModelConfig, nil); err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Validation error: %v\", err), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif mc.APIKey != \"\" {\n\t\tmc.ModelConfig.SetAPIKey(mc.APIKey)\n\t}\n\n\tcfg, err := config.LoadConfig(h.configPath)\n\tif err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Failed to load config: %v\", err), http.StatusInternalServerError)\n\t\treturn","sourceCodeStart":305,"sourceCodeEnd":341,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/web/backend/api/models.go#L305-L341","documentation":"HTTP 400 returned by POST /api/models (handleAddModel) when json.Unmarshal(body, &mc) fails on the first parse into the {ModelConfig + api_key} struct. It means the body is syntactically invalid JSON, or a field has the wrong type (e.g. api_base as a number instead of a string, model_name as a number). Bodies truncated at the 1 MiB LimitReader cap also land here with an 'unexpected end of JSON input' message.","triggerScenarios":"POST /api/models with a body like {\"model_name\": 123}, a payload built with string concatenation instead of JSON.stringify, double-stringified objects ('{\"model_name\":\"gpt\"}'), or an extra_body blob over 1 MiB cut mid-JSON by the LimitReader.","commonSituations":"Hand-written curl commands with quoting mistakes; frontend code that calls JSON.stringify twice; copying payloads from docs that contain smart quotes or comments (JSON has no comments); oversized extra_body/tool schema payloads.","solutions":["Build the body with JSON.stringify(payload) exactly once and pass it as the fetch/curl body","Pipe the payload through 'jq .' to confirm it is valid, complete JSON","Match field types to the ModelConfig schema: model_name/model/provider/api_base/api_key are strings; extra_body is an object","Shrink extra_body below 1 MiB — silently truncated bodies fail with 'unexpected end of JSON input'"],"exampleFix":"// before: manual string concatenation breaks quoting\nconst res = await fetch('/api/models', {\n  method: 'POST',\n  body: '{model_name:\"gpt\",model:\"openai/gpt-4o\"}',\n});\n\n// after: serialize once, set JSON content type\nconst res = await fetch('/api/models', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({ model_name: 'gpt', model: 'openai/gpt-4o', provider: 'openai' }),\n});","handlingStrategy":"validation","validationCode":"const body = JSON.stringify(payload);\ntry { JSON.parse(body); } catch { throw new Error('payload is not valid JSON'); }\nif (new Blob([body]).size >= 1 << 20) throw new Error('payload exceeds 1 MiB limit and will be truncated');","typeGuard":"function isModelPayload(v) {\n  return typeof v === 'object' && v !== null && !Array.isArray(v)\n    && typeof v.model_name === 'string' && typeof v.model === 'string'\n    && (v.api_base === undefined || typeof v.api_base === 'string')\n    && (v.extra_body === undefined || typeof v.extra_body === 'object');\n}","tryCatchPattern":"const res = await fetch('/api/models', { method: 'POST', body });\nif (!res.ok && (await res.text()).startsWith('Invalid JSON')) {\n  throw new Error('payload malformed or >1MiB truncated — re-serialize with JSON.stringify');\n}","preventionTips":["Always build bodies with JSON.stringify and let fetch set Content-Type","Never stringify twice or concatenate JSON by hand","Keep extra_body blobs small; the server truncates silently at 1 MiB","Check field types against GET /api/models output before adding new fields"],"tags":["go","http","json","serialization","bad-request"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}