{"record":{"id":"84ab78b3533dc991","repo":"signalapp/Signal-Server","slug":"return-response-status-429-build","errorCode":null,"errorMessage":"return Response.status(429).build();","messagePattern":"return Response\\.status\\(429\\)\\.build\\(\\);","errorType":"http","errorClass":null,"httpStatus":429,"severity":"warning","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/ChallengeController.java","lineNumber":193,"sourceCode":"  @ApiResponse(responseCode = \"404\", description = \"\"\"\n      The server does not have a push notification token for the authenticated account’s main device; clients may add a push\n      token and try again\n      \"\"\")\n  @ApiResponse(responseCode = \"413\", description = \"Too many attempts\", headers = @Header(\n      name = \"Retry-After\",\n      description = \"If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed\"))\n  @ApiResponse(responseCode = \"429\", description = \"Too many attempts\", headers = @Header(\n      name = \"Retry-After\",\n      description = \"If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed\"))\n  public Response requestPushChallenge(@Auth final AuthenticatedDevice auth,\n      @Context ContainerRequestContext requestContext) {\n\n    final Account account = accountsManager.getByAccountIdentifier(auth.accountIdentifier())\n        .orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED));\n\n    final ChallengeConstraints constraints = challengeConstraintChecker.challengeConstraintsHttp(requestContext, account);\n    if (!constraints.pushPermitted()) {\n      return Response.status(429).build();\n    }\n    try {\n      rateLimitChallengeManager.sendPushChallenge(account);\n      return Response.status(200).build();\n    } catch (final NotPushRegisteredException e) {\n      return Response.status(404).build();\n    }\n  }\n}\n","sourceCodeStart":175,"sourceCodeEnd":203,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ChallengeController.java#L175-L203","documentation":"The push-challenge endpoint (POST /v1/challenge/push) returns HTTP 429 when the challenge constraint check says push challenges are not permitted for this account. ChallengeConstraintChecker aggregates rate limits, CAPTCHA/registration-lock state, and abuse heuristics; pushPermitted()==false means the server has decided this account has requested too many push challenges or must satisfy another challenge first. It is a deliberate throttle, not a crash.","triggerScenarios":"Client calls requestPushChallenge for an account whose combined constraints (per-IP, per-account rate limits, unmet CAPTCHA requirements) yield pushPermitted()=false. Repeatedly requesting push challenges for the same account or IP within the rate-limit window triggers it.","commonSituations":"Mobile clients auto-retrying push-challenge requests after message-send failures; load tests or shared IPs (NAT, VPN) exhausting per-IP quotas; an account stuck in a state requiring CAPTCHA but the client never solving it and re-requesting pushes.","solutions":["Stop retrying and honor the 429; wait before the next push-challenge request","Check the rate-limit configuration (challenge rate limiter TTLs/limits) if legitimate traffic is throttled","Complete any alternative challenge (e.g. CAPTCHA) required before requesting push challenges again","Inspect ChallengeConstraintChecker wiring if constraints are unexpectedly false in a deployment"],"exampleFix":"// before: immediate retry on failure\nsendPushChallenge(account);\n// after: back off on 429\nif (response.code() == 429) {\n  long retryAfter = parseRetryAfter(response);\n  scheduler.schedule(this::sendPushChallenge, retryAfter, TimeUnit.SECONDS);\n}","handlingStrategy":"retry","validationCode":"// client-side: check local request history before calling\nif (lastPushChallengeAt != null && Duration.between(lastPushChallengeAt, Instant.now()).toMinutes() < COOLDOWN_MINUTES) {\n  return; // skip; server will 429 otherwise\n}","typeGuard":null,"tryCatchPattern":"// on 429, schedule a delayed retry instead of immediate resend\nif (response.code() == 429) {\n  backoffScheduler.schedule(this::requestPushChallenge, cooldown(), TimeUnit.SECONDS);\n}","preventionTips":["Rate-limit push-challenge requests locally on the client","Solve required CAPTCHAs before requesting push challenges","Avoid shared/flagged IPs when testing","Honor Retry-After-style backoff instead of tight retry loops"],"tags":["http","rate-limit","jaxrs","signal-server"],"backgroundTag":"rate-limit-exceeded","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"}