{"record":{"id":"8c1a9847a8f6cb8c","repo":"tinyhumansai/openhuman","slug":"mascot-manifest-missing-mascots-array","errorCode":null,"errorMessage":"mascot manifest: missing mascots array","messagePattern":"mascot manifest: missing mascots array","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/src/features/human/Mascot/manifest/manifestService.ts","lineNumber":112,"sourceCode":"    (!Array.isArray(se.channels) || !se.channels.every(isManifestChannel))\n  ) {\n    return false;\n  }\n  if (!Array.isArray(m.files) || !m.files.every(isManifestFile)) return false;\n  // A renderable mascot needs a runtime `.riv`; drop entries that only ship a\n  // source `.rev` so callers never select an unplayable asset.\n  return !!runtimeFile(m as MascotManifestEntry);\n}\n\n/**\n * Validate and normalise a parsed manifest document. Throws on a\n * fundamentally malformed payload; silently drops individual malformed\n * mascot entries (a single bad entry should never blank the whole library).\n */\nexport function parseManifest(raw: unknown): MascotManifest {\n  const doc = raw as Partial<MascotManifest> | null;\n  if (!doc || !Array.isArray(doc.mascots)) {\n    throw new Error('mascot manifest: missing mascots array');\n  }\n  const mascots = doc.mascots.filter(isManifestEntry);\n  if (mascots.length === 0) {\n    throw new Error('mascot manifest: no renderable mascots');\n  }\n  return {\n    schemaVersion: typeof doc.schemaVersion === 'number' ? doc.schemaVersion : 1,\n    generatedAt: isNonEmptyString(doc.generatedAt) ? doc.generatedAt : '',\n    mascots,\n    source: {\n      repository: doc.source?.repository ?? '',\n      branch: doc.source?.branch ?? '',\n      commit: doc.source?.commit ?? '',\n    },\n  };\n}\n\nfunction readSnapshot(): MascotManifest | null {","sourceCodeStart":94,"sourceCodeEnd":130,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/app/src/features/human/Mascot/manifest/manifestService.ts#L94-L130","documentation":"parseManifest() validates the fetched mascot library manifest document. It throws 'mascot manifest: missing mascots array' when the parsed JSON is null/undefined or lacks a mascots property that is an Array. This is a hard failure on a fundamentally malformed payload — the fetch succeeded (HTTP 200) but the body does not resemble a manifest at all.","triggerScenarios":"fetchMascotManifest() receives a 200 response whose JSON body is null, an empty object, or has mascots as a non-array (string, object, number). Typical causes: CDN/GitHub Pages returning an HTML error or SPA fallback page that happens to parse as JSON-null, a truncated JSON body, or a manifest generation script emitting {schemaVersion, generatedAt} without the mascots key.","commonSituations":"The manifest URL (MASCOT_MANIFEST_URL) points at a raw.githubusercontent/Pages URL that 404s into an HTML fallback; a CI job regenerated the manifest with a bug dropping the array; an upstream repo restructured and renamed the manifest file; response caching serving a corrupted body.","solutions":["Open MASCOT_MANIFEST_URL in a browser/curl and inspect the body — confirm it is JSON with a top-level \"mascots\": [...] array.","If the body is HTML (repo renamed, Pages fallback), update MASCOT_MANIFEST_URL to the correct manifest location.","If the manifest is genuinely broken upstream, fix the generator to always emit the mascots array; note fetchMascotManifest() falls back to an IndexedDB/localStorage snapshot when available, so clearing the snapshot store makes the failure reproducible.","As a consumer, catch the error and fall back to a bundled default manifest instead of blanking the mascot feature."],"exampleFix":"// before\nconst manifest = parseManifest(await res.json());\n\n// after — hard-fail only on truly wrong shape, else fall back\nlet manifest: MascotManifest;\ntry {\n  manifest = parseManifest(await res.json());\n} catch (err) {\n  manifest = bundledFallbackManifest; // shipped default\n  warnLog('mascot manifest unusable, using bundled fallback', err);\n}","handlingStrategy":"validation","validationCode":"const raw: unknown = await res.json();\nconst looksLikeManifest =\n  !!raw && typeof raw === 'object' && Array.isArray((raw as any).mascots);\nif (!looksLikeManifest) { /* use fallback manifest */ }","typeGuard":"function isManifestDoc(v: unknown): v is { mascots: unknown[] } {\n  return !!v && typeof v === 'object' && Array.isArray((v as { mascots?: unknown }).mascots);\n}","tryCatchPattern":"try {\n  manifest = parseManifest(raw);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith('mascot manifest:')) {\n    manifest = bundledFallback;\n  } else throw err;\n}","preventionTips":["Pin the manifest URL to an immutable revision (tagged commit) rather than a moving branch.","Keep a snapshot/bundled fallback so malformed upstream payloads never blank the feature.","Validate the manifest in CI when it is generated, not only at read time."],"tags":["mascot","manifest","validation","json","cdn"],"backgroundTag":"invalid-json-response","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}