Subject: [PATCH] Send CAP END on RPL_SASLSUCCESS

From: [email-masked]
Date: Mon, 3 Aug 2026 12:37:19 +0200

From: delthas <[email-masked]>

soju doesn't send RPL_LOGGEDIN on successful authentication, reserving it
to mirror the upstream connection status[1], so waiting for it means
registration never completes. RPL_SASLSUCCESS is what ends the SASL
exchange; RPL_LOGGEDIN only means an account name was set, by SASL or
otherwise[2], and can arrive long after registration. Only send CAP END
the first time.

Also treat ERR_NICKLOCKED as a SASL failure, which otherwise hangs the
same way.

[1]: https://github.com/ircv3/ircv3-specifications/pull/476
[2]: https://ircv3.net/specs/extensions/sasl-3.1
---
 handle.c | 20 ++++++++++++++------
 1 file changed, 14 insertions(+), 6 deletions(-)

diff --git a/handle.c b/handle.c
index 0b01de6..fe7b462 100644
--- a/handle.c
+++ b/handle.c
@@ -167,6 +167,13 @@ static void handleErrorErroneousNickname(struct Message *msg) {
 	}
 }
 
+static void capEnd(void) {
+	static bool sent;
+	if (sent) return;
+	sent = true;
+	ircFormat("CAP END\r\n");
+}
+
 static void handleCap(struct Message *msg) {
 	require(msg, false, 3);
 	enum Cap caps = capParse(msg->params[2]);
@@ -181,7 +188,7 @@ static void handleCap(struct Message *msg) {
 			capList(buf, sizeof(buf), caps);
 			ircFormat("CAP REQ :%s\r\n", buf);
 		} else {
-			if (!(self.caps & CapSASL)) ircFormat("CAP END\r\n");
+			if (!(self.caps & CapSASL)) capEnd();
 		}
 	} else if (!strcmp(msg->params[1], "ACK")) {
 		self.caps |= caps;
@@ -190,7 +197,7 @@ static void handleCap(struct Message *msg) {
 				"AUTHENTICATE %s\r\n", (self.plainUser ? "PLAIN" : "EXTERNAL")
 			);
 		}
-		if (!(self.caps & CapSASL)) ircFormat("CAP END\r\n");
+		if (!(self.caps & CapSASL)) capEnd();
 	} else if (!strcmp(msg->params[1], "NAK")) {
 		errx(1, "server does not support %s", msg->params[2]);
 	}
@@ -251,9 +258,8 @@ static void handleAuthenticate(struct Message *msg) {
 	explicit_bzero(self.plainPass, strlen(self.plainPass));
 }
 
-static void handleReplyLoggedIn(struct Message *msg) {
-	(void)msg;
-	ircFormat("CAP END\r\n");
+static void handleReplySASLSuccess(struct Message *msg) {
+	capEnd();
 	handleReplyGeneric(msg);
 }
 
@@ -1390,7 +1396,9 @@ static const struct Handler {
 	{ "704", +ReplyHelp, handleReplyHelp },
 	{ "705", +ReplyHelp, handleReplyHelp },
 	{ "706", -ReplyHelp, NULL },
-	{ "900", 0, handleReplyLoggedIn },
+	{ "900", 0, handleReplySASLSuccess },
+	{ "902", 0, handleErrorSASLFail },
+	{ "903", 0, handleReplySASLSuccess },
 	{ "904", 0, handleErrorSASLFail },
 	{ "905", 0, handleErrorSASLFail },
 	{ "906", 0, handleErrorSASLFail },

base-commit: 38dc6a301914efd34f9a57cf75e4d0dae8a529fc
-- 
2.55.0