{"record":{"id":"e12eddff89f42d84","repo":"signalapp/Signal-Server","slug":"too-few-parts","errorCode":null,"errorMessage":"too few parts","messagePattern":"too few parts","errorType":"validation","errorClass":"InvalidCaptchaArgumentException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/captcha/CaptchaChecker.java","lineNumber":69,"sourceCode":"   *                       expected format is {@code version-prefix.sitekey.action.token}\n   * @param ip             IP of the solver\n   * @param userAgent      User-Agent of the solver\n   * @return An {@link AssessmentResult} indicating whether the solution should be accepted, and a score that can be\n   * used for metrics\n   * @throws IOException                     if there is an error validating the captcha with the underlying service\n   * @throws InvalidCaptchaArgumentException if input is not in the expected format\n   */\n  public AssessmentResult verify(\n      final Optional<UUID> maybeAci,\n      final Action expectedAction,\n      final String input,\n      final String ip,\n      @Nullable final String userAgent) throws IOException, InvalidCaptchaArgumentException {\n    final String[] parts = input.split(\"\\\\\" + SEPARATOR, 4);\n\n    // we allow missing actions, if we're missing 1 part, assume it's the action\n    if (parts.length < 4) {\n      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\");","sourceCodeStart":51,"sourceCodeEnd":87,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/captcha/CaptchaChecker.java#L51-L87","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Send the full 4-segment string: prefix + separator + siteKey + separator + action + separator + token","Update old clients to the current captcha input format (4 parts)","Validate the segment count client-side before calling the verification endpoint","Log the received input segment count (never the token) to spot format drift"],"exampleFix":"// before\nString input = rawChallengeToken; // 1 part\nchecker.verify(remoteAddr, input, userAgent); // too few parts\n// after\nString input = \"signalcaptcha\" + SEPARATOR + siteKey + SEPARATOR + action + SEPARATOR + rawChallengeToken;\nchecker.verify(remoteAddr, input, userAgent);","handlingStrategy":"validation","validationCode":"String[] parts = captchaInput.split(\"\\\\\" + SEPARATOR, 4);\nif (parts.length < 4 || parts[3].isBlank()) {\n  throw new IllegalArgumentException(\"captcha input must be prefix\"\n      + SEPARATOR + \"siteKey\" + SEPARATOR + \"action\" + SEPARATOR + \"token\");\n}","typeGuard":null,"tryCatchPattern":"try { checker.verify(ip, input, userAgent); }\ncatch (InvalidCaptchaArgumentException e) {\n  respondCaptchaFormatError(e); // ask client to resend the full 4-part captcha string\n}","preventionTips":["Always send the full prefixed captcha string, never the raw provider token","Count separator-delimited segments client-side before submission","Keep client captcha format templates in sync with server expectations","Strip nothing from the token segment when assembling the input"],"tags":["captcha","input-format","validation"],"backgroundTag":"invalid-argument-format","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"}