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

  1. If you want to resend, delete the existing pending invite first (DeleteInviteHandler) then invite again
  2. Treat the 400 as a no-op: an invite is already waiting in the invitee's inbox
  3. Check the pending invites list (ListPendingInvitesHandler) before re-inviting
  4. 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

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


AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05). Data as JSON: /api/errors/f4d571d01c85509e. Report an issue: GitHub.