moeru-ai/airi · error · Error
listAccounts failed
Error message
listAccounts failed
What it means
Fallback message used in refresh() when the better-auth client's listAccounts() call resolves with an error object whose own message is empty. The throw is caught by the composable and written to its error ref, which the linked-accounts settings UI renders. Root causes are transport- or session-level: expired auth cookie, auth server unreachable, or a proxy interfering with the account list endpoint.
Solutions
- Sign out and back in to mint a fresh session, then reopen the linked-accounts page
- Verify the auth server the client points at is running and its base URL is correct
- Inspect the network tab for the list-accounts request: 401 means session, 5xx means server
- If behind a proxy, confirm cookies and CORS headers survive for the auth origin
Example fix
// before
const { data, error } = await args.client.listAccounts() // opaque 'listAccounts failed'
// after
const session = await args.client.getSession()
if (!session) {
error.value = 'Session expired — sign in again'
return
}
const { data, error } = await args.client.listAccounts() Defensive patterns
Strategy: try-catch
Validate before calling
const session = await args.client.getSession() if (!session) return // skip listAccounts until re-authenticated
Try / catch
try {
await refresh()
}
catch (err) {
// error.value already carries describeError(err) or the fallback;
// treat session expiry as a redirect, everything else as a toast.
if (isAuthExpired(err))
router.push('/sign-in')
} Prevention
- Check getSession() before rendering the linked-accounts panel
- Keep the auth service and app on matching better-auth versions
- Configure cookie SameSite and CORS for the auth origin when proxying
- Refresh account data on window focus to recover from stale sessions
When it happens
Trigger: Calling client.listAccounts() with an expired or revoked session cookie; the standalone auth service being down so the client surfaces a network error result; a reverse proxy or CORS layer returning an error status for the list-accounts route; a better-auth server version that renamed or removed the route.
Common situations: Dev server restart invalidated the session; auth service stopped while the settings page loads; cookies blocked in an embedded webview or iframe (SameSite); user signed out in another tab.
Related errors
- unlinkAccount failed
- audio models upstream
- audio models upstream returned malformed body
- audio voices upstream
- audio voices upstream returned malformed body
AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18).
Data as JSON: /api/errors/9640cb85aaf259eb.
Report an issue: GitHub.
Appendix: source
Thrown at packages/stage-ui/src/composables/use-linked-accounts.ts:162
)
/**
* Client-side mirror of better-auth's `FAILED_TO_UNLINK_LAST_ACCOUNT`
* guard so we can surface a user-friendly message before round-tripping.
*/
function isLastSignInMethod(providerId: string): boolean {
if (providerId === 'credential')
return socialLinkedCount.value === 0
return !hasCredentialAccount.value && socialLinkedCount.value <= 1
}
async function refresh() {
loading.value = true
error.value = null
try {
const { data, error: apiError } = await args.client.listAccounts()
if (apiError)
throw new Error(apiError.message ?? 'listAccounts failed')
// better-auth 1.6.6 widens listAccounts elements to `any`; consume
// the row directly rather than dressing `any` up with a fake shape.
// Field layout: node_modules/better-auth/dist/api/routes/account.mjs L20-50.
linkedAccounts.value = (data ?? []).map(account => ({
id: account.id,
accountId: account.accountId,
providerId: account.providerId,
createdAt: account.createdAt instanceof Date
? account.createdAt.toISOString()
: account.createdAt,
scopes: account.scopes ?? [],
}))
loaded.value = true
}
catch (err) {
// Keep prior `linkedAccounts` on error so a transient 5xx doesn't
// flip `hasCredentialAccount` and mis-route the password UI.
// Source: PR #1753 review (chatgpt-codex-connector P2).View on GitHub (pinned to 677329427f)