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

  1. Re-fetch GET /api/models immediately before the PUT and use fresh indices from that response
  2. Re-fetch and re-submit if you receive the 404 — the error text tells you the valid range (0-N)
  3. Never craft negative indices; guard idx >= 0 client-side
  4. 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

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


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