Return a Retry-After on rate-limited responses

Previously, only endpoints throwing a RetryLaterException would include
a Retry-After header in the 413 response. Now, by default, all
RateLimitExceededExceptions will be marshalled into a 413 with a
Retry-After included if possible.
This commit is contained in:
Ravi Khadiwala
2022-02-16 14:34:25 -06:00
committed by ravi-signal
parent 43792e2426
commit ae3a5c5f5e
11 changed files with 103 additions and 77 deletions

View File

@@ -213,7 +213,7 @@ public class AccountController {
@QueryParam("client") Optional<String> client,
@QueryParam("captcha") Optional<String> captcha,
@QueryParam("challenge") Optional<String> pushChallenge)
throws RateLimitExceededException, RetryLaterException, ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException {
throws RateLimitExceededException, ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException {
Util.requireNormalizedNumber(number);
@@ -234,24 +234,16 @@ public class AccountController {
return Response.status(402).build();
}
try {
switch (transport) {
case "sms":
rateLimiters.getSmsDestinationLimiter().validate(number);
break;
case "voice":
rateLimiters.getVoiceDestinationLimiter().validate(number);
rateLimiters.getVoiceDestinationDailyLimiter().validate(number);
break;
default:
throw new WebApplicationException(Response.status(422).build());
}
} catch (RateLimitExceededException e) {
if (!e.getRetryDuration().isNegative()) {
throw new RetryLaterException(e);
} else {
throw e;
}
switch (transport) {
case "sms":
rateLimiters.getSmsDestinationLimiter().validate(number);
break;
case "voice":
rateLimiters.getVoiceDestinationLimiter().validate(number);
rateLimiters.getVoiceDestinationDailyLimiter().validate(number);
break;
default:
throw new WebApplicationException(Response.status(422).build());
}
VerificationCode verificationCode = generateVerificationCode(number);
@@ -643,7 +635,9 @@ public class AccountController {
}
final String mostRecentProxy = ForwardedIpUtil.getMostRecentProxy(forwardedFor)
.orElseThrow(() -> new RateLimitExceededException(Duration.ofHours(1)));
// Missing/malformed Forwarded-For, cannot calculate a reasonable backoff
// duration
.orElseThrow(() -> new RateLimitExceededException(Duration.ofHours(-1)));
rateLimiters.getCheckAccountExistenceLimiter().validate(mostRecentProxy);

View File

@@ -51,7 +51,7 @@ public class ChallengeController {
public Response handleChallengeResponse(@Auth final AuthenticatedAccount auth,
@Valid final AnswerChallengeRequest answerRequest,
@HeaderParam("X-Forwarded-For") final String forwardedFor,
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent) throws RetryLaterException {
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent) throws RateLimitExceededException {
Tags tags = Tags.of(UserAgentTagUtil.getPlatformTag(userAgent));
@@ -76,8 +76,6 @@ public class ChallengeController {
} else {
tags = tags.and(CHALLENGE_TYPE_TAG, "unrecognized");
}
} catch (final RateLimitExceededException e) {
throw new RetryLaterException(e);
} finally {
Metrics.counter(CHALLENGE_RESPONSE_COUNTER_NAME, tags).increment();
}

View File

@@ -5,10 +5,11 @@
package org.whispersystems.textsecuregcm.controllers;
import java.time.Duration;
import java.util.Optional;
public class RateLimitExceededException extends Exception {
private final Duration retryDuration;
private final Optional<Duration> retryDuration;
public RateLimitExceededException(final Duration retryDuration) {
this(null, retryDuration);
@@ -16,8 +17,9 @@ public class RateLimitExceededException extends Exception {
public RateLimitExceededException(final String message, final Duration retryDuration) {
super(message, null, true, false);
this.retryDuration = retryDuration;
// we won't provide a backoff in the case the duration is negative
this.retryDuration = retryDuration.isNegative() ? Optional.empty() : Optional.of(retryDuration);
}
public Duration getRetryDuration() { return retryDuration; }
public Optional<Duration> getRetryDuration() { return retryDuration; }
}

View File

@@ -1,19 +0,0 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import java.time.Duration;
public class RetryLaterException extends Exception {
private final Duration backoffDuration;
public RetryLaterException(RateLimitExceededException e) {
super(null, e, true, false);
this.backoffDuration = e.getRetryDuration();
}
public Duration getBackoffDuration() { return backoffDuration; }
}