sipeed/picoclaw · warning
Invalid JSON: %v
Error message
Invalid JSON: %v
What it means
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.
Source
Thrown at web/backend/api/models.go:323
// handleAddModel appends a new model configuration entry.
//
// POST /api/models
func (h *Handler) handleAddModel(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
if err != nil {
http.Error(w, "Failed to read request body", http.StatusBadRequest)
return
}
defer r.Body.Close()
type custom struct {
config.ModelConfig
APIKey string `json:"api_key"`
}
var mc custom
if err = json.Unmarshal(body, &mc); err != nil {
http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
return
}
normalizeIncomingModelConfig(&mc.ModelConfig)
if err = validateIncomingModelConfig(&mc.ModelConfig, nil); err != nil {
http.Error(w, fmt.Sprintf("Validation error: %v", err), http.StatusBadRequest)
return
}
if mc.APIKey != "" {
mc.ModelConfig.SetAPIKey(mc.APIKey)
}
cfg, err := config.LoadConfig(h.configPath)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError)
returnView on GitHub (pinned to 49183d7e8d)
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'
Example fix
// before: manual string concatenation breaks quoting
const res = await fetch('/api/models', {
method: 'POST',
body: '{model_name:"gpt",model:"openai/gpt-4o"}',
});
// after: serialize once, set JSON content type
const res = await fetch('/api/models', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model_name: 'gpt', model: 'openai/gpt-4o', provider: 'openai' }),
}); Defensive patterns
Strategy: validation
Validate before calling
const body = JSON.stringify(payload);
try { JSON.parse(body); } catch { throw new Error('payload is not valid JSON'); }
if (new Blob([body]).size >= 1 << 20) throw new Error('payload exceeds 1 MiB limit and will be truncated'); Type guard
function isModelPayload(v) {
return typeof v === 'object' && v !== null && !Array.isArray(v)
&& typeof v.model_name === 'string' && typeof v.model === 'string'
&& (v.api_base === undefined || typeof v.api_base === 'string')
&& (v.extra_body === undefined || typeof v.extra_body === 'object');
} Try / catch
const res = await fetch('/api/models', { method: 'POST', body });
if (!res.ok && (await res.text()).startsWith('Invalid JSON')) {
throw new Error('payload malformed or >1MiB truncated — re-serialize with JSON.stringify');
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- failed to serialize config: %w
- Invalid JSON: %v
- Failed to load config: %v
- Validation error: %v
- Invalid index
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/dcc988ec21e7b227.
Report an issue: GitHub.