signalapp/Signal-Server · warning

return Response.status(428).build();

Error message

return Response.status(428).build();

What it means

ChallengeController's captcha verification path returns HTTP 428 (Precondition Required) when the captcha token supplied by the client fails verification at the configured score threshold. It is not an exception but a deliberate guard response signaling the client must pass a valid challenge before proceeding.

Solutions

  1. Obtain and submit a fresh, valid captcha token for the request
  2. Check captcha service configuration (site key/secret) and that tokens match the site
  3. Lower captchaScoreThreshold in configuration if legitimate users are rejected
  4. Inspect captcha service logs/scores for the failing request to confirm cause

Example fix

// before
curl -X POST /v1/challenge -d '{"captcha":"stale-token"}'  -> 428
// after
request a new captcha, then
curl -X POST /v1/challenge -d '{"captcha":"fresh-valid-token"}'
Defensive patterns

Strategy: retry

Validate before calling

if (captchaToken == null || captchaToken.isBlank()) {
    throw new IllegalArgumentException("captcha token required");
}

Try / catch

if (response.getStatus() == 428) { requestNewCaptchaAndRetry(); }

Prevention

When it happens

Trigger: POSTing a challenge response with a captcha whose score (as evaluated by the captcha service against remoteAddress/userAgent and constraints.captchaScoreThreshold()) comes back success=false.

Common situations: Stale or already-used captcha tokens; bots/low-score traffic; user behind datacenter IP causing low score; captcha service outage or misconfigured site key/secret; score threshold set too aggressively high.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


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

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/ChallengeController.java:124

        if (!constraints.pushPermitted()) {
          return Response.status(429).build();
        }
        rateLimitChallengeManager.answerPushChallenge(account, pushChallengeRequest.getChallenge());
      } else if (answerRequest instanceof final AnswerCaptchaChallengeRequest captchaChallengeRequest) {
        tags = tags.and(CHALLENGE_TYPE_TAG, "captcha");

        final String remoteAddress = (String) requestContext.getProperty(
            RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
        final boolean success = rateLimitChallengeManager.answerCaptchaChallenge(
            account,
            captchaChallengeRequest.getCaptcha(),
            remoteAddress,
            userAgent,
            constraints.captchaScoreThreshold());

        if (!success) {
          return Response.status(428).build();
        }

      } else {
        tags = tags.and(CHALLENGE_TYPE_TAG, "unrecognized");
      }
    } catch (final InvalidCaptchaArgumentException e) {
      return Response.status(Response.Status.BAD_REQUEST.getStatusCode(), e.getMessage()).build();
    } catch (final IOException e) {
      logger.error("error assessing captcha during challenge response handling", e);
      return Response.status(Response.Status.SERVICE_UNAVAILABLE).build();
    } finally {
      Metrics.counter(CHALLENGE_RESPONSE_COUNTER_NAME, tags).increment();
    }

    return Response.status(200).build();
  }

  @POST

View on GitHub (pinned to 100ab61c82)