{"record":{"id":"e98694fef79def51","repo":"sipeed/picoclaw","slug":"catalog-not-found","errorCode":null,"errorMessage":"catalog not found","messagePattern":"catalog not found","errorType":"http","errorClass":null,"httpStatus":404,"severity":"info","filePath":"web/backend/api/model_catalog.go","lineNumber":159,"sourceCode":"\n// handleDeleteCatalog deletes a saved model catalog by ID.\n//\n//\tDELETE /api/models/catalog/{id}\nfunc (h *Handler) handleDeleteCatalog(w http.ResponseWriter, r *http.Request) {\n\tid := r.PathValue(\"id\")\n\tif id == \"\" {\n\t\thttp.Error(w, \"id is required\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tstore, err := loadCatalogs()\n\tif err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Failed to load catalogs: %v\", err), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif _, ok := store.Entries[id]; !ok {\n\t\thttp.Error(w, \"catalog not found\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\tdelete(store.Entries, id)\n\tif err := saveCatalogs(store); err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Failed to save catalogs: %v\", err), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tjson.NewEncoder(w).Encode(map[string]string{\"status\": \"ok\"})\n}\n","sourceCodeStart":141,"sourceCodeEnd":172,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/web/backend/api/model_catalog.go#L141-L172","documentation":"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.","triggerScenarios":"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.","commonSituations":"Stale SPA state after credential rotation; double-clicked delete buttons where the first request wins; bookmarks to individual catalog ids.","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"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Verify the id still exists immediately before deleting (guards stale UI state).\nconst {entries} = await (await fetch('/api/models/catalog')).json();\nconst target = entries.find(e => e.id === id);\nif (!target) {\n  return {deleted: true, alreadyGone: true};   // idempotent: goal state reached\n}\nawait fetch(`/api/models/catalog/${encodeURIComponent(id)}`, {method: 'DELETE'});","typeGuard":"type CatalogEntry = {id: string; provider: string; api_base: string; models: unknown[]; fetched_at: string};\nconst isCatalogEntryList = (v: unknown): v is CatalogEntry[] =>\n  Array.isArray(v) && v.every(e =>\n    typeof e === 'object' && e !== null && typeof (e as CatalogEntry).id === 'string');","tryCatchPattern":"const res = await fetch(`/api/models/catalog/${encodeURIComponent(id)}`, {method: 'DELETE'});\nif (res.status === 404) {\n  const text = await res.text();\n  if (text.includes('catalog not found')) return;   // treat as success (idempotent delete)\n  throw new Error(text);\n}","preventionTips":["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"],"tags":["http","not-found","model-catalog","idempotency","go"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}