moeru-ai/airi · error · Error

Failed to fetch characters

Error message

Failed to fetch characters

What it means

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.

Source

Thrown at packages/stage-ui/src/services/characters.ts:163

      prompts: payload.prompts?.map(prompt => ({
        id: nanoid(),
        characterId: id,
        language: prompt.language,
        type: prompt.type,
        content: prompt.content,
      })),
      likes: [],
      bookmarks: [],
    })
  }

  async function fetchRemote(client: CharactersRemoteClient, params: { all?: boolean }, options?: CharacterServiceOptions): Promise<Character[]> {
    options?.abortSignal?.throwIfAborted()
    const res = await client.api.v1.characters.$get({
      query: { all: String(params.all ?? false) },
    }, requestOptions(options))
    if (!res.ok)
      throw new Error('Failed to fetch characters')

    const data = await res.json()
    options?.abortSignal?.throwIfAborted()
    return data.map((item: unknown) => parse(item))
  }

  async function fetchRemoteById(client: CharactersRemoteClient, id: string, options?: CharacterServiceOptions): Promise<Character> {
    options?.abortSignal?.throwIfAborted()
    const res = await client.api.v1.characters[':id'].$get({ param: { id } }, requestOptions(options))
    if (!res.ok)
      throw new Error('Failed to fetch character')

    const data = await res.json()
    options?.abortSignal?.throwIfAborted()
    return parse(data)
  }

  async function createRemote(client: CharactersRemoteClient, payload: CreateCharacterPayload, options?: CharacterServiceOptions): Promise<Character> {

View on GitHub (pinned to 27111382b4)

Solutions

  1. Confirm the user is authenticated and the JWT is valid before calling fetchRemote.
  2. Verify SERVER_URL points at a backend that serves the /v1/characters route.
  3. Improve diagnostics by including res.status in the thrown message (the current code discards it).
  4. Wrap the call in try/catch and fall back to local/cached characters or show an empty state with a retry.

Example fix

// before
if (!res.ok)
  throw new Error('Failed to fetch characters')

// after: preserve status for diagnosis
if (!res.ok)
  throw new Error(`Failed to fetch characters (status ${res.status})`)
Defensive patterns

Strategy: try-catch

Validate before calling

function isAuthenticated(): boolean {
  return !!getAuthToken()
}

// before fetching
if (!isAuthenticated()) {
  // skip or redirect to login
  return []
}
await fetchRemote(client, { all })

Try / catch

try {
  const characters = await fetchRemote(client, { all }, { abortSignal })
}
catch (error) {
  // Fall back to local/cached characters or show empty state with retry.
  // Consider improving the service to include res.status for branching.
  return localCache
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12). Data as JSON: /api/errors/1ec1d33df03f11d2. Report an issue: GitHub.