{"record":{"id":"bdd2152c931ad749","repo":"chenhg5/cc-connect","slug":"invalid-json","errorCode":null,"errorMessage":"invalid JSON: ","messagePattern":"invalid JSON: ","errorType":"validation","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"core/api.go","lineNumber":217,"sourceCode":"\t\tslog.Error(\"api server: write JSON failed\", \"error\", err)\n\t}\n}\n\nfunc (s *APIServer) handleSend(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != http.MethodPost {\n\t\thttp.Error(w, \"POST only\", http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\n\t// Attachments travel base64-encoded inside the JSON body (~4/3 expansion)\n\t// plus the request envelope, so size the reader to fit one max-size\n\t// attachment with overhead to spare. The previous hard-coded 52 MB cap was\n\t// smaller than a single 50 MB attachment after base64 encoding and would\n\t// reject valid sends; deriving it from maxAttachmentBytes keeps the body\n\t// limit in step with the configured attachment limit.\n\tvar req SendRequest\n\tif err := json.NewDecoder(io.LimitReader(r.Body, s.sendBodyLimit())).Decode(&req); err != nil {\n\t\thttp.Error(w, \"invalid JSON: \"+err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\tif req.Message == \"\" && strings.TrimSpace(req.TTSText) == \"\" && len(req.Images) == 0 && len(req.Files) == 0 && len(req.Audios) == 0 && len(req.Videos) == 0 {\n\t\thttp.Error(w, \"message, tts_text, or attachment is required\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\ts.mu.RLock()\n\tvar engine *Engine\n\tvar ok bool\n\tif req.Project != \"\" {\n\t\tengine, ok = s.engines[req.Project]\n\t} else if len(s.engines) == 1 {\n\t\t// No project specified and only one engine: use it by default.\n\t\t// Do NOT silently fall back when a non-empty project name is unknown —\n\t\t// that misroutes the message to the wrong engine. Mirrors the resolve\n\t\t// pattern in webhook.go and handleCronAdd.\n\t\tfor _, e := range s.engines {","sourceCodeStart":199,"sourceCodeEnd":235,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/core/api.go#L199-L235","documentation":"The /send endpoint failed to decode the request body into SendRequest JSON. The raw decode error is appended after the prefix \"invalid JSON: \" and returned with 400. The body is read through io.LimitReader sized by sendBodyLimit(), so an over-large body can also produce a truncated/failed decode rather than a clean size error.","triggerScenarios":"POSTing to /send with a malformed JSON body, wrong Content-Type payload (form-encoded instead of JSON), an empty body, or a body exceeding sendBodyLimit() so the LimitReader truncates it mid-object.","commonSituations":"curl -d with unquoted/unescaped JSON on the shell, sending form data or plain text instead of application/json, attaching a file whose base64 form pushes the body past the configured attachment-derived limit, trailing commas in hand-written JSON.","solutions":["Read the \"invalid JSON: ...\" suffix — it names the exact byte offset and syntax problem.","Validate the payload: `jq . body.json` must succeed before sending; ensure Content-Type: application/json.","If sending large attachments, check the base64-encoded size stays under the limit derived from maxAttachmentBytes; compress or lower quality if not.","Ensure attachments are base64-encoded strings in the JSON, not raw binary multipart form fields."],"exampleFix":"// before (shell)\ncurl -X POST http://127.0.0.1:8080/send -d message=hi\n// after\ncurl -X POST http://127.0.0.1:8080/send -H 'Content-Type: application/json' -d '{\"message\":\"hi\"}'","handlingStrategy":"validation","validationCode":"if err := json.Unmarshal(payload, &map[string]any{}); err != nil {\n    return fmt.Errorf(\"payload is not valid JSON: %w\", err)\n}\nif len(payload) > maxBodyLimit { return errors.New(\"payload too large\") }","typeGuard":null,"tryCatchPattern":"if resp.StatusCode == 400 && strings.HasPrefix(body, \"invalid JSON:\") {\n    slog.Error(\"send rejected\", \"detail\", body) // inspect offset in detail\n}","preventionTips":["Always send Content-Type: application/json with a jq-validated body","Base64-encode attachments and keep the encoded body under the configured limit","Never post form-encoded or plain-text bodies to /send"],"tags":["http","json","api","validation"],"backgroundTag":"json-decode-failed","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}