tinyhumansai/openhuman · error · Error
Failed to send magic link
Error message
Failed to send magic link (${response.status}) What it means
Thrown by the magic-link request in authApi.ts when the POST to `${backendUrl}/auth/email/send-link` returns a non-ok HTTP status AND the response body is either unparseable or has no `error` field to use instead. The numeric status is embedded as the fallback message. This is a backend HTTP error surfaced to the login UI — the fetch itself succeeded at the transport level.
Solutions
- Read the status code from the message: 400 → validate email and frontendRedirectUri, 429 → wait and retry, 5xx → backend/provider outage, check backend health.
- Verify backendUrl (resolveBackendUrl / BACKEND_API_URL config) points at the intended environment.
- Confirm frontendRedirectUri is an allow-listed redirect for the backend's email-link flow.
- If it persists, curl the endpoint directly with the same payload to see the backend's raw response.
Example fix
// before
if (!response.ok) {
const body = (await response.json().catch(() => ({}))) as { error?: string };
throw new Error(body.error ?? `Failed to send magic link (${response.status})`);
}
// after — map status to actionable user guidance
if (!response.ok) {
const body = (await response.json().catch(() => ({}))) as { error?: string; message?: string };
const hint = response.status === 429 ? 'Too many attempts — try again shortly.'
: response.status >= 500 ? 'Server error — please retry later.'
: undefined;
throw new Error(body.error ?? hint ?? `Failed to send magic link (${response.status})`);
} Defensive patterns
Strategy: try-catch
Validate before calling
const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const redirectOk = /^https?:\/\//.test(frontendRedirectUri ?? '');
if (!emailOk || !redirectOk) {
// fix inputs before calling sendMagicLink — avoids the most common 400s
} Try / catch
try {
await sendMagicLink(email, frontendRedirectUri);
} catch (e) {
if (e instanceof Error && /Failed to send magic link \((\d+)\)/.test(e.message)) {
const status = Number(e.message.match(/\((\d+)\)/)?.[1]);
if (status === 429) notify('Too many attempts — wait a moment.');
else if (status >= 500) notify('Server issue — try again later.');
else notify('Could not send the link — check the email address.');
} else throw e;
} Prevention
- Validate email format and redirect URI client-side before the POST.
- Confirm the backend URL environment before testing auth flows.
- Parse the backend's error field when present; only the status fallback is generic.
When it happens
Trigger: Backend returns 400 (malformed email/redirect URI), 429 (rate limited), 5xx, or a proxy/gateway error page with an empty JSON body; backendUrl points to an environment whose /auth/email/send-link route is disabled or misconfigured; frontendRedirectUri not allow-listed by the backend.
Common situations: Wrong BACKEND_API_URL (staging vs prod) in .env; the email-sending provider behind the backend is down so the backend 500s with an empty body; corporate proxy intercepting and returning HTML (json() fails → {}); rate-limiting on repeated login attempts.
Understand the failure class
Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.
Related errors
AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17).
Data as JSON: /api/errors/8dec4d3ccdab5aec.
Report an issue: GitHub.
Appendix: 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 7491200858)