{"record":{"id":"11a462053b958ec5","repo":"Significant-Gravitas/AutoGPT","slug":"invitation-already-revoked","errorCode":null,"errorMessage":"Invitation already revoked","messagePattern":"Invitation already revoked","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"autogpt_platform/backend/backend/api/features/orgs/invitation_routes.py","lineNumber":234,"sourceCode":"    \"/{token}/decline\",\n    summary=\"Decline invitation\",\n    tags=[\"invitations\"],\n    dependencies=[Security(requires_user)],\n    status_code=204,\n)\nasync def decline_invitation(\n    token: str,\n    user_id: Annotated[str, Security(get_user_id)],\n) -> None:\n    invitation = await prisma.orginvitation.find_unique(where={\"token\": token})\n    if invitation is None:\n        raise NotFoundError(\"Invitation not found\")\n\n    # State checks — same as accept_invitation\n    if invitation.acceptedAt is not None:\n        raise HTTPException(400, detail=\"Invitation already accepted\")\n    if invitation.revokedAt is not None:\n        raise HTTPException(400, detail=\"Invitation already revoked\")\n    if invitation.expiresAt < datetime.now(timezone.utc):\n        raise HTTPException(400, detail=\"Invitation has expired\")\n\n    # Verify the declining user's email matches the invitation\n    declining_user = await prisma.user.find_unique(where={\"id\": user_id})\n    if declining_user is None:\n        raise HTTPException(401, detail=\"User not found\")\n    if declining_user.email.lower() != invitation.email.lower():\n        raise HTTPException(\n            403, detail=\"This invitation was sent to a different email address\"\n        )\n\n    await prisma.orginvitation.update(\n        where={\"id\": invitation.id},\n        data={\"revokedAt\": datetime.now(timezone.utc)},\n    )\n\n","sourceCodeStart":216,"sourceCodeEnd":252,"githubUrl":"https://github.com/Significant-Gravitas/AutoGPT/blob/9c8bb5550f446ba5d3046b78896578742495b3cf/autogpt_platform/backend/backend/api/features/orgs/invitation_routes.py#L216-L252","documentation":"Raised by POST /api/invitations/{token}/decline when revokedAt is already set — an admin revoked the invitation. Decline is redundant for revoked invitations; note decline itself also works by setting revokedAt. HTTP 400.","triggerScenarios":"Admin revokes while the invitee still has the link open; invitee clicks decline on an already-revoked (or already-declined-from-another-device) invitation.","commonSituations":"Race between admin cleanup and user action; user clicks the decline link twice; two devices acting on the same invitation.","solutions":["Treat this 400 as success — the intended end state (revoked) already holds; clear the invitation from the UI.","Do not retry; re-fetch pending invitations to reconcile client state.","Admins seeing this: the invitation is already inactive; no further action needed."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await api.post(`/api/invitations/${token}/decline`);\n} catch (e) {\n  if (e.status === 400 && /revoked/.test(e.detail)) { clearInvitation(token); return; }\n  throw e;\n}","preventionTips":["Do not retry decline after revoked response — the end state already holds","Reconcile with the pending list after admin revocations","Remember decline is implemented as revocation; double decline hits this path"],"tags":["invitations","revoked","state","http-400","decline"],"backgroundTag":null,"analyzedSha":"9c8bb5550f446ba5d3046b78896578742495b3cf","analyzedAt":"2026-08-14T17:17:21.957Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}