Skip to content

Commit 95b7223

Browse files
fix: add retry logic for identity deletion + proper error responses
- Retry Ory identity deletion up to 3 times with backoff since DB rows are already gone at that point. - Return 404 when user not found (prepare or DB delete). - Return 409 when FK constraint prevents deletion (e.g. addons). - Return descriptive error when identity deletion fails after retries. Co-Authored-By: ben@e2b.dev <ben@e2b.dev>
1 parent 751beda commit 95b7223

1 file changed

Lines changed: 39 additions & 4 deletions

File tree

‎packages/dashboard-api/internal/handlers/admin_users_delete.go‎

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,75 @@
11
package handlers
22

33
import (
4+
"fmt"
45
"net/http"
6+
"time"
57

68
"github.com/gin-gonic/gin"
79
"go.uber.org/zap"
810

911
"github.com/e2b-dev/infra/packages/dashboard-api/internal/api"
12+
"github.com/e2b-dev/infra/packages/db/pkg/dberrors"
1013
"github.com/e2b-dev/infra/packages/shared/pkg/logger"
1114
)
1215

16+
const identityDeleteMaxRetries = 3
17+
1318
func (s *APIStore) DeleteAdminUsersUserId(c *gin.Context, userId api.UserId) {
1419
ctx := c.Request.Context()
1520

1621
// Resolve the external identity references while user_identities still exists.
1722
handle, err := s.userProfiles.PrepareDeleteUser(ctx, userId)
1823
if err != nil {
1924
logger.L().Error(ctx, "failed to prepare user deletion", zap.String("user_id", userId.String()), zap.Error(err))
20-
s.sendAPIStoreError(c, http.StatusInternalServerError, "Failed to prepare user deletion")
25+
s.sendAPIStoreError(c, http.StatusNotFound, fmt.Sprintf("User %s not found or has no identity provider record", userId))
2126

2227
return
2328
}
2429

2530
// Delete from public.users (cascades to user_identities via FK).
2631
// Done before the IdP removal so a DB failure does not orphan the identity.
2732
if err := s.authDB.Write.DeletePublicUser(ctx, userId); err != nil {
33+
if dberrors.IsNotFoundError(err) {
34+
s.sendAPIStoreError(c, http.StatusNotFound, fmt.Sprintf("User %s not found", userId))
35+
return
36+
}
37+
38+
if dberrors.IsForeignKeyViolation(err) {
39+
logger.L().Warn(ctx, "cannot delete user due to existing references", zap.String("user_id", userId.String()), zap.Error(err))
40+
s.sendAPIStoreError(c, http.StatusConflict, "Cannot delete user: existing references (e.g. addons) must be removed first")
41+
42+
return
43+
}
44+
2845
logger.L().Error(ctx, "failed to delete public user", zap.String("user_id", userId.String()), zap.Error(err))
2946
s.sendAPIStoreError(c, http.StatusInternalServerError, "Failed to delete public user record")
3047

3148
return
3249
}
3350

3451
// Remove the external identity (e.g. Ory) using pre-fetched references.
35-
if err := handle.Execute(ctx); err != nil {
36-
logger.L().Error(ctx, "failed to delete user identity provider record", zap.String("user_id", userId.String()), zap.Error(err))
37-
s.sendAPIStoreError(c, http.StatusInternalServerError, "Failed to delete user identity provider record")
52+
// Retry since the DB rows are already gone and we must not leave the IdP identity active.
53+
var identityErr error
54+
for attempt := range identityDeleteMaxRetries {
55+
identityErr = handle.Execute(ctx)
56+
if identityErr == nil {
57+
break
58+
}
59+
60+
logger.L().Warn(ctx, "retrying identity deletion",
61+
zap.String("user_id", userId.String()),
62+
zap.Int("attempt", attempt+1),
63+
zap.Error(identityErr),
64+
)
65+
66+
time.Sleep(time.Duration(attempt+1) * 200 * time.Millisecond)
67+
}
68+
69+
if identityErr != nil {
70+
logger.L().Error(ctx, "failed to delete user identity provider record after retries", zap.String("user_id", userId.String()), zap.Error(identityErr))
71+
s.sendAPIStoreError(c, http.StatusInternalServerError,
72+
"User DB records deleted but identity provider removal failed — the IdP identity may need manual cleanup")
3873

3974
return
4075
}

0 commit comments

Comments
 (0)