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)
		return

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Build the body with JSON.stringify(payload) exactly once and pass it as the fetch/curl body
  2. Pipe the payload through 'jq .' to confirm it is valid, complete JSON
  3. Match field types to the ModelConfig schema: model_name/model/provider/api_base/api_key are strings; extra_body is an object
  4. 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

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

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/dcc988ec21e7b227. Report an issue: GitHub.