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

  1. Re-fetch GET /api/models/catalog and use a currently listed id
  2. Treat 404 on delete as success if the goal is 'make it gone' (idempotent delete)
  3. 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

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


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