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
- 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
- Confirm the backend URL config used by the frontend matches the intended environment
- Reproduce with curl against the same URL to see the raw body the browser received
- 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
- Verify the backend URL env per environment before release builds
- Surface the embedded HTTP status in error reporting for triage
- Rate-limit the send button client-side to avoid 429s from double clicks
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
- HTTP error! status: ${response.status}
- Request timed out. Please try again.
- [transport:cloud] HTTP ${response.status}: ${text || respons
- [transport:lan] HTTP ${response.status}: ${text || response.
- OpenAI Responses API failed (${response.status}): ${message}
AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16).
Data as JSON: /api/errors/8dec4d3ccdab5aec.
Report an issue: GitHub.