{"record":{"id":"8dec4d3ccdab5aec","repo":"tinyhumansai/openhuman","slug":"failed-to-send-magic-link-response-status","errorCode":null,"errorMessage":"Failed to send magic link (${response.status})","messagePattern":"Failed to send magic link \\((.+?)\\)","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"app/src/services/api/authApi.ts","lineNumber":33,"sourceCode":"  email: string,\n  frontendRedirectUri: string,\n  timeoutMs = EMAIL_MAGIC_LINK_TIMEOUT_MS\n): Promise<void> {\n  const backendUrl = await getBackendUrl();\n  const controller = new AbortController();\n  const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs);\n\n  try {\n    const versionHeaders = await getClientVersionHeaders();\n    const response = await fetch(`${backendUrl}/auth/email/send-link`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json', ...versionHeaders },\n      body: JSON.stringify({ email, frontendRedirectUri }),\n      signal: controller.signal,\n    });\n    if (!response.ok) {\n      const body = (await response.json().catch(() => ({}))) as { error?: string };\n      throw new Error(body.error ?? `Failed to send magic link (${response.status})`);\n    }\n  } catch (error) {\n    if (\n      (error instanceof DOMException && error.name === 'AbortError') ||\n      (error instanceof Error && error.name === 'AbortError')\n    ) {\n      throw new Error('Request timed out. Please try again.');\n    }\n    throw error;\n  } finally {\n    window.clearTimeout(timeoutId);\n  }\n}\n\n/**\n * Consume a verified login token and return the JWT.\n * Works for both Telegram and OAuth login tokens.\n * POST /telegram/login-tokens/:token/consume (no auth required)","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/app/src/services/api/authApi.ts#L15-L51","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nif (!response.ok) {\n  const body = (await response.json().catch(() => ({}))) as { error?: string };\n  throw new Error(body.error ?? `Failed to send magic link (${response.status})`);\n}\n\n// after — map status to actionable user guidance\nif (!response.ok) {\n  const body = (await response.json().catch(() => ({}))) as { error?: string; message?: string };\n  const hint = response.status === 429 ? 'Too many attempts — try again shortly.'\n    : response.status >= 500 ? 'Server error — please retry later.'\n    : undefined;\n  throw new Error(body.error ?? hint ?? `Failed to send magic link (${response.status})`);\n}","handlingStrategy":"try-catch","validationCode":"const emailOk = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);\nconst redirectOk = /^https?:\\/\\//.test(frontendRedirectUri ?? '');\nif (!emailOk || !redirectOk) {\n  // fix inputs before calling sendMagicLink — avoids the most common 400s\n}","typeGuard":null,"tryCatchPattern":"try {\n  await sendMagicLink(email, frontendRedirectUri);\n} catch (e) {\n  if (e instanceof Error && /Failed to send magic link \\((\\d+)\\)/.test(e.message)) {\n    const status = Number(e.message.match(/\\((\\d+)\\)/)?.[1]);\n    if (status === 429) notify('Too many attempts — wait a moment.');\n    else if (status >= 500) notify('Server issue — try again later.');\n    else notify('Could not send the link — check the email address.');\n  } else throw e;\n}","preventionTips":["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."],"tags":["http","auth","magic-link","backend-api","status-code"],"backgroundTag":"http-request-failed","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}