{"record":{"id":"f1ad53cf7b3b4c92","repo":"tinyhumansai/openhuman","slug":"mascot-id-is-empty","errorCode":null,"errorMessage":"mascot id is empty","messagePattern":"mascot id is empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"app/src/services/mascotService.ts","lineNumber":24,"sourceCode":"import type {\n  GetMascotResponse,\n  ListMascotsResponse,\n  MascotDetailUnion,\n  MascotSummary,\n  RiveMascotDetail,\n} from '../features/human/Mascot/backend/types';\nimport { loadRivBuffer } from '../features/human/Mascot/rivCache';\nimport { apiClient } from './apiClient';\nimport { getBackendUrl } from './backendUrl';\n\nexport async function fetchMascotList(): Promise<MascotSummary[]> {\n  const res = await apiClient.get<ListMascotsResponse>('/mascots', { requireAuth: false });\n  return res.data.mascots;\n}\n\nexport async function fetchMascotDetail(id: string): Promise<MascotDetailUnion> {\n  const safe = encodeURIComponent(id.trim());\n  if (!safe) throw new Error('mascot id is empty');\n  const res = await apiClient.get<GetMascotResponse>(`/mascots/${safe}`, { requireAuth: false });\n  return res.data.mascot;\n}\n\n/**\n * Resolve a Rive mascot's binary, version-cached in IndexedDB. The backend\n * stamps `version` into both the manifest and the `rivFileUrl` (`?v=`), so the\n * binary is only re-downloaded when that version changes.\n */\nexport async function loadMascotRivBuffer(detail: RiveMascotDetail): Promise<ArrayBuffer> {\n  const base = await getBackendUrl();\n  // rivFileUrl is backend-relative (e.g. \"/mascots/toshi/riv?v=1.0.0\").\n  const url = `${base}${detail.rivFileUrl}`;\n  return loadRivBuffer(detail.id, detail.version, url);\n}\n\n/**\n * Lightweight in-memory cache for manifest fetches. Manifests carry the","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/app/src/services/mascotService.ts#L6-L42","documentation":"Deliberate input validation in `fetchMascotDetail`: the id is trimmed and URI-encoded, and because `encodeURIComponent('')` is `''`, the guard fires exactly when the caller passes an empty or whitespace-only string. It throws before any network call, so it can never be caused by the backend. Any non-blank id encodes to a non-empty string and proceeds to `GET /mascots/<id>`.","triggerScenarios":"Calling `fetchMascotDetail('')` or `fetchMascotDetail('   ')` — e.g. a mascot-detail modal opened before a list row is selected, a route param defaulting to blank, or a config-driven default mascot id that is unset.","commonSituations":"UI state races where the selected-mascot id is still empty when the detail panel mounts; a settings field meant to hold a default mascot id left blank; tests calling the service directly with placeholder ids.","solutions":["Guard the call site: skip the fetch when `id.trim()` is empty and render a placeholder instead","Fix the source of the blank id (route param, selection state) rather than swallowing the error","If a default mascot is expected, fall back to a known id from `fetchMascotList()`"],"exampleFix":"// before\nconst detail = await fetchMascotDetail(selectedId);\n\n// after\nconst detail = selectedId.trim()\n  ? await fetchMascotDetail(selectedId)\n  : null; // render 'select a mascot' empty state","handlingStrategy":"validation","validationCode":"const id = raw ?? '';\nif (!id.trim()) {\n  // render 'select a mascot' instead of calling the API\n  return null;\n}\nconst detail = await fetchMascotDetail(id);","typeGuard":"function isNonBlankId(id: unknown): id is string {\n  return typeof id === 'string' && id.trim().length > 0;\n}","tryCatchPattern":"try { const d = await fetchMascotDetail(id); }\ncatch (e) {\n  if (e instanceof Error && e.message === 'mascot id is empty') return null;\n  throw e;\n}","preventionTips":["Gate UI actions on a non-blank selection before invoking detail fetches","Default mascot ids in config must be validated as non-blank at load time","Treat this message as a caller bug, not a backend error — never retry it"],"tags":["validation","mascot","api-client"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}