signalapp/Signal-Server · error · BadRequestException
400 Bad Request
Error message
400 Bad Request
What it means
handleCaptcha throws BadRequestException (HTTP 400) when captcha assessment fails with InvalidCaptchaArgumentException, meaning the captcha argument supplied by the client is malformed or unusable — e.g. missing, empty, or structurally invalid token rather than merely a low-scored one. The server rejects the request without consuming a verification attempt.
Solutions
- Ensure the captcha token is present and non-empty before calling updateSession; don't send the captcha field unless a token was obtained.
- Fetch a fresh token from the captcha widget/SDK and pass it verbatim (no trimming/truncation).
- Check you are using the captcha endpoint matching the service's configured site key so the token shape is accepted.
- Inspect the wrapped InvalidCaptchaArgumentException message for the exact argument problem.
Example fix
// before
updateSession(captcha: token ?? "")
// after
if (token != null && !token.isEmpty()) { updateSession(captcha: token); } Defensive patterns
Strategy: validation
Validate before calling
if (typeof token !== 'string' || token.length === 0) { throw new Error('captcha token required'); } Type guard
function hasCaptchaToken(args) { return typeof args.captcha === 'string' && args.captcha.length > 0; } Try / catch
try { await updateSession(...); } catch (e) {
if (e.status === 400) { const t = await fetchNewCaptchaToken(); return updateSession({captcha: t}); }
throw e;
} Prevention
- Never send an empty captcha field
- Pass the token verbatim without truncation
- Only include the captcha field when a token was actually obtained
When it happens
Trigger: updateSession called with a captcha field that is null/empty/blank or otherwise fails the captcha client's argument validation before any remote assessment (InvalidCaptchaArgumentException from the captcha client).
Common situations: Client sending an empty captcha header when no captcha was actually solved; truncating a long captcha token; sending the token in the wrong field/header; frontend SDK failing silently and returning an empty token.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- 400 Bad Request (INVALID_ARGUMENTS)
- too few parts
- invalid captcha scheme
- invalid captcha action
- invalid captcha site-key
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/2cc82ba786d88fc6.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java:484
Metrics.counter(CAPTCHA_ATTEMPT_COUNTER_NAME, Tags.of(
Tag.of(SUCCESS_TAG_NAME, String.valueOf(assessmentResult.isValid(captchaScoreThreshold))),
UserAgentTagUtil.getPlatformTag(userAgent),
Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(registrationServiceSession.number())),
Tag.of(REGION_CODE_TAG_NAME, Util.getRegion(registrationServiceSession.number())),
Tag.of(SCORE_TAG_NAME, assessmentResult.getScoreString())))
.increment();
CaptchaMetrics.measureCaptchaOutcome(assessmentResult.getNormalizedIntScore(),
assessmentResult.isValid(captchaScoreThreshold),
Util.getRegion(registrationServiceSession.number()),
"verification");
} catch (final IOException e) {
logger.error("error assessing captcha during registration verification", e);
throw new ServerErrorException(Response.Status.SERVICE_UNAVAILABLE, e);
} catch (InvalidCaptchaArgumentException e) {
throw new BadRequestException(e);
}
if (assessmentResult.isValid(captchaScoreThreshold)) {
final List<VerificationSession.Information> submittedInformation = new ArrayList<>(
verificationSession.submittedInformation());
submittedInformation.add(VerificationSession.Information.CAPTCHA);
final List<VerificationSession.Information> requestedInformation = new ArrayList<>(
verificationSession.requestedInformation());
// a captcha satisfies a push challenge, in case of push deliverability issues
requestedInformation.remove(VerificationSession.Information.PUSH_CHALLENGE);
final boolean allowedToRequestCode = (verificationSession.allowedToRequestCode()
|| requestedInformation.remove(VerificationSession.Information.CAPTCHA))
&& requestedInformation.isEmpty();
verificationSession = new VerificationSession(verificationSession.sessionId(),
verificationSession.pushChallenge(),
verificationSession.carrierData(),View on GitHub (pinned to 100ab61c82)