sipeed/picoclaw · warning
Invalid JSON: %v
Error message
Invalid JSON: %v
What it means
Returned by PUT /api/system/launcher-config when json.NewDecoder(r.Body).Decode of the update payload fails. The body must be a single JSON object with the expected types: port a number, public a boolean, allowed_cidrs/trusted_proxy_cidrs arrays of strings, allow_localhost_bypass a boolean or null. Unlike the config test endpoint there is no explicit size cap, and the decoder reads only the first JSON value, so trailing garbage after the object is ignored rather than erroring.
Source
Thrown at web/backend/api/launcher_config.go:74
if err != nil {
http.Error(w, fmt.Sprintf("Failed to load launcher config: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(launcherConfigPayload{
Port: cfg.Port,
Public: cfg.Public,
AllowedCIDRs: append([]string(nil), cfg.AllowedCIDRs...),
AllowLocalhostBypass: cfg.AllowLocalhostBypass,
TrustedProxyCIDRs: append([]string(nil), cfg.TrustedProxyCIDRs...),
})
}
func (h *Handler) handleUpdateLauncherConfig(w http.ResponseWriter, r *http.Request) {
var payload launcherConfigUpdatePayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
return
}
cfg, err := h.loadLauncherConfig()
if err != nil {
http.Error(w, fmt.Sprintf("Failed to load launcher config: %v", err), http.StatusInternalServerError)
return
}
cfg.Port = payload.Port
cfg.Public = payload.Public
cfg.AllowedCIDRs = append([]string(nil), payload.AllowedCIDRs...)
if payload.AllowLocalhostBypass != nil {
cfg.AllowLocalhostBypass = *payload.AllowLocalhostBypass
}
cfg.TrustedProxyCIDRs = append([]string(nil), payload.TrustedProxyCIDRs...)
cfg.LegacyLauncherToken = ""
if err := launcherconfig.Validate(cfg); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)View on GitHub (pinned to 49183d7e8d)
Solutions
- Send a well-formed object, e.g. {"port":18800,"public":false,"allowed_cidrs":["127.0.0.0/8"],"allow_localhost_bypass":true,"trusted_proxy_cidrs":[]}
- Use JSON.stringify client-side so syntax is always valid
- Match types exactly: numbers for port, booleans for public/allow_localhost_bypass, string arrays for CIDR lists
- Check the wrapped decoder error for the offending byte offset if the body is large
Example fix
// before - port as string, cidrs as scalar string
fetch('/api/system/launcher-config', {method:'PUT', body: '{"port":"18800","allowed_cidrs":"127.0.0.0/8"}'})
// after - correct types
fetch('/api/system/launcher-config', {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({port: 18800, public: false, allowed_cidrs: ['127.0.0.0/8'], trusted_proxy_cidrs: []})
}) Defensive patterns
Strategy: validation
Validate before calling
const payload = {
port: Number(cfg.port) | 0,
public: Boolean(cfg.public),
allowed_cidrs: (cfg.allowed_cidrs ?? []).map(s => String(s).trim()).filter(Boolean),
allow_localhost_bypass: cfg.allow_localhost_bypass ?? undefined,
trusted_proxy_cidrs: (cfg.trusted_proxy_cidrs ?? []).map(s => String(s).trim()).filter(Boolean)
};
const body = JSON.stringify(payload);
JSON.parse(body); // guarantees syntactic validity before the request Type guard
type LauncherConfigUpdate = {
port: number;
public: boolean;
allowed_cidrs: string[];
allow_localhost_bypass?: boolean | null;
trusted_proxy_cidrs: string[];
};
function isLauncherConfigUpdate(v: unknown): v is LauncherConfigUpdate {
if (typeof v !== 'object' || v === null) return false;
const o = v as Record<string, unknown>;
if (!Number.isInteger(o.port) || typeof o.public !== 'boolean') return false;
for (const k of ['allowed_cidrs', 'trusted_proxy_cidrs'] as const) {
if (o[k] !== undefined && (!Array.isArray(o[k]) || o[k].some(x => typeof x !== 'string'))) return false;
}
if (o.allow_localhost_bypass !== undefined && o.allow_localhost_bypass !== null && typeof o.allow_localhost_bypass !== 'boolean') return false;
return true;
} Try / catch
const res = await fetch('/api/system/launcher-config', {method: 'PUT', headers: {'Content-Type': 'application/json'}, body});
if (res.status === 400 && (await res.text()).startsWith('Invalid JSON')) {
throw new Error('client bug: serialized body was not valid JSON - ' + body.slice(0, 80));
} Prevention
- Always JSON.stringify the payload; never template it by hand
- Coerce types (Number/Boolean/Array) before sending - the Go decoder is strict
- Send Content-Type: application/json
When it happens
Trigger: Malformed JSON body (trailing comma, single quotes, unquoted keys); port sent as string "18800"; allowed_cidrs sent as a comma-separated string instead of an array; empty request body.
Common situations: Hand-built fetch/curl requests; a client serializing with a template engine instead of JSON.stringify; proxies stripping or re-encoding the body.
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 load launcher config: %v
- port %d is out of range (1-65535)
- build request: %w
- invalid CIDR %q
- invalid trusted proxy CIDR %q
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/778f83df53785dbc.
Report an issue: GitHub.