sipeed/picoclaw · info
catalog not found
Error message
catalog not found
What it means
404 from DELETE /api/models/catalog/{id}: the id is not a key in store.Entries. IDs are deterministic keys produced by generateCatalogKey (model_catalog.go:45-50) in the form "provider|apiBase|first-6-hex-of-sha256(apiKey)". A stale id (entry already deleted in another tab/session) or a changed API key (new hash, new key) produces this. Normal REST semantics - the resource simply is not there.
Source
Thrown at web/backend/api/model_catalog.go:159
// handleDeleteCatalog deletes a saved model catalog by ID.
//
// DELETE /api/models/catalog/{id}
func (h *Handler) handleDeleteCatalog(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
http.Error(w, "id is required", http.StatusBadRequest)
return
}
store, err := loadCatalogs()
if err != nil {
http.Error(w, fmt.Sprintf("Failed to load catalogs: %v", err), http.StatusInternalServerError)
return
}
if _, ok := store.Entries[id]; !ok {
http.Error(w, "catalog not found", http.StatusNotFound)
return
}
delete(store.Entries, id)
if err := saveCatalogs(store); err != nil {
http.Error(w, fmt.Sprintf("Failed to save catalogs: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
View on GitHub (pinned to 49183d7e8d)
Solutions
- Re-fetch GET /api/models/catalog and use a currently listed id
- Treat 404 on delete as success if the goal is 'make it gone' (idempotent delete)
- After rotating a provider API key, expect old catalog ids to orphan - delete them or ignore them
Defensive patterns
Strategy: validation
Validate before calling
// Verify the id still exists immediately before deleting (guards stale UI state).
const {entries} = await (await fetch('/api/models/catalog')).json();
const target = entries.find(e => e.id === id);
if (!target) {
return {deleted: true, alreadyGone: true}; // idempotent: goal state reached
}
await fetch(`/api/models/catalog/${encodeURIComponent(id)}`, {method: 'DELETE'}); Type guard
type CatalogEntry = {id: string; provider: string; api_base: string; models: unknown[]; fetched_at: string};
const isCatalogEntryList = (v: unknown): v is CatalogEntry[] =>
Array.isArray(v) && v.every(e =>
typeof e === 'object' && e !== null && typeof (e as CatalogEntry).id === 'string'); Try / catch
const res = await fetch(`/api/models/catalog/${encodeURIComponent(id)}`, {method: 'DELETE'});
if (res.status === 404) {
const text = await res.text();
if (text.includes('catalog not found')) return; // treat as success (idempotent delete)
throw new Error(text);
} Prevention
- Treat 404 on DELETE as success in UIs - the catalog is gone either way
- Refresh the list after credential rotation: ids hash the API key, so rotated keys orphan old catalogs
- Disable delete buttons for entries missing from the latest GET
When it happens
Trigger: Two UI sessions listing catalogs, one deletes first, the second's delete gets 404; the provider API key changed so re-fetched catalogs stored under a new key; id typo'd or hand-constructed instead of taken from the list endpoint.
Common situations: Stale SPA state after credential rotation; double-clicked delete buttons where the first request wins; bookmarks to individual catalog ids.
Related errors
- id is required
- Index %d out of range (0-%d)
- create request: %w
- API error %d: %s
- failed to get WeCom QR code: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/e98694fef79def51.
Report an issue: GitHub.