{"record":{"id":"52d90c377bfee2b2","repo":"BloopAI/vibe-kanban","slug":"failed-to-accept-invitation-res-status","errorCode":null,"errorMessage":"Failed to accept invitation (${res.status})","messagePattern":"Failed to accept invitation \\((.+?)\\)","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/remote-web/src/shared/lib/api.ts","lineNumber":137,"sourceCode":"  if (!res.ok) {\n    throw new Error(`Invitation not found (${res.status})`);\n  }\n  return res.json();\n}\n\nexport async function acceptInvitation(\n  token: string,\n  accessToken: string,\n): Promise<AcceptInvitationResponse> {\n  const res = await fetch(`${API_BASE}/v1/invitations/${token}/accept`, {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      Authorization: `Bearer ${accessToken}`,\n    },\n  });\n  if (!res.ok) {\n    throw new Error(`Failed to accept invitation (${res.status})`);\n  }\n  return res.json();\n}\n\nexport async function refreshTokens(\n  refreshToken: string,\n): Promise<{ access_token: string; refresh_token: string }> {\n  const res = await fetch(`${API_BASE}/v1/tokens/refresh`, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ refresh_token: refreshToken }),\n  });\n  if (!res.ok) {\n    const err = new Error(`Token refresh failed (${res.status})`);\n    (err as Error & { status: number }).status = res.status;\n    throw err;\n  }\n  return res.json();","sourceCodeStart":119,"sourceCodeEnd":155,"githubUrl":"https://github.com/BloopAI/vibe-kanban/blob/4deb7eca8f381f7cbc1f9d15515a9ab8f8009053/packages/remote-web/src/shared/lib/api.ts#L119-L155","documentation":"acceptInvitation POSTs to {API_BASE}/v1/invitations/{token}/accept with a Bearer access token to join the invited organization. Any non-ok response triggers this generic Error containing only the status code. Unlike refreshTokens, it does not attach the status as a property, so callers can only parse the message to distinguish causes.","triggerScenarios":"POST /v1/invitations/{token}/accept returns 401/403 (access token expired, invalid, or belonging to a different user than the invitee), 404 (token unknown/already accepted), 410 (expired invitation), or 409/422 (user already a member, role conflict).","commonSituations":"User's session token expired between login and accepting; the access token passed in is from a different account than the invited email; invitation link reused after acceptance; invite expired while the user sat on the acceptance page.","solutions":["Ensure the user is logged in as the invited account and pass a fresh access token; call refreshTokens if it may be stale.","If status is 401/403, re-authenticate (the token is expired or for the wrong user).","If status is 404/410, request a new invitation from the admin.","Check the server response body (e.g. via devtools) for the specific rejection reason and surface it to the user."],"exampleFix":"// before\nif (!res.ok) {\n  throw new Error(`Failed to accept invitation (${res.status})`);\n}\n// after\nif (!res.ok) {\n  const err = new Error(`Failed to accept invitation (${res.status})`);\n  (err as Error & { status: number }).status = res.status;\n  throw err;\n}","handlingStrategy":"try-catch","validationCode":"const accessToken = await getToken();\nif (!accessToken) {\n  // force login before attempting acceptance\n  redirectToLogin();\n}","typeGuard":"function isAcceptInvitationResponse(v: unknown): v is AcceptInvitationResponse {\n  const r = v as AcceptInvitationResponse;\n  return !!r && typeof r.organization_id === 'string' && typeof r.role === 'string';\n}","tryCatchPattern":"try {\n  const result = await acceptInvitation(token, accessToken);\n  navigate(`/org/${result.organization_slug}`);\n} catch (e) {\n  const status = Number(/\\((\\d{3})\\)$/.exec((e as Error).message)?.[1]);\n  if (status === 401 || status === 403) {\n    await triggerRefresh().catch(() => redirectToLogin());\n  } else {\n    show('Unable to accept this invitation — it may be expired or already used.');\n  }\n}","preventionTips":["Always send a freshly obtained access token; refresh proactively if it may be near expiry.","Confirm the logged-in user matches the invited email before showing the accept button.","Handle single-use invitations: disable the accept button after success.","Parse the status code out of the message or patch the throw site to attach status as a property."],"tags":["http","api","invitation","auth"],"backgroundTag":"invitation-accept-failed","analyzedSha":"4deb7eca8f381f7cbc1f9d15515a9ab8f8009053","analyzedAt":"2026-08-29T09:24:13.446Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}