{"record":{"id":"a50ca2bcff56b150","repo":"signalapp/Signal-Server","slug":"invalid-captcha-scheme","errorCode":null,"errorMessage":"invalid captcha scheme","messagePattern":"invalid captcha scheme","errorType":"validation","errorClass":"InvalidCaptchaArgumentException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/captcha/CaptchaChecker.java","lineNumber":87,"sourceCode":"      throw new InvalidCaptchaArgumentException(\"too few parts\");\n    }\n\n    final String prefix = parts[0];\n    final String siteKey = parts[1].toLowerCase(Locale.ROOT).strip();\n    final String action = parts[2];\n    String token = parts[3];\n\n    String provider = prefix;\n    if (prefix.endsWith(SHORT_SUFFIX)) {\n      // This is a \"short\" solution that points to the actual solution. We need to fetch the\n      // full solution before proceeding\n      provider = prefix.substring(0, prefix.length() - SHORT_SUFFIX.length());\n      token = shortCodeExpander.retrieve(token).orElseThrow(() -> new InvalidCaptchaArgumentException(\"invalid shortcode\"));\n    }\n\n    final CaptchaClient client = this.captchaClientSupplier.apply(provider);\n    if (client == null) {\n      throw new InvalidCaptchaArgumentException(\"invalid captcha scheme\");\n    }\n\n    final Action parsedAction = Action.parse(action)\n        .orElseThrow(() -> {\n          Metrics.counter(INVALID_ACTION_COUNTER_NAME).increment();\n          return new InvalidCaptchaArgumentException(\"invalid captcha action\");\n        });\n\n    if (!parsedAction.equals(expectedAction)) {\n      Metrics.counter(INVALID_ACTION_COUNTER_NAME, \"action\", action).increment();\n      throw new InvalidCaptchaArgumentException(\"invalid captcha action\");\n    }\n\n    final Set<String> allowedSiteKeys = client.validSiteKeys(parsedAction);\n    if (!allowedSiteKeys.contains(siteKey)) {\n      logger.debug(\"invalid site-key {}, action={}\", siteKey, action);\n      Metrics.counter(INVALID_SITEKEY_COUNTER_NAME, \"action\", action).increment();\n      throw new InvalidCaptchaArgumentException(\"invalid captcha site-key\");","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/captcha/CaptchaChecker.java#L69-L105","documentation":"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').","triggerScenarios":"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).","commonSituations":"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.","solutions":["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.","Have the client fetch a fresh captcha token from the correct provider used by this server deployment.","Check the token format: it must carry a recognized provider prefix (or a valid short code) before the ':' separator.","Verify no environment mismatch: token issued by a staging server won't validate on prod."],"exampleFix":"// before (client sends unsupported scheme)\ncaptchaToken = \"hcaptcha:\" + hcaptchaResponse;\n// after (match server-configured provider)\ncaptchaToken = \"signal-recaptcha:\" + grecaptchaResponse;","handlingStrategy":"validation","validationCode":"const scheme = captchaToken.slice(0, captchaToken.lastIndexOf(':'));\nif (!SUPPORTED_SCHEMES.has(scheme)) throw new Error(`unsupported captcha scheme: ${scheme}`);","typeGuard":"const isSupportedScheme = (t) => typeof t === 'string' && SUPPORTED_SCHEMES.has(t.slice(0, t.lastIndexOf(':')));","tryCatchPattern":"try { await register(captchaToken); } catch (e) { if (e.message.includes('invalid captcha scheme')) { await refreshCaptchaFromSupportedProvider(); } else throw e; }","preventionTips":["Always fetch captcha tokens from the provider configured on the server you target","Keep the provider prefix on the token intact when passing it to the API","Align captcha config across environments before deploying clients"],"tags":["captcha","configuration","validation"],"backgroundTag":"invalid-config-value","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}