{"record":{"id":"150f35805076acff","repo":"sipeed/picoclaw","slug":"index-d-out-of-range-0-d","errorCode":null,"errorMessage":"Index %d out of range (0-%d)","messagePattern":"Index (.+?) out of range \\(0-(.+?)\\)","errorType":"http","errorClass":null,"httpStatus":404,"severity":"warning","filePath":"web/backend/api/models.go","lineNumber":403,"sourceCode":"\ttype custom struct {\n\t\tconfig.ModelConfig\n\t\tAPIKey string `json:\"api_key\"`\n\t}\n\n\tvar mc custom\n\tif err = json.Unmarshal(body, &mc); err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Invalid JSON: %v\", err), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tcfg, err := config.LoadConfig(h.configPath)\n\tif err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Failed to load config: %v\", err), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif idx < 0 || idx >= len(cfg.ModelList) {\n\t\thttp.Error(w, fmt.Sprintf(\"Index %d out of range (0-%d)\", idx, len(cfg.ModelList)-1), http.StatusNotFound)\n\t\treturn\n\t}\n\n\t// Preserve the existing API key when the caller omits it (empty string).\n\t// This lets the UI update api_base / proxy without clearing the stored secret.\n\tif mc.APIKey == \"\" {\n\t\tmc.ModelConfig.SetAPIKey(cfg.ModelList[idx].APIKey())\n\t} else {\n\t\tmc.ModelConfig.SetAPIKey(mc.APIKey)\n\t}\n\t// Preserve existing ExtraBody when omitted (nil), but clear it when\n\t// the frontend sends an empty object {} to indicate the field should\n\t// be removed.\n\tif mc.ExtraBody == nil {\n\t\tmc.ExtraBody = cfg.ModelList[idx].ExtraBody\n\t} else if len(mc.ExtraBody) == 0 {\n\t\tmc.ExtraBody = nil\n\t}","sourceCodeStart":385,"sourceCodeEnd":421,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/web/backend/api/models.go#L385-L421","documentation":"HTTP 404 returned by PUT /api/models/{index} (handleUpdateModel) when the index parses as an integer but falls outside cfg.ModelList (idx < 0 or idx >= len). Because entries are addressed by list position, any client holding a stale listing (deletion, reordering, or config edited elsewhere since the GET) will address a slot that no longer exists.","triggerScenarios":"PUT /api/models/5 when the list has 3 entries; PUT /api/models/-1 (negative parses via Atoi and only fails the range check); PUT to an index that was deleted from another tab or by the CLI while the current page was open.","commonSituations":"Two UI tabs open, one deletes a model and shifts positions, the other still PUTs the old index; user bookmarks a deep-link with a hardcoded index; race between CLI edits and the web UI.","solutions":["Re-fetch GET /api/models immediately before the PUT and use fresh indices from that response","Re-fetch and re-submit if you receive the 404 — the error text tells you the valid range (0-N)","Never craft negative indices; guard idx >= 0 client-side","Keep a single editor open, or refetch after any out-of-band change to the model list"],"exampleFix":"// before: stale index captured at page load\nawait fetch(`/api/models/${row.index}`, { method: 'PUT', body });\n\n// after: refresh the list, then submit within bounds\nconst models = await (await fetch('/api/models')).json();\nif (row.index >= models.data.total) throw new Error(`index ${row.index} no longer exists`);\nawait fetch(`/api/models/${row.index}`, { method: 'PUT', body });","handlingStrategy":"validation","validationCode":"const list = await (await fetch('/api/models')).json();\nconst total = list.data.total;\nif (!(Number.isInteger(idx) && idx >= 0 && idx < total)) {\n  throw new Error(`index ${idx} out of range (0-${Math.max(total - 1, 0)})`);\n}","typeGuard":"function isValidModelIndex(idx, total) {\n  return Number.isInteger(idx) && idx >= 0 && idx < total;\n}","tryCatchPattern":"const res = await fetch(`/api/models/${idx}`, { method: 'PUT', body });\nif (res.status === 404 && (await res.text()).includes('out of range')) {\n  // list shifted under us: refetch, re-resolve the row by model_name, re-submit\n  await refreshModels();\n}","preventionTips":["Re-fetch the model list immediately before any PUT","Track rows by model_name so you can re-resolve the index after a 404","Use a single editor session; multiple tabs mutate indices concurrently","Reject negative indices in the UI before requests are issued"],"tags":["go","http","indexing","stale-state","not-found"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}