{"record":{"id":"778f83df53785dbc","repo":"sipeed/picoclaw","slug":"invalid-json-v-778f83","errorCode":null,"errorMessage":"Invalid JSON: %v","messagePattern":"Invalid JSON: (.+?)","errorType":"validation","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"web/backend/api/launcher_config.go","lineNumber":74,"sourceCode":"\tif err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Failed to load launcher config: %v\", err), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tjson.NewEncoder(w).Encode(launcherConfigPayload{\n\t\tPort:                 cfg.Port,\n\t\tPublic:               cfg.Public,\n\t\tAllowedCIDRs:         append([]string(nil), cfg.AllowedCIDRs...),\n\t\tAllowLocalhostBypass: cfg.AllowLocalhostBypass,\n\t\tTrustedProxyCIDRs:    append([]string(nil), cfg.TrustedProxyCIDRs...),\n\t})\n}\n\nfunc (h *Handler) handleUpdateLauncherConfig(w http.ResponseWriter, r *http.Request) {\n\tvar payload launcherConfigUpdatePayload\n\tif err := json.NewDecoder(r.Body).Decode(&payload); err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Invalid JSON: %v\", err), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tcfg, err := h.loadLauncherConfig()\n\tif err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Failed to load launcher config: %v\", err), http.StatusInternalServerError)\n\t\treturn\n\t}\n\tcfg.Port = payload.Port\n\tcfg.Public = payload.Public\n\tcfg.AllowedCIDRs = append([]string(nil), payload.AllowedCIDRs...)\n\tif payload.AllowLocalhostBypass != nil {\n\t\tcfg.AllowLocalhostBypass = *payload.AllowLocalhostBypass\n\t}\n\tcfg.TrustedProxyCIDRs = append([]string(nil), payload.TrustedProxyCIDRs...)\n\tcfg.LegacyLauncherToken = \"\"\n\tif err := launcherconfig.Validate(cfg); err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusBadRequest)","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/web/backend/api/launcher_config.go#L56-L92","documentation":"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.","triggerScenarios":"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.","commonSituations":"Hand-built fetch/curl requests; a client serializing with a template engine instead of JSON.stringify; proxies stripping or re-encoding the body.","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"],"exampleFix":"// before - port as string, cidrs as scalar string\nfetch('/api/system/launcher-config', {method:'PUT', body: '{\"port\":\"18800\",\"allowed_cidrs\":\"127.0.0.0/8\"}'})\n\n// after - correct types\nfetch('/api/system/launcher-config', {\n  method: 'PUT',\n  headers: {'Content-Type': 'application/json'},\n  body: JSON.stringify({port: 18800, public: false, allowed_cidrs: ['127.0.0.0/8'], trusted_proxy_cidrs: []})\n})","handlingStrategy":"validation","validationCode":"const payload = {\n  port: Number(cfg.port) | 0,\n  public: Boolean(cfg.public),\n  allowed_cidrs: (cfg.allowed_cidrs ?? []).map(s => String(s).trim()).filter(Boolean),\n  allow_localhost_bypass: cfg.allow_localhost_bypass ?? undefined,\n  trusted_proxy_cidrs: (cfg.trusted_proxy_cidrs ?? []).map(s => String(s).trim()).filter(Boolean)\n};\nconst body = JSON.stringify(payload);\nJSON.parse(body); // guarantees syntactic validity before the request","typeGuard":"type LauncherConfigUpdate = {\n  port: number;\n  public: boolean;\n  allowed_cidrs: string[];\n  allow_localhost_bypass?: boolean | null;\n  trusted_proxy_cidrs: string[];\n};\nfunction isLauncherConfigUpdate(v: unknown): v is LauncherConfigUpdate {\n  if (typeof v !== 'object' || v === null) return false;\n  const o = v as Record<string, unknown>;\n  if (!Number.isInteger(o.port) || typeof o.public !== 'boolean') return false;\n  for (const k of ['allowed_cidrs', 'trusted_proxy_cidrs'] as const) {\n    if (o[k] !== undefined && (!Array.isArray(o[k]) || o[k].some(x => typeof x !== 'string'))) return false;\n  }\n  if (o.allow_localhost_bypass !== undefined && o.allow_localhost_bypass !== null && typeof o.allow_localhost_bypass !== 'boolean') return false;\n  return true;\n}","tryCatchPattern":"const res = await fetch('/api/system/launcher-config', {method: 'PUT', headers: {'Content-Type': 'application/json'}, body});\nif (res.status === 400 && (await res.text()).startsWith('Invalid JSON')) {\n  throw new Error('client bug: serialized body was not valid JSON - ' + body.slice(0, 80));\n}","preventionTips":["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"],"tags":["json","validation","http","launcher","go"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}