signalapp/Signal-Server · error · InvalidCaptchaArgumentException
invalid captcha action
Error message
invalid captcha action
What it means
After parsing the captcha token's intended Action, verify compares it to the action expected for the endpoint being called. A mismatch — or an unparseable action string — increments the invalid-action counter and throws InvalidCaptchaArgumentException('invalid captcha action'). The captcha must be solved for the specific action the API request declares.
Solutions
- Solve the captcha for the exact action the endpoint requires and pass that same action string in the request.
- Update the client to the action names used by the current server API version.
- Do not reuse captcha tokens across different endpoints or flows.
- If server-side, confirm the expectedAction passed to verify() matches the endpoint's documented action.
Example fix
// before String action = "challenge"; // solved for registration // after String action = "registration"; // matches expectedAction for this endpoint
Defensive patterns
Strategy: validation
Validate before calling
if (!ACTIONS.includes(action)) throw new Error(`invalid captcha action: ${action}`);
if (action !== expectedActionForEndpoint) throw new Error('captcha action mismatch'); Type guard
const isValidAction = (a) => typeof a === 'string' && ['registration','challenge','recovery'].includes(a);
Try / catch
try { await call(captcha, action); } catch (e) { if (e.message.includes('invalid captcha action')) { return retryWithFreshCaptcha(expectedAction); } throw e; } Prevention
- Solve the captcha on the flow that matches the endpoint being called
- Never reuse captcha tokens across endpoints
- Track action-name changes between server versions
When it happens
Trigger: Calling an endpoint with expectedAction e.g. 'challenge' or 'registration' while the captcha token was solved for a different action, or passing a malformed/unrecognized action string to verify().
Common situations: Client solved a captcha on the wrong page/flow (e.g. recovery captcha reused for registration), API version changed action names and the client cached the old value, or a proxy/gateway forwards the wrong action parameter.
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.
Related errors
- too few parts
- invalid captcha scheme
- invalid captcha site-key
- 400 Bad Request
- Start timestamp not specified
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/cc5dc2fc390086d9.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/captcha/CaptchaChecker.java:98
// 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");
}
final AssessmentResult result = client.verify(maybeAci, siteKey, parsedAction, token, ip, userAgent);
Metrics.counter(ASSESSMENTS_COUNTER_NAME,
"action", action,
"score", result.getScoreString(),
"provider", provider)
.increment();
return result;
}
}View on GitHub (pinned to 100ab61c82)