{"record":{"id":"1ec1d33df03f11d2","repo":"moeru-ai/airi","slug":"failed-to-fetch-characters","errorCode":null,"errorMessage":"Failed to fetch characters","messagePattern":"Failed to fetch characters","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/services/characters.ts","lineNumber":163,"sourceCode":"      prompts: payload.prompts?.map(prompt => ({\n        id: nanoid(),\n        characterId: id,\n        language: prompt.language,\n        type: prompt.type,\n        content: prompt.content,\n      })),\n      likes: [],\n      bookmarks: [],\n    })\n  }\n\n  async function fetchRemote(client: CharactersRemoteClient, params: { all?: boolean }, options?: CharacterServiceOptions): Promise<Character[]> {\n    options?.abortSignal?.throwIfAborted()\n    const res = await client.api.v1.characters.$get({\n      query: { all: String(params.all ?? false) },\n    }, requestOptions(options))\n    if (!res.ok)\n      throw new Error('Failed to fetch characters')\n\n    const data = await res.json()\n    options?.abortSignal?.throwIfAborted()\n    return data.map((item: unknown) => parse(item))\n  }\n\n  async function fetchRemoteById(client: CharactersRemoteClient, id: string, options?: CharacterServiceOptions): Promise<Character> {\n    options?.abortSignal?.throwIfAborted()\n    const res = await client.api.v1.characters[':id'].$get({ param: { id } }, requestOptions(options))\n    if (!res.ok)\n      throw new Error('Failed to fetch character')\n\n    const data = await res.json()\n    options?.abortSignal?.throwIfAborted()\n    return parse(data)\n  }\n\n  async function createRemote(client: CharactersRemoteClient, payload: CreateCharacterPayload, options?: CharacterServiceOptions): Promise<Character> {","sourceCodeStart":145,"sourceCodeEnd":181,"githubUrl":"https://github.com/moeru-ai/airi/blob/27111382b4a79a7e983289d6e983a06af185ed0f/packages/stage-ui/src/services/characters.ts#L145-L181","documentation":"Thrown by CharactersService.fetchRemote after the GET /v1/characters endpoint returns a non-ok response. The service checks abortSignal before and after the call, then requires res.ok to parse the list. The generic message drops the status code and body, so the caller only knows the list fetch failed.","triggerScenarios":"client.api.v1.characters.$get({ query: { all } }) resolves with ok=false. Typical: 401/403 (not authenticated), 500 (server error), network-layer 0 mapped to a non-ok response, or the backend characters route not deployed at the configured server URL.","commonSituations":"Logged-out session hitting the characters list; backend API server down or misconfigured; wrong SERVER_URL; expired JWT returning 401; CORS blocking the response so fetch resolves to an opaque/non-ok result.","solutions":["Confirm the user is authenticated and the JWT is valid before calling fetchRemote.","Verify SERVER_URL points at a backend that serves the /v1/characters route.","Improve diagnostics by including res.status in the thrown message (the current code discards it).","Wrap the call in try/catch and fall back to local/cached characters or show an empty state with a retry."],"exampleFix":"// before\nif (!res.ok)\n  throw new Error('Failed to fetch characters')\n\n// after: preserve status for diagnosis\nif (!res.ok)\n  throw new Error(`Failed to fetch characters (status ${res.status})`)","handlingStrategy":"try-catch","validationCode":"function isAuthenticated(): boolean {\n  return !!getAuthToken()\n}\n\n// before fetching\nif (!isAuthenticated()) {\n  // skip or redirect to login\n  return []\n}\nawait fetchRemote(client, { all })","typeGuard":null,"tryCatchPattern":"try {\n  const characters = await fetchRemote(client, { all }, { abortSignal })\n}\ncatch (error) {\n  // Fall back to local/cached characters or show empty state with retry.\n  // Consider improving the service to include res.status for branching.\n  return localCache\n}","preventionTips":["Confirm authentication and SERVER_URL before calling fetchRemote.","Patch the service to embed res.status in the message so callers can branch on 401/500.","Cache the last successful list so the UI degrades gracefully on failure."],"tags":["network","http","characters","api","auth"],"backgroundTag":null,"analyzedSha":"27111382b4a79a7e983289d6e983a06af185ed0f","analyzedAt":"2026-08-12T18:33:34.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}