AOSSIE-Org / AOSSIE-Org/DebateAI

[BUG]: Unlike path can decrement like count multiple times per like — count drifts and can go negative

Open
#475 2 comments 0 reactions 1 assignee Claimed by @Ri1tik View on GitHub
bug
Dominant language
TypeScript
Stars
84
Forks
198
Avg merge
2d 19h
Merged PRs (30d)
30

Description

### 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

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.