{"record":{"id":"0fbbf9d219002f12","repo":"moeru-ai/airi","slug":"failed-to-bookmark-character","errorCode":null,"errorMessage":"Failed to bookmark character","messagePattern":"Failed to bookmark character","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/services/characters.ts","lineNumber":229,"sourceCode":"    options?.abortSignal?.throwIfAborted()\n  }\n\n  async function likeRemote(client: CharactersRemoteClient, id: string, options?: CharacterServiceOptions): Promise<Character> {\n    options?.abortSignal?.throwIfAborted()\n    const res = await client.api.v1.characters[':id'].like.$post({ param: { id } }, requestOptions(options))\n    if (!res.ok)\n      throw new Error('Failed to like character')\n\n    const data = await res.json()\n    options?.abortSignal?.throwIfAborted()\n    return parse(data)\n  }\n\n  async function bookmarkRemote(client: CharactersRemoteClient, id: string, options?: CharacterServiceOptions): Promise<Character> {\n    options?.abortSignal?.throwIfAborted()\n    const res = await client.api.v1.characters[':id'].bookmark.$post({ param: { id } }, requestOptions(options))\n    if (!res.ok)\n      throw new Error('Failed to bookmark character')\n\n    const data = await res.json()\n    options?.abortSignal?.throwIfAborted()\n    return parse(data)\n  }\n\n  return {\n    buildLocal,\n    fetchRemote,\n    fetchRemoteById,\n    createRemote,\n    updateRemote,\n    removeRemote,\n    likeRemote,\n    bookmarkRemote,\n  }\n}\n","sourceCodeStart":211,"sourceCodeEnd":247,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-ui/src/services/characters.ts#L211-L247","documentation":"Thrown by bookmarkRemote when POST /api/v1/characters/:id/bookmark returns a non-2xx status through the typed RPC client. Like the other character service methods it only inspects res.ok, so authentication problems, missing characters, and backend failures are indistinguishable from the message alone. It fires on the bookmark toggle path, typically from a character card action button.","triggerScenarios":"Bookmarking a character whose id is unknown to the hosted API (404); bookmarking with an expired or missing auth session (401/403); API server error (500); request racing a character removal. Happens on the same client used by likeRemote, so any environment where likes also fail will fail bookmarks too.","commonSituations":"Remote client pointed at a backend that is stopped or misrouted; stale character id kept in UI state after the character was deleted in another tab/window; token expiry during a long-lived session.","solutions":["Inspect the HTTP status of the failing request (network tab or by extending the thrown error with res.status).","Ensure the API backend and its edge routing are running if the status is 5xx.","Refresh authentication if 401/403.","Verify the character still exists (fetchRemoteById) before bookmarking.","Guard the UI toggle with an in-flight flag and abort signal."],"exampleFix":"// before\nconst res = await client.api.v1.characters[':id'].bookmark.$post({ param: { id } }, requestOptions(options))\nif (!res.ok)\n  throw new Error('Failed to bookmark character')\n\n// after\nconst res = await client.api.v1.characters[':id'].bookmark.$post({ param: { id } }, requestOptions(options))\nif (!res.ok)\n  throw new Error(`Failed to bookmark character (HTTP ${res.status})`)","handlingStrategy":"try-catch","validationCode":"const character = await charactersService.fetchRemoteById(client, id, { abortSignal }).catch(() => null)\nif (!character)\n  throw new Error(`Character ${id} does not exist on this server`)\nawait charactersService.bookmarkRemote(client, id, { abortSignal })","typeGuard":null,"tryCatchPattern":"try {\n  await charactersService.bookmarkRemote(client, id, { abortSignal })\n}\ncatch (error) {\n  if (abortSignal?.aborted) return\n  toast.error(errorMessageFrom(error) ?? 'Failed to bookmark character')\n  // on auth suspicion (401), route the user to sign-in\n}","preventionTips":["Treat bookmark and like as one auth/availability domain: if one fails, expect the other to.","Refresh bookmarks from the server after recovery instead of trusting local toggle state.","Bound retries; do not auto-retry non-idempotent-looking failures in a loop."],"tags":["http","api-client","characters","remote","hono-rpc"],"backgroundTag":"http-error-status","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}