plandex-ai/plandex · warning
Invite already exists
Error message
Invite already exists
What it means
InviteUserHandler returns this 400 when db.GetActiveInviteByEmail finds an already-active invite for the same org and email. It is a deliberate idempotency guard: only one pending invite per email per org is allowed. The request is rejected before any new invite row is created.
Source
Thrown at app/server/handlers/invites.go:124
if isMember {
log.Println("User is already a member of org")
http.Error(w, "User is already a member of org", http.StatusBadRequest)
return
}
}
// ensure invite isn't already active
invite, err := db.GetActiveInviteByEmail(auth.OrgId, req.Email)
if err != nil {
log.Printf("Error getting invite: %v\n", err)
http.Error(w, "Error getting invite: "+err.Error(), http.StatusInternalServerError)
return
}
if invite != nil {
log.Println("Invite already exists")
http.Error(w, "Invite already exists", http.StatusBadRequest)
return
}
err = db.WithTx(r.Context(), "invite user", func(tx *sqlx.Tx) error {
err = db.CreateInvite(&db.Invite{
OrgId: auth.OrgId,
OrgRoleId: req.OrgRoleId,
Email: req.Email,
Name: req.Name,
InviterId: currentUserId,
}, tx)
if err != nil {
log.Printf("Error creating invite: %v\n", err)
return fmt.Errorf("error creating invite: %v", err)
}
View on GitHub (pinned to e2d772072e)
Solutions
- If you want to resend, delete the existing pending invite first (DeleteInviteHandler) then invite again
- Treat the 400 as a no-op: an invite is already waiting in the invitee's inbox
- Check the pending invites list (ListPendingInvitesHandler) before re-inviting
- Client-side: catch this 400 and show 'An invite for this email is already pending'
Example fix
// before
if invite != nil {
http.Error(w, "Invite already exists", http.StatusBadRequest)
return
}
// after
if invite != nil {
writeApiError(w, shared.ApiError{Type: shared.ApiErrorTypeOther, Status: http.StatusConflict, Msg: "An active invite for this email already exists"})
return
} Defensive patterns
Strategy: validation
Validate before calling
// client: look for an existing active invite before calling invite again
invites, err := listPendingInvites(orgId)
if err == nil {
for _, inv := range invites {
if strings.ToLower(inv.Email) == strings.ToLower(email) {
return fmt.Errorf("an active invite for %s already exists", email)
}
}
} Type guard
func isInviteAlreadyExistsError(statusCode int, body string) bool {
return statusCode == http.StatusBadRequest && strings.Contains(body, "Invite already exists")
} Try / catch
resp, err := client.Post(inviteUrl, jsonBody)
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode == 400 && strings.Contains(string(body), "Invite already exists") {
// idempotent outcome: delete the old invite first if a resend is intended
return nil
} Prevention
- Disable the invite button in the UI while an invite for that email is pending
- Delete the existing pending invite (DELETE invite endpoint) before resending
- Cache known pending invite emails client-side
- Treat this 400 as success in retry/idempotency logic
When it happens
Trigger: POST to the invite-user endpoint with an email that already has a pending (not yet accepted, expired, or deleted) invite in auth.OrgId; re-inviting someone before they accept; resending an invite to an unaccepted email.
Common situations: Admins clicking 'invite' twice; wanting to resend an invitation but the old one is still pending; a stale pending invite from a previous teammate; the invited person's email was entered slightly differently previously (aliases, case handled by lowercase normalization).
Related errors
- User is already a member of org
- Error parsing request body
- Cannot delete main branch
- User already has access to org via domain:
- Error parsing request body
AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05).
Data as JSON: /api/errors/f4d571d01c85509e.
Report an issue: GitHub.