signalapp/Signal-Server · error · InvalidCaptchaArgumentException

invalid captcha scheme

Error message

invalid captcha scheme

What it means

CaptchaChecker.verify resolves a captcha provider from the token's prefix (e.g. 'signal-recaptcha:' or a short code) and looks up the registered CaptchaClient for that scheme. If no client is registered for the resolved provider, the token's scheme is not supported by this server configuration, so verify throws InvalidCaptchaArgumentException('invalid captcha scheme').

Solutions

  1. Add the matching captcha provider configuration (API keys/site keys for recaptcha or hcaptcha) to the server config so a CaptchaClient is registered for the token's scheme.
  2. Have the client fetch a fresh captcha token from the correct provider used by this server deployment.
  3. Check the token format: it must carry a recognized provider prefix (or a valid short code) before the ':' separator.
  4. Verify no environment mismatch: token issued by a staging server won't validate on prod.

Example fix

// before (client sends unsupported scheme)
captchaToken = "hcaptcha:" + hcaptchaResponse;
// after (match server-configured provider)
captchaToken = "signal-recaptcha:" + grecaptchaResponse;
Defensive patterns

Strategy: validation

Validate before calling

const scheme = captchaToken.slice(0, captchaToken.lastIndexOf(':'));
if (!SUPPORTED_SCHEMES.has(scheme)) throw new Error(`unsupported captcha scheme: ${scheme}`);

Type guard

const isSupportedScheme = (t) => typeof t === 'string' && SUPPORTED_SCHEMES.has(t.slice(0, t.lastIndexOf(':')));

Try / catch

try { await register(captchaToken); } catch (e) { if (e.message.includes('invalid captcha scheme')) { await refreshCaptchaFromSupportedProvider(); } else throw e; }

Prevention

When it happens

Trigger: POST /v1/registration or /v1/accounts with a captcha token whose provider prefix (text before the final ':') does not match any configured CaptchaClient (e.g. token prefixed 'hcaptcha:' when only reCAPTCHA is configured, or a token with no recognizable prefix).

Common situations: Server launched without captcha API keys in config (captcha config block missing), clients from a newer app version sending a captcha scheme the server doesn't support yet, tokens copied from a different environment (staging vs prod), or a region/proxy that issues short codes the expander can't resolve.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


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

Appendix: source

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

      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");
    }

    final Action parsedAction = Action.parse(action)
        .orElseThrow(() -> {
          Metrics.counter(INVALID_ACTION_COUNTER_NAME).increment();
          return new InvalidCaptchaArgumentException("invalid captcha action");
        });

    if (!parsedAction.equals(expectedAction)) {
      Metrics.counter(INVALID_ACTION_COUNTER_NAME, "action", action).increment();
      throw new InvalidCaptchaArgumentException("invalid captcha action");
    }

    final Set<String> allowedSiteKeys = client.validSiteKeys(parsedAction);
    if (!allowedSiteKeys.contains(siteKey)) {
      logger.debug("invalid site-key {}, action={}", siteKey, action);
      Metrics.counter(INVALID_SITEKEY_COUNTER_NAME, "action", action).increment();
      throw new InvalidCaptchaArgumentException("invalid captcha site-key");

View on GitHub (pinned to 100ab61c82)