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):
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):
_, 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
- Like a post (likeCount = 1, user like key exists in Redis).
- Fire two unlike requests for the same post/user nearly simultaneously (double-tap, retry, or two tabs).
- Both pass the
Exists check and both run the -1 decrement.
- 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
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):
SetNXis atomic andsetis 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):
The deleted-count from
Delis discarded (_), and the decrement runs onerr == nilalone.Delreturning0(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
Existscheck, both callDel, but only one actually removes the key. Both still seeerr == nil, so both decrementlikeCount(and the Redis ZSet). A single like gets decremented twice — the count drifts and can go negative.Steps to Reproduce
Existscheck and both run the-1decrement.Logs and Screenshots
Unlike path discards Del's deleted-count (like_controller.go ~line 62):
Suggested fix — gate on the deleted count, symmetric with the like path's
if set:Environment Details
if set, the unlike path does notImpact
Low - Minor inconvenience
Code of Conduct