moeru-ai/airi · error · Error
link failed
Error message
link failed
What it means
Fallback message when better-auth's link initiation call (linkSocial for OAuth2 providers, or linkSteam for the OpenID 2.0 Steam flow) returns an error with no message. The happy path returns data.url and the code navigates the browser there, so this throw means the server refused to start the link at all. Typical causes are server-side provider misconfiguration or an unknown providerId.
Solutions
- Check the auth server config defines the provider (socialProviders entry with client id and secret)
- Confirm the providerId sent matches the server-side key exactly
- Verify the session is valid — if listAccounts works, the session is not the problem
- Inspect the /link-social or /link/steam response body for the server's own error
- For Steam, ensure the dedicated steam plugin exposing /link/steam is loaded
Example fix
// before
startLink('steam-oauth') // typo — no such provider on the server → 'link failed'
// after
const configured = new Set(['google', 'github', 'discord', 'steam'])
if (!configured.has(providerId))
throw new Error(`Provider ${providerId} is not configured on the auth server`)
startLink(providerId) Defensive patterns
Strategy: try-catch
Validate before calling
const providerIds = new Set(serverProviders.map(p => p.id)) if (!providerIds.has(providerId)) return // don't call link for unknown providers
Try / catch
try {
await startLink(providerId)
}
catch (err) {
// error.value carries describeError(err) or the fallback;
// point the user at server-side provider config, not the browser.
toast('Could not start linking — check that the provider is configured on the server')
} Prevention
- Configure the provider on the auth server before surfacing its button in the UI
- Source the provider list from server metadata instead of hardcoding
- Keep callback and errorCallbackURL origins in the server's trustedOrigins
When it happens
Trigger: Provider not configured in better-auth server options (missing client id/secret env vars); providerId string not matching any server-side socialProvider key; session expired so the link route returns 401; the steam plugin absent so /link/steam 404s; callbackURL rejected by the server's trustedOrigins.
Common situations: Adding a new social login without setting its env vars on the auth service; frontend and backend disagreeing on provider naming; deploying UI before the auth server update that adds the steam plugin.
Related errors
- changePassword failed
- deleteUser failed
- Invalid transcription model
- listAccounts failed
- [markdown-stress] No active provider/model for online mode
AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18).
Data as JSON: /api/errors/8fc1b0399e77551d.
Report an issue: GitHub.
Appendix: source
Thrown at packages/stage-ui/src/composables/use-linked-accounts.ts:236
async function link(providerId: LinkedProviderId, providerName: string) {
if (inFlight.value)
return
inFlight.value = providerId
error.value = null
message.value = args.messages.linkStarted(providerName)
try {
const callbackURL = args.buildCallbackURL ? args.buildCallbackURL() : window.location.href
// Steam is OpenID 2.0, not OAuth2 — the server steam plugin exposes a
// dedicated `/link/steam` endpoint, surfaced as `linkSteam` by the
// client plugin. Every other provider uses `/link-social`.
const result = providerId === 'steam'
? await args.client.linkSteam({ callbackURL, errorCallbackURL: callbackURL })
: await args.client.linkSocial({ provider: providerId, callbackURL, errorCallbackURL: callbackURL })
const { data, error: apiError } = result
if (apiError)
throw new Error(apiError.message ?? 'link failed')
if (data?.url) {
args.onLinkStarted?.(providerId)
window.location.assign(data.url)
return
}
// No URL came back (e.g. provider returned success synchronously) —
// refresh so the new row shows up without a navigation.
args.onLinkStarted?.(providerId)
await refresh()
}
catch (err) {
error.value = args.describeError(err) || args.messages.linkFailed
message.value = null
inFlight.value = null
}
}
// Auto-refresh: load on mount when already authed; react to sign-in /View on GitHub (pinned to 677329427f)