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 the

View on GitHub (pinned to a221052e0d)

Solutions

  1. Guard the call site: skip the fetch when `id.trim()` is empty and render a placeholder instead
  2. Fix the source of the blank id (route param, selection state) rather than swallowing the error
  3. 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

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


AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16). Data as JSON: /api/errors/f1ad53cf7b3b4c92. Report an issue: GitHub.