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

  1. Inspect error.value (args.describeError) for the backend message — it usually names the missing config.
  2. Ensure the provider is configured in better-auth socialProviders and the Steam plugin is registered for providerId === 'steam'.
  3. Confirm callbackURL is allowlisted; buildCallbackURL must return a URL the server accepts.
  4. 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

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


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