moeru-ai/airi · error · Error
link failed
Error message
link failed
What it means
Thrown in link() when either linkSocial() (OAuth2 providers) or linkSteam() (OpenID 2.0) resolves with a non-null error. It indicates the better-auth/social or steam server plugin rejected the link-start request before issuing a redirect URL. 'link failed' is the fallback string when the backend error has no message.
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 27111382b4)
Solutions
- Inspect error.value (args.describeError) for the backend message — it usually names the missing config.
- Ensure the provider is configured in better-auth socialProviders and the Steam plugin is registered for providerId === 'steam'.
- Confirm callbackURL is allowlisted; buildCallbackURL must return a URL the server accepts.
- Re-authenticate if the session lapsed, then retry link().
Defensive patterns
Strategy: try-catch
Validate before calling
// confirm the provider is configured server-side before offering it if (!knownSocialProviders.includes(providerId) && providerId !== 'steam') // do not attempt link; provider not registered
Type guard
null
Try / catch
try {
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')
}
catch (err) {
error.value = args.describeError(err) || args.messages.linkFailed
} Prevention
- Ensure each provider is configured in better-auth socialProviders and the Steam plugin is registered.
- Allowlist the callbackURL/buildCallbackURL output server-side.
- Serialize link attempts via the inFlight guard to avoid overlapping OAuth redirects.
When it happens
Trigger: Calling link(providerId, providerName) where linkSocial({ provider, callbackURL, errorCallbackURL }) or linkSteam({ callbackURL, errorCallbackURL }) returns an error object. Server-side provider not configured in socialProviders, callbackURL not in the allow-list, the steam plugin not registered, or the user is not authenticated.
Common situations: Provider disabled/missing in better-auth config (e.g. Google client ID/secret unset). Redirect/callback URL not allowlisted on the server. Steam plugin not loaded. Auth session expired before the click.
Related errors
- listAccounts failed
- unlinkAccount failed
- Auth request failed (${response.status})
- Unexpected response
- Token exchange failed: ${response.status} ${error}
AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12).
Data as JSON: /api/errors/8fc1b0399e77551d.
Report an issue: GitHub.