sipeed/picoclaw · warning
Index %d out of range (0-%d)
Error message
Index %d out of range (0-%d)
What it means
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.
Source
Thrown at web/backend/api/models.go:403
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
}
cfg, err := config.LoadConfig(h.configPath)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError)
return
}
if idx < 0 || idx >= len(cfg.ModelList) {
http.Error(w, fmt.Sprintf("Index %d out of range (0-%d)", idx, len(cfg.ModelList)-1), http.StatusNotFound)
return
}
// Preserve the existing API key when the caller omits it (empty string).
// This lets the UI update api_base / proxy without clearing the stored secret.
if mc.APIKey == "" {
mc.ModelConfig.SetAPIKey(cfg.ModelList[idx].APIKey())
} else {
mc.ModelConfig.SetAPIKey(mc.APIKey)
}
// Preserve existing ExtraBody when omitted (nil), but clear it when
// the frontend sends an empty object {} to indicate the field should
// be removed.
if mc.ExtraBody == nil {
mc.ExtraBody = cfg.ModelList[idx].ExtraBody
} else if len(mc.ExtraBody) == 0 {
mc.ExtraBody = nil
}View on GitHub (pinned to 49183d7e8d)
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
Example fix
// before: stale index captured at page load
await fetch(`/api/models/${row.index}`, { method: 'PUT', body });
// after: refresh the list, then submit within bounds
const models = await (await fetch('/api/models')).json();
if (row.index >= models.data.total) throw new Error(`index ${row.index} no longer exists`);
await fetch(`/api/models/${row.index}`, { method: 'PUT', body }); Defensive patterns
Strategy: validation
Validate before calling
const list = await (await fetch('/api/models')).json();
const total = list.data.total;
if (!(Number.isInteger(idx) && idx >= 0 && idx < total)) {
throw new Error(`index ${idx} out of range (0-${Math.max(total - 1, 0)})`);
} Type guard
function isValidModelIndex(idx, total) {
return Number.isInteger(idx) && idx >= 0 && idx < total;
} Try / catch
const res = await fetch(`/api/models/${idx}`, { method: 'PUT', body });
if (res.status === 404 && (await res.text()).includes('out of range')) {
// list shifted under us: refetch, re-resolve the row by model_name, re-submit
await refreshModels();
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- catalog not found
- create request: %w
- API error %d: %s
- failed to get WeCom QR code: %w
- unexpected status %s
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/150f35805076acff.
Report an issue: GitHub.