Add distinct JSON-RPC error code for captcha rejection (#2021)

* Add distinct JSON-RPC error code for captcha rejection

Previously submitRateLimitChallenge mapped CaptchaRejectedException to
the generic USER_ERROR code (-1), making it indistinguishable from any
other user error (bad params, unknown command, etc.).

Introduce CaptchaRejectedErrorException and wire it to a new error code
(-6 / CAPTCHA_REJECTED_ERROR) throughout the JSON-RPC layer. Callers can
now reliably distinguish a rejected captcha token (user must obtain a
fresh token) from a network failure (transient, worth retrying) or a
generic argument error.

The CLI exit code for this path becomes 6, consistent with the existing
per-error-type exit code convention.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add exit code 6 to man page

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
tonycpsu 2026-04-16 02:00:39 -04:00 committed by GitHub
parent 7e95ea7403
commit ddfad2c4ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 23 additions and 4 deletions

View File

@ -1168,6 +1168,7 @@ signal-cli -a ACCOUNT trust -a RECIPIENT
* *3*: Server or IO error
* *4*: Sending failed due to untrusted key
* *5*: Server rate limiting error
* *6*: CAPTCHA was rejected
== Files

View File

@ -22,6 +22,7 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.ArgumentParserException;
import net.sourceforge.argparse4j.inf.Namespace;
import org.asamk.signal.commands.exceptions.CaptchaRejectedErrorException;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.RateLimitErrorException;
@ -128,6 +129,7 @@ public class Main {
case IOErrorException ioErrorException -> 3;
case UntrustedKeyErrorException untrustedKeyErrorException -> 4;
case RateLimitErrorException rateLimitErrorException -> 5;
case CaptchaRejectedErrorException captchaRejectedErrorException -> 6;
case null -> 2;
};
}

View File

@ -3,9 +3,9 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.commands.exceptions.CaptchaRejectedErrorException;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.api.CaptchaRejectedException;
import org.asamk.signal.output.OutputWriter;
@ -41,8 +41,8 @@ public class SubmitRateLimitChallengeCommand implements JsonRpcLocalCommand {
} catch (IOException e) {
throw new IOErrorException("Submit challenge error: " + e.getMessage(), e);
} catch (CaptchaRejectedException e) {
throw new UserErrorException(
"Captcha rejected, it may be outdated, already used or solved from a different IP address.");
throw new CaptchaRejectedErrorException(
"Captcha rejected, it may be outdated, already used or solved from a different IP address.", e);
}
}
}

View File

@ -0,0 +1,10 @@
package org.asamk.signal.commands.exceptions;
import org.asamk.signal.manager.api.CaptchaRejectedException;
public final class CaptchaRejectedErrorException extends CommandException {
public CaptchaRejectedErrorException(final String message, final CaptchaRejectedException cause) {
super(message, cause);
}
}

View File

@ -1,6 +1,6 @@
package org.asamk.signal.commands.exceptions;
public sealed abstract class CommandException extends Exception permits IOErrorException, RateLimitErrorException, UnexpectedErrorException, UntrustedKeyErrorException, UserErrorException {
public sealed abstract class CommandException extends Exception permits CaptchaRejectedErrorException, IOErrorException, RateLimitErrorException, UnexpectedErrorException, UntrustedKeyErrorException, UserErrorException {
public CommandException(final String message) {
super(message);

View File

@ -12,6 +12,7 @@ import org.asamk.signal.commands.Command;
import org.asamk.signal.commands.JsonRpcMultiCommand;
import org.asamk.signal.commands.JsonRpcRegistrationCommand;
import org.asamk.signal.commands.JsonRpcSingleCommand;
import org.asamk.signal.commands.exceptions.CaptchaRejectedErrorException;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.RateLimitErrorException;
@ -39,6 +40,7 @@ public class SignalJsonRpcCommandHandler {
private static final int IO_ERROR = -3;
private static final int UNTRUSTED_KEY_ERROR = -4;
private static final int RATELIMIT_ERROR = -5;
private static final int CAPTCHA_REJECTED_ERROR = -6;
private final Manager m;
private final MultiAccountManager c;
@ -258,6 +260,10 @@ public class SignalJsonRpcCommandHandler {
case RateLimitErrorException e -> throw new JsonRpcException(new JsonRpcResponse.Error(RATELIMIT_ERROR,
e.getMessage(),
getErrorDataNode(objectMapper, result)));
case CaptchaRejectedErrorException e -> throw new JsonRpcException(new JsonRpcResponse.Error(
CAPTCHA_REJECTED_ERROR,
e.getMessage(),
getErrorDataNode(objectMapper, result)));
case UnexpectedErrorException e -> {
logger.error("Command execution failed with unexpected error", e);
throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INTERNAL_ERROR,