Support Retry-After HTTP header
It would be great if Spring Retry could support Retry-After HTTP header. Currently, this is not possible because one gets this header (and its value) from the response itself. In other words, it would be nice to have a way to set BackOff policy with a possibility to adjust it during retrying based on the value of the Retry-After header.
Interesting idea. Spring Retry has no dependency on HTTP though. So you would have to implement it as a RestTemplate interceptor (for example - same approach would work for other HTTP client libraries) that accesses the RetryContext via the RetrySynchronizationManager.
@dsyer
Sure, that makes sense. Do I understand it correctly that it should be possible even now? Or is there anything needed to be done in Spring Retry itself?
Thank you.
You might need to provide a custom BackoffPolicy or RetryPolicy (not sure). But I think it's doable with the retry context as it is already implemented.
@dsyer
Thanks, will have a look at it.
@tmysik
Hey, how it ended up?
@bkrzyzanski
Thread.sleep()
@tmysik
Did you do it in your custom Backoff policy?
@bkrzyzanski
No.
@tmysik
Okay, I guess your intention isn't to be helpful.
i had the same requirement and did a basic implementation for the Retry-After header to support seconds (a date is not supported). This is what i came up with:
@Slf4j
@RequiredArgsConstructor
public class HttpRetryAfterBackOffPolicy implements BackOffPolicy {
public static final long DEFAULT_BACK_OFF_PERIOD = 1000L;
@Setter
private long defaultBackOffPeriod = DEFAULT_BACK_OFF_PERIOD;
@Setter
private Sleeper sleeper = new ThreadWaitSleeper();
@Override
public BackOffContext start(RetryContext context) {
return new RetryAfterBackOffContext(context);
}
@Override
public void backOff(BackOffContext backOffContext) throws BackOffInterruptedException {
final RetryAfterBackOffContext context = (RetryAfterBackOffContext)backOffContext;
final Long backOffPeriod = tryGetBackOffPeriod(context.getRetryContext().getLastThrowable())
.orElse(this.defaultBackOffPeriod);
try {
sleeper.sleep(backOffPeriod);
} catch (InterruptedException e) {
throw new BackOffInterruptedException("Thread interrupted while sleeping", e);
}
}
private Optional<Long> tryGetBackOffPeriod(Throwable throwable) {
return throwable instanceof HttpClientErrorException.TooManyRequests
? tryGetRetryAfterHeaderValue((HttpClientErrorException.TooManyRequests)throwable)
.map(TimeUnit.SECONDS::toMillis)
: Optional.empty();
}
private Optional<Long> tryGetRetryAfterHeaderValue(HttpClientErrorException.TooManyRequests tooManyRequests) {
final HttpHeaders responseHeaders = tooManyRequests.getResponseHeaders();
if (responseHeaders != null) {
final List<String> values = responseHeaders.get(HttpHeaders.RETRY_AFTER);
if (values != null && !values.isEmpty()) {
try {
return Optional.of(Long.valueOf(values.get(0)));
} catch (NumberFormatException e) {
log.warn(e.getMessage(), e);
}
}
}
return Optional.empty();
}
@Data
@RequiredArgsConstructor
private static class RetryAfterBackOffContext implements BackOffContext {
private final RetryContext retryContext;
}
}