signalapp/Signal-Server · error · InvalidCaptchaArgumentException

too few parts

Error message

too few parts

What it means

InvalidCaptchaArgumentException thrown by CaptchaChecker.verify when the captcha input string splits into fewer than 4 parts on the SEPARATOR. A valid input carries prefix, siteKey, action, and token segments; anything shorter cannot be parsed and is rejected before any challenge verification is attempted. The parse also normalizes the siteKey to lowercase and strips whitespace, expecting exact structural input.

Solutions

  1. Send the full 4-segment string: prefix + separator + siteKey + separator + action + separator + token
  2. Update old clients to the current captcha input format (4 parts)
  3. Validate the segment count client-side before calling the verification endpoint
  4. Log the received input segment count (never the token) to spot format drift

Example fix

// before
String input = rawChallengeToken; // 1 part
checker.verify(remoteAddr, input, userAgent); // too few parts
// after
String input = "signalcaptcha" + SEPARATOR + siteKey + SEPARATOR + action + SEPARATOR + rawChallengeToken;
checker.verify(remoteAddr, input, userAgent);
Defensive patterns

Strategy: validation

Validate before calling

String[] parts = captchaInput.split("\\" + SEPARATOR, 4);
if (parts.length < 4 || parts[3].isBlank()) {
  throw new IllegalArgumentException("captcha input must be prefix"
      + SEPARATOR + "siteKey" + SEPARATOR + "action" + SEPARATOR + "token");
}

Try / catch

try { checker.verify(ip, input, userAgent); }
catch (InvalidCaptchaArgumentException e) {
  respondCaptchaFormatError(e); // ask client to resend the full 4-part captcha string
}

Prevention

When it happens

Trigger: Calling verify with a captcha token string missing the prefix, site key, action, or token segment — e.g. passing only the raw hCaptcha/Turnstile response token, or a string where the separator character does not appear enough times.

Common situations: Clients submitting the challenge provider's token directly instead of the full prefixed captcha string the Signal client assembles; old clients producing a 3-part format when the server expects 4; copy-pasted tokens with segments stripped; misconfigured client templates dropping the action segment.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09). Data as JSON: /api/errors/e12eddff89f42d84. Report an issue: GitHub.

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/captcha/CaptchaChecker.java:69

   *                       expected format is {@code version-prefix.sitekey.action.token}
   * @param ip             IP of the solver
   * @param userAgent      User-Agent of the solver
   * @return An {@link AssessmentResult} indicating whether the solution should be accepted, and a score that can be
   * used for metrics
   * @throws IOException                     if there is an error validating the captcha with the underlying service
   * @throws InvalidCaptchaArgumentException if input is not in the expected format
   */
  public AssessmentResult verify(
      final Optional<UUID> maybeAci,
      final Action expectedAction,
      final String input,
      final String ip,
      @Nullable final String userAgent) throws IOException, InvalidCaptchaArgumentException {
    final String[] parts = input.split("\\" + SEPARATOR, 4);

    // we allow missing actions, if we're missing 1 part, assume it's the action
    if (parts.length < 4) {
      throw new InvalidCaptchaArgumentException("too few parts");
    }

    final String prefix = parts[0];
    final String siteKey = parts[1].toLowerCase(Locale.ROOT).strip();
    final String action = parts[2];
    String token = parts[3];

    String provider = prefix;
    if (prefix.endsWith(SHORT_SUFFIX)) {
      // This is a "short" solution that points to the actual solution. We need to fetch the
      // full solution before proceeding
      provider = prefix.substring(0, prefix.length() - SHORT_SUFFIX.length());
      token = shortCodeExpander.retrieve(token).orElseThrow(() -> new InvalidCaptchaArgumentException("invalid shortcode"));
    }

    final CaptchaClient client = this.captchaClientSupplier.apply(provider);
    if (client == null) {
      throw new InvalidCaptchaArgumentException("invalid captcha scheme");

View on GitHub (pinned to 100ab61c82)