{"record":{"id":"423da1c62a6ecc17","repo":"plandex-ai/plandex","slug":"error-accepting-invite-v","errorCode":null,"errorMessage":"error accepting invite: %v","messagePattern":"error accepting invite: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/server/db/invite_helpers.go","lineNumber":122,"sourceCode":"\tif tx == nil {\n\t\t_, err = Conn.Exec(query, id)\n\t} else {\n\t\t_, err = tx.Exec(query, id)\n\t}\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error deleting invite: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc AcceptInvite(ctx context.Context, invite *Invite, inviteeId string) error {\n\terr := WithTx(ctx, \"accept invite\", func(tx *sqlx.Tx) error {\n\n\t\t_, err := tx.Exec(`UPDATE invites SET accepted_at = NOW(), invitee_id = $1 WHERE id = $2`, inviteeId, invite.Id)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error accepting invite: %v\", err)\n\t\t}\n\n\t\t// create org user\n\t\terr = CreateOrgUser(invite.OrgId, inviteeId, invite.OrgRoleId, tx)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating org user: %v\", err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error accepting invite: %v\", err)\n\t}\n\n\tinvite.InviteeId = &inviteeId\n","sourceCodeStart":104,"sourceCodeEnd":140,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/db/invite_helpers.go#L104-L140","documentation":"AcceptInvite runs inside a WithTx ('accept invite') transaction: it stamps accepted_at/invitee_id on the invite, then creates the org user. The wrapped 'error accepting invite' message is returned by the inner UPDATE when it fails, causing WithTx to roll back the whole acceptance. The invite is not marked accepted and no org membership is created.","triggerScenarios":"tx.Exec(\"UPDATE invites SET accepted_at = NOW(), invitee_id = $1 WHERE id = $2\") fails: invite.Id is empty/invalid (stale invite object, e.g. already-accepted row is fine for the UPDATE but empty Id breaks the WHERE binding), malformed inviteeId vs. column type, DB connection drop mid-transaction, or schema mismatch on invites.","commonSituations":"Race where two acceptance requests run concurrently and one hits serialization/lock timeouts, invite object constructed from client payload without fetching the row (so Id is blank), or DB failover during the transaction.","solutions":["Verify invite.Id is populated — fetch the invite via GetInvite/GetActiveInviteByEmail before calling AcceptInvite instead of trusting client-supplied data.","Validate inviteeId format (UUID) before invoking.","Retry idempotently: an already-accepted invite returns rows-affected 0, not an error; only genuine DB failures need handling — inspect the wrapped cause.","Check DB connectivity/pool health if errors are transient under concurrent accept load.","Apply migrations if the error mentions unknown columns; WithTx will have rolled back, so simply re-run after fixing."],"exampleFix":"// before: accepting a client-supplied invite object directly\nvar invite db.Invite\njson.NewDecoder(r.Body).Decode(&invite)\nerr := db.AcceptInvite(ctx, &invite, userId)\n\n// after: load the invite from the DB first\ninvite, err := db.GetInvite(inviteId)\nif err != nil || invite == nil {\n    http.Error(w, \"invite not found\", http.StatusNotFound)\n    return\n}\nerr = db.AcceptInvite(ctx, invite, userId)","handlingStrategy":"validation","validationCode":"invite, err := db.GetInvite(inviteId)\nif err != nil {\n    return fmt.Errorf(\"load invite: %w\", err)\n}\nif invite == nil {\n    return errors.New(\"invite not found\")\n}\nif _, err := uuid.Parse(inviteeId); err != nil {\n    return fmt.Errorf(\"invalid invitee id %q\", inviteeId)\n}","typeGuard":"func isAcceptableInvite(invite *db.Invite) bool {\n    return invite != nil && invite.Id != \"\" && invite.OrgId != \"\" && invite.OrgRoleId != \"\"\n}","tryCatchPattern":"if err := db.AcceptInvite(ctx, invite, inviteeId); err != nil {\n    var pgErr *pq.Error\n    if errors.As(err, &pgErr) && (pgErr.Code == \"40001\" || pgErr.Code == \"40P01\") {\n        // serialization failure / deadlock: safe to retry — WithTx rolled back\n        return retryAccept(ctx, invite, inviteeId)\n    }\n    return fmt.Errorf(\"accept invite: %w\", err)\n}","preventionTips":["Always load the invite from the DB before accepting; never trust client-supplied invite objects.","Validate inviteeId format before the transaction.","Make the accept flow idempotent so retries after rollback are safe.","Retry serialization/deadlock failures with backoff under concurrent acceptance.","Keep the transaction short — accept plus CreateOrgUser only — to reduce lock contention."],"tags":["database","sql","transaction","update","concurrency"],"backgroundTag":"database-update-failed","analyzedSha":"e2d772072efadbe41d2946d97d79be55532dbab5","analyzedAt":"2026-09-05T20:56:53.631Z","contentChangedAt":"2026-09-05T20:56:53.631Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}