Repository: valkey-io/valkey (2025-06-30T08:46:07Z)

Issue:
[BUG] missing response when AUTH is errored inside a transaction
When we introduced module based authentication, we provided a way for the module to own the authentication and fail it in case needed.
for this we do not write error reply in case the auth identify there are existing replies in the client reply list:

```
/* If `err` is provided, this is added as an error reply to the client.
 * Otherwise, the standard Auth error is added as a reply. */
void addAuthErrReply(client *c, robj *err) {
    if (clientHasPendingReplies(c)) return; <---------- here
    if (!err) {
        addReplyError(c, "-WRONGPASS invalid username-password pair or user is disabled.");
        return;
    }
    addReplyError(c, err->ptr);
}
```

However this introduces an issue as the auth might be used inside a transaction or pipeline which already has some pending replies in the client reply list. 

example using valkey-cli:

> multi
> auth no-user 123
> exec

Credits to @uriyage for locating this


Discussion:
so module auth can add reply in its callback, or not add reply and just return an err robj in its callback? And we have to figure a way to determine whether it has added a reply?
> so module auth can add reply in its callback, or not add reply and just return an err robj in its callback? And we have to figure a way to determine whether it has added a reply?

IIUC yes. maybe we can count on the deferred_reply_errors, but I am not sure it will cover all cases.
deferred_reply_errors wont work since it is only used for module command i guess, in this auth command it does not trigger it.

Another ugly way is, check and see if `c->bufpos` and `listLength(c->reply)` changed before and after, if the value is changed that means the auth callback already add the reply...
```diff
diff --git a/src/acl.c b/src/acl.c
index c33b5d1ed..23e0737ae 100644
--- a/src/acl.c
+++ b/src/acl.c
@@ -1439,7 +1439,6 @@ int ACLCheckUserCredentials(robj *username, robj *password) {
 /* If `err` is provided, this is added as an error reply to the client.
  * Otherwise, the standard Auth error is added as a reply. */
 void addAuthErrReply(client *c, robj *err) {
-    if (clientHasPendingReplies(c)) return;
     if (!err) {
         addReplyError(c, "-WRONGPASS invalid username-password pair or user is disabled.");
         return;
@@ -3158,11 +3157,21 @@ void authCommand(client *c) {
     }
 
     robj *err = NULL;
+
+    size_t old_bufpos = c->bufpos;
+    unsigned long old_reply_length = listLength(c->reply);
     int result = ACLAuthenticateUser(c, username, password, &err);
+    size_t new_bufpos = c->bufpos;
+    unsigned long new_reply_length = listLength(c->reply);
+
     if (result == AUTH_OK) {
         addReply(c, shared.ok);
     } else if (result == AUTH_ERR) {
-        addAuthErrReply(c, err);
+        if (old_bufpos == new_bufpos && old_reply_length == new_reply_length) {
+            /* COB does not changed at all, this mean module auth callback does
+             * not added any reply. */
+            addAuthErrReply(c, err);
+        }
     }
     if (err) decrRefCount(err);
 }

```



Patch:
diff --git a/src/acl.c b/src/acl.c
index c33b5d1ed7..87a86724cf 100644
--- a/src/acl.c
+++ b/src/acl.c
@@ -1439,7 +1439,12 @@ int ACLCheckUserCredentials(robj *username, robj *password) {
 /* If `err` is provided, this is added as an error reply to the client.
  * Otherwise, the standard Auth error is added as a reply. */
 void addAuthErrReply(client *c, robj *err) {
-    if (clientHasPendingReplies(c)) return;
+    /* Note that a module auth can add reply in its callback, or not
+     * add reply and just return an error robj in its callback. So in
+     * here, we use buffered_reply flag to determine if auth command
+     * has already had a reply added. */
+    if (c->flag.buffered_reply) return;
+
     if (!err) {
         addReplyError(c, "-WRONGPASS invalid username-password pair or user is disabled.");
         return;


Test patch:
diff --git a/tests/unit/multi.tcl b/tests/unit/multi.tcl
index 3a279840b7..f54ec5e837 100644
--- a/tests/unit/multi.tcl
+++ b/tests/unit/multi.tcl
@@ -955,6 +955,12 @@ start_server {tags {"multi"}} {
         $rd close
     }
 
+    test "AUTH errored inside MULTI will add the reply" {
+        r config set requirepass ""
+        r multi
+        r auth no-user foobar
+        assert_error {WRONGPASS invalid username-password pair or user is disabled.} {r exec}
+    }
 }
 
 start_server {overrides {appendonly {yes} appendfilename {appendonly.aof} appendfsync always} tags {external:skip}} {