moeru-ai/airi · error · Error

HTTP ${res.status}: ${await readErrorDetail(res)}

Error message

HTTP ${res.status}: ${await readErrorDetail(res)}

What it means

Thrown by readJsonOrThrow() in cloud-mapper.ts when a /api/v1/chats request (GET list, POST create) returns non-2xx. It reads the error body as JSON and prefers body.message then body.error then statusText, so the server's structured error is surfaced. Used by listChats() and the createChat() success path; the 409 idempotent path is handled separately before this can fire for create.

Source

Thrown at packages/stage-ui/src/libs/chat-sync/cloud-mapper.ts:94

interface ApiErrorBody {
  error?: string
  message?: string
}

async function readErrorDetail(res: Response): Promise<string> {
  try {
    const body = await res.json() as ApiErrorBody
    return body.message ?? body.error ?? res.statusText
  }
  catch {
    // Non-JSON body — keep statusText.
    return res.statusText
  }
}

async function readJsonOrThrow<T>(res: Response, schema: v.BaseSchema<unknown, T, v.BaseIssue<unknown>>): Promise<T> {
  if (!res.ok) {
    throw new Error(`HTTP ${res.status}: ${await readErrorDetail(res)}`)
  }
  // External boundary: validate the success shape too. A server schema drift
  // would otherwise feed a structurally broken object into `reconcileLocalAndRemote`.
  const raw: unknown = await res.json()
  return v.parse(schema, raw)
}

async function throwOnError(res: Response): Promise<void> {
  if (res.ok)
    return
  throw new Error(`HTTP ${res.status}: ${await readErrorDetail(res)}`)
}

/**
 * Build a thin REST client over `/api/v1/chats` for cloud reconcile use cases.
 *
 * Use when:
 * - The session store needs to mirror local sessions to the server `chats`

View on GitHub (pinned to 27111382b4)

Solutions

  1. Check the HTTP status in the message: 401 → re-authenticate; 5xx → server issue; timeout → raise requestTimeoutMs or reduce payload.
  2. Confirm options.serverUrl is correct and reachable from the client.
  3. Ensure authedFetch is wired so Authorization is attached and 401 triggers a refresh before this throw.
  4. Inspect the server logs for the matching request to see the structured error detail.
Defensive patterns

Strategy: try-catch

Validate before calling

// ensure auth is wired and serverUrl is reachable before reconcile
if (!options.serverUrl) throw new Error('createCloudChatMapper: serverUrl required')
// authedFetch should attach Authorization and refresh on 401 before this throw fires

Type guard

null

Try / catch

try {
  const chats = await mapper.listChats()
}
catch (err) {
  if (err instanceof Error && err.message.startsWith('HTTP 401')) {
    // re-authenticate; the refresh cycle could not renew the token
  }
  else if (err instanceof Error && err.message.startsWith('HTTP ')) {
    // log status/body; retry transient 5xx with backoff
  }
  else throw err
}

Prevention

When it happens

Trigger: createCloudChatMapper's listChats() or createChat() receives a non-ok Response. Typical statuses: 401 (auth token expired and authedFetch refresh also failed), 403 (forbidden), 5xx (server error), network timeout via AbortSignal.timeout, or a non-JSON error body (falls back to statusText).

Common situations: Auth token expired and the refresh cycle could not renew it. Server /api/v1/chats route threw. Wrong serverUrl in mapper options. Request exceeded requestTimeoutMs (default 10s). Schema drift causing a 422 validation error upstream.

Related errors


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