spring-retry icon indicating copy to clipboard operation
spring-retry copied to clipboard

Support Retry-After HTTP header

Open tmysik opened this issue 7 years ago • 10 comments

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.

tmysik avatar Feb 06 '18 14:02 tmysik

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 avatar Feb 20 '18 13:02 dsyer

@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.

tmysik avatar Feb 20 '18 14:02 tmysik

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 avatar Feb 20 '18 15:02 dsyer

@dsyer

Thanks, will have a look at it.

tmysik avatar Feb 20 '18 15:02 tmysik

@tmysik

Hey, how it ended up?

bkrzyzanski avatar Jun 06 '19 17:06 bkrzyzanski

@bkrzyzanski

Thread.sleep()

tmysik avatar Jun 06 '19 17:06 tmysik

@tmysik

Did you do it in your custom Backoff policy?

bkrzyzanski avatar Jun 06 '19 17:06 bkrzyzanski

@bkrzyzanski

No.

tmysik avatar Jun 06 '19 17:06 tmysik

@tmysik

Okay, I guess your intention isn't to be helpful.

bkrzyzanski avatar Jun 06 '19 17:06 bkrzyzanski

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;
	}
}

julien-may avatar Dec 05 '19 13:12 julien-may