AOSSIE-Org / AOSSIE-Org/DebateAI
[BUG]: Unlike path can decrement like count multiple times per like — count drifts and can go negative
- Lenguaje dominante
- TypeScript
- Estrellas
- 84
- Forks
- 198
- Merge medio
- 2 d 19 h
- PR fusionados (30 d)
- 30
Descripción
### Bug Description
In `ToggleLikeHandler` (`backend/controllers/like_controller.go`), the **like** and **unlike** paths are asymmetric in how they guard the count update, and the unlike path has a race condition the like path doesn't.
**Like path (race-safe):**
```go
set, err := db.RedisClient.SetNX(ctx, userKey, "1", 0).Result()
...
if set { // only increment if THIS request actually created the key
db.RedisClient.ZIncrBy(ctx, key, 1, postID)
postCollection.UpdateOne(ctx, bson.M{"_id": postObjectID}, bson.M{"$inc": bson.M{"likeCount": 1}})
}
```
`SetNX` is atomic and `set` is true only if the key didn't already exist, so concurrent likes can't double-count. (The code even comments that this prevents the race.)
**Unlike path (NOT race-safe):**
```go
_, err = db.RedisClient.Del(ctx, userKey).Result() // deleted-count discarded
if err == nil { // Del returning 0 is not an error
db.RedisClient.ZIncrBy(ctx, key, -1, postID)
postCollection.UpdateOne(ctx, bson.M{"_id": postObjectID}, bson.M{"$inc": bson.M{"likeCount": -1}})
}
```
The deleted-count from `Del` is discarded (`_`), and the decrement runs on `err == nil` alone. `Del` returning `0` (key already gone) is **not** an error, so it still passes the check.
**Race:** two concurrent unlike requests for the same post/user both pass the earlier `Exists` check, both call `Del`, but only one actually removes the key. Both still see `err == nil`, so **both** decrement `likeCount` (and the Redis ZSet). A single like gets decremented twice — the count drifts and can go negative.
### Steps to Reproduce
1. Like a post (likeCount = 1, user like key exists in Redis).
2. Fire two unlike requests for the same post/user nearly simultaneously (double-tap, retry, or two tabs).
3. Both pass the `Exists` check and both run the `-1` decrement.
4. Observe likeCount ends at -1 instead of 0 (drifts negative / desyncs from actual like state).
### Logs and Screenshots
Unlike path discards Del's deleted-count (like_controller.go ~line 62):
_, err = db.RedisClient.Del(ctx, userKey).Result()
if err == nil {
// decrement runs even if 0 keys were deleted
}
Suggested fix — gate on the deleted count, symmetric with the like path's `if set`:
deleted, err := db.RedisClient.Del(ctx, userKey).Result()
if err == nil && deleted > 0 {
db.RedisClient.ZIncrBy(ctx, key, -1, postID)
postCollection.UpdateOne(ctx, bson.M{"_id": postObjectID}, bson.M{"$inc": bson.M{"likeCount": -1}})
}
### Environment Details
- File: backend/controllers/like_controller.go (ToggleLikeHandler, unlike branch)
- Backend: Go / Redis / MongoDB
- Branch: main
- Note: surfaces under concurrent or duplicate unlike requests (double-tap, retries, multiple tabs); the like path already guards against this via SetNX + `if set`, the unlike path does not
### Impact
Low - Minor inconvenience
### Code of Conduct
- [x] I have joined the [Discord server](https://discord.gg/hjUhu33uAn) and will post updates there
- [x] I have searched existing issues to avoid duplicates
Guía de contribución
No hay ninguna guía de contribución indexada para este repositorio
Evaluación
Este issue todavía no se ha evaluado.