tinyhumansai/openhuman · error

Failed to send magic link (${response.status})

Error message

Failed to send magic link (${response.status})

What it means

POST to <backendUrl>/auth/email/send-link (magic-link email login) returned a non-2xx status, and the JSON body either failed to parse or contained no error field, so the client falls back to a generic message that embeds the HTTP status. The specific status is the main diagnostic.

Source

Thrown at app/src/services/api/authApi.ts:33

  email: string,
  frontendRedirectUri: string,
  timeoutMs = EMAIL_MAGIC_LINK_TIMEOUT_MS
): Promise<void> {
  const backendUrl = await getBackendUrl();
  const controller = new AbortController();
  const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs);

  try {
    const versionHeaders = await getClientVersionHeaders();
    const response = await fetch(`${backendUrl}/auth/email/send-link`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', ...versionHeaders },
      body: JSON.stringify({ email, frontendRedirectUri }),
      signal: controller.signal,
    });
    if (!response.ok) {
      const body = (await response.json().catch(() => ({}))) as { error?: string };
      throw new Error(body.error ?? `Failed to send magic link (${response.status})`);
    }
  } catch (error) {
    if (
      (error instanceof DOMException && error.name === 'AbortError') ||
      (error instanceof Error && error.name === 'AbortError')
    ) {
      throw new Error('Request timed out. Please try again.');
    }
    throw error;
  } finally {
    window.clearTimeout(timeoutId);
  }
}

/**
 * Consume a verified login token and return the JWT.
 * Works for both Telegram and OAuth login tokens.
 * POST /telegram/login-tokens/:token/consume (no auth required)

View on GitHub (pinned to a221052e0d)

Solutions

  1. Check the embedded status: 4xx → fix the request (email format, rate limit); 5xx/502-504 → backend/infra issue, verify the backend URL and service health
  2. Confirm the backend URL config used by the frontend matches the intended environment
  3. Reproduce with curl against the same URL to see the raw body the browser received
  4. Add the response body text to error reporting so the fallback path is observable

Example fix

// before
throw new Error(body.error ?? `Failed to send magic link (${response.status})`);

// after (in caller, capture status for diagnostics)
try { await sendMagicLink(email); }
catch (e) {
  const m = /\((\d{3})\)$/.exec(String(e.message));
  report('magic-link-failed', { status: m?.[1], emailDomain: email.split('@')[1] });
  showError(e.message);
}
Defensive patterns

Strategy: try-catch

Try / catch

try { await sendMagicLink(email); showCheckYourEmail(); }
catch (e) {
  const status = /\((\d{3})\)$/.exec(String((e as Error).message))?.[1];
  if (status === '429') showNotice('Too many attempts — wait a moment and retry.');
  else if (status && Number(status) >= 500) showError('Service issue — try again shortly.');
  else showError((e as Error).message);
}

Prevention

When it happens

Trigger: Backend unreachable-through-proxy returning 502/504; 400 from a malformed email; 429 from rate limiting; 500 from a backend bug — all with a non-JSON or error-less body trigger the fallback text. Bodies that do include error use that message instead.

Common situations: Wrong VITE backend URL for the environment (staging URL from a prod build); backend redeploy/down; rate limit hit by repeated resend clicks; CORS/network middleboxes replacing the body with HTML.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16). Data as JSON: /api/errors/8dec4d3ccdab5aec. Report an issue: GitHub.