chenhg5/cc-connect · warning

invalid JSON:

Error message

invalid JSON: 

What it means

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.

Source

Thrown at core/api.go:217

		slog.Error("api server: write JSON failed", "error", err)
	}
}

func (s *APIServer) handleSend(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST only", http.StatusMethodNotAllowed)
		return
	}

	// Attachments travel base64-encoded inside the JSON body (~4/3 expansion)
	// plus the request envelope, so size the reader to fit one max-size
	// attachment with overhead to spare. The previous hard-coded 52 MB cap was
	// smaller than a single 50 MB attachment after base64 encoding and would
	// reject valid sends; deriving it from maxAttachmentBytes keeps the body
	// limit in step with the configured attachment limit.
	var req SendRequest
	if err := json.NewDecoder(io.LimitReader(r.Body, s.sendBodyLimit())).Decode(&req); err != nil {
		http.Error(w, "invalid JSON: "+err.Error(), http.StatusBadRequest)
		return
	}
	if req.Message == "" && strings.TrimSpace(req.TTSText) == "" && len(req.Images) == 0 && len(req.Files) == 0 && len(req.Audios) == 0 && len(req.Videos) == 0 {
		http.Error(w, "message, tts_text, or attachment is required", http.StatusBadRequest)
		return
	}

	s.mu.RLock()
	var engine *Engine
	var ok bool
	if req.Project != "" {
		engine, ok = s.engines[req.Project]
	} else if len(s.engines) == 1 {
		// No project specified and only one engine: use it by default.
		// Do NOT silently fall back when a non-empty project name is unknown —
		// that misroutes the message to the wrong engine. Mirrors the resolve
		// pattern in webhook.go and handleCronAdd.
		for _, e := range s.engines {

View on GitHub (pinned to 4000b2338a)

Solutions

  1. Read the "invalid JSON: ..." suffix — it names the exact byte offset and syntax problem.
  2. Validate the payload: `jq . body.json` must succeed before sending; ensure Content-Type: application/json.
  3. If sending large attachments, check the base64-encoded size stays under the limit derived from maxAttachmentBytes; compress or lower quality if not.
  4. Ensure attachments are base64-encoded strings in the JSON, not raw binary multipart form fields.

Example fix

// before (shell)
curl -X POST http://127.0.0.1:8080/send -d message=hi
// after
curl -X POST http://127.0.0.1:8080/send -H 'Content-Type: application/json' -d '{"message":"hi"}'
Defensive patterns

Strategy: validation

Validate before calling

if err := json.Unmarshal(payload, &map[string]any{}); err != nil {
    return fmt.Errorf("payload is not valid JSON: %w", err)
}
if len(payload) > maxBodyLimit { return errors.New("payload too large") }

Try / catch

if resp.StatusCode == 400 && strings.HasPrefix(body, "invalid JSON:") {
    slog.Error("send rejected", "detail", body) // inspect offset in detail
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06). Data as JSON: /api/errors/bdd2152c931ad749. Report an issue: GitHub.