tinyhumansai/openhuman · warning
mascot id is empty
Error message
mascot id is empty
What it means
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>`.
Source
Thrown at app/src/services/mascotService.ts:24
import type {
GetMascotResponse,
ListMascotsResponse,
MascotDetailUnion,
MascotSummary,
RiveMascotDetail,
} from '../features/human/Mascot/backend/types';
import { loadRivBuffer } from '../features/human/Mascot/rivCache';
import { apiClient } from './apiClient';
import { getBackendUrl } from './backendUrl';
export async function fetchMascotList(): Promise<MascotSummary[]> {
const res = await apiClient.get<ListMascotsResponse>('/mascots', { requireAuth: false });
return res.data.mascots;
}
export async function fetchMascotDetail(id: string): Promise<MascotDetailUnion> {
const safe = encodeURIComponent(id.trim());
if (!safe) throw new Error('mascot id is empty');
const res = await apiClient.get<GetMascotResponse>(`/mascots/${safe}`, { requireAuth: false });
return res.data.mascot;
}
/**
* Resolve a Rive mascot's binary, version-cached in IndexedDB. The backend
* stamps `version` into both the manifest and the `rivFileUrl` (`?v=`), so the
* binary is only re-downloaded when that version changes.
*/
export async function loadMascotRivBuffer(detail: RiveMascotDetail): Promise<ArrayBuffer> {
const base = await getBackendUrl();
// rivFileUrl is backend-relative (e.g. "/mascots/toshi/riv?v=1.0.0").
const url = `${base}${detail.rivFileUrl}`;
return loadRivBuffer(detail.id, detail.version, url);
}
/**
* Lightweight in-memory cache for manifest fetches. Manifests carry theView on GitHub (pinned to a221052e0d)
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()`
Example fix
// before const detail = await fetchMascotDetail(selectedId); // after const detail = selectedId.trim() ? await fetchMascotDetail(selectedId) : null; // render 'select a mascot' empty state
Defensive patterns
Strategy: validation
Validate before calling
const id = raw ?? '';
if (!id.trim()) {
// render 'select a mascot' instead of calling the API
return null;
}
const detail = await fetchMascotDetail(id); Type guard
function isNonBlankId(id: unknown): id is string {
return typeof id === 'string' && id.trim().length > 0;
} Try / catch
try { const d = await fetchMascotDetail(id); }
catch (e) {
if (e instanceof Error && e.message === 'mascot id is empty') return null;
throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Finish choosing how OpenHuman runs (tap Continue on the setu
- OpenHuman could not reach its remote (cloud) runtime. Check
- agentTeamApi: ${label} must be a positive integer
- agentTeamApi.get: teamId is required
- agentTeamApi.listMessages: teamId is required
AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16).
Data as JSON: /api/errors/f1ad53cf7b3b4c92.
Report an issue: GitHub.