RetryStatusCodes.java
package com.maybeitssquid.retry;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.function.Predicate;
/**
* Predicate to decide whether a retry is allowable based on the HTTP response code. By default,
* retries are allowed for HTTP response codes in the 1xx and 3xx ranges, as shown in the following
* table.
*
* <table>
* <caption>Default retry behavior</caption>
* <thead>
* <tr><th>Status</th><th>Idempotent</th><th>Non-Idempotent</th></tr>
* </thead>
* <tbody>
* <tr><td>1xx</td><td>true</td><td>true</td></tr>
* <tr><td>2xx</td><td>false</td><td>false</td></tr>
* <tr><td>3xx</td><td>true</td><td>true</td></tr>
* <tr><td>4xx</td><td>false</td><td>false</td></tr>
* <tr><td>{@link HttpServletResponse#SC_REQUEST_TIMEOUT 408}</td><td>true</td><td>true</td></tr>
* <tr><td>{@link HttpServletResponse#SC_CONFLICT 409}</td><td>true</td><td>true</td></tr>
* <tr><td>429</td><td>true</td><td>true</td></tr>
* <tr><td>5xx</td><td>true</td><td><strong>false</strong></td></tr>
* <tr><td>{@link HttpServletResponse#SC_NOT_IMPLEMENTED 501}</td><td>false</td><td>false</td></tr>
* <tr><td>{@link HttpServletResponse#SC_HTTP_VERSION_NOT_SUPPORTED 505}</td><td>false</td><td>false</td></tr>
* </tbody>
* </table>
*/
public class RetryStatusCodes implements Predicate<HttpServletResponse> {
/** The HTTP status code for a retry being too early. */
public static final int SC_TOO_EARLY = 425;
/** The HTTP status code for a server complaining of too many requests. */
public static final int SC_TOO_MANY_REQUESTS = 429;
/** The number of valid status codes */
private static final int CODES = 500;
/** Offset applied to responses table entry because 0xx status codes are unused. */
private static final int OFFSET = 100;
/**
* Default decisions for non-idempotent retry. Note that index is {@link #OFFSET} from status
* codes.
*/
private static final boolean[] IDEMPOTENT_DEFAULTS = new boolean[CODES];
/**
* Default decisions for non-idempotent retry. Note that index is {@link #OFFSET} from status
* codes.
*/
private static final boolean[] NON_IDEMPOTENT_DEFAULTS = new boolean[CODES];
static {
// 1xx are incomplete results, so the “retry” is to continue processing
Arrays.fill(NON_IDEMPOTENT_DEFAULTS, 100 - OFFSET, 199 - OFFSET, true);
// 3xx are redirections, so the “retry” is to follow the redirection to a new target
Arrays.fill(NON_IDEMPOTENT_DEFAULTS, 300 - OFFSET, 399 - OFFSET, true);
// 4xx is client error, but there a few where retry should be safe
// 408 is request timeout, server confirming it did not receive the request so OK to retry
NON_IDEMPOTENT_DEFAULTS[HttpServletResponse.SC_REQUEST_TIMEOUT - OFFSET] = true;
// 409 is conflict in resource state, it may resolve upon retry
NON_IDEMPOTENT_DEFAULTS[HttpServletResponse.SC_CONFLICT - OFFSET] = true;
// 425 is due to risk of replay of data during TLS negotiation, expect server to ensure safe
// retry
NON_IDEMPOTENT_DEFAULTS[SC_TOO_EARLY - OFFSET] = true;
// 429 is server-managed throttling of the client, expect server to ensure safe retry
NON_IDEMPOTENT_DEFAULTS[SC_TOO_MANY_REQUESTS - OFFSET] = true;
System.arraycopy(NON_IDEMPOTENT_DEFAULTS, 0, IDEMPOTENT_DEFAULTS, 0, CODES);
// 5xx codes are retried, with exceptions
Arrays.fill(IDEMPOTENT_DEFAULTS, 500 - OFFSET, 599 - OFFSET, true);
IDEMPOTENT_DEFAULTS[HttpServletResponse.SC_NOT_IMPLEMENTED - OFFSET] = false;
IDEMPOTENT_DEFAULTS[HttpServletResponse.SC_HTTP_VERSION_NOT_SUPPORTED - OFFSET] = false;
}
/** Decisions for retry */
private final boolean[] responses;
/**
* Creates a predicate to decide whether a retry is allowable based on the HTTP response code.
*
* @param idempotent whether the service is idempotent. If it is not idempotent, server errors are
* not retried unless explicitly allowed by the {@code retry} parameter.
* @param additional status codes that are expressly allowed for retry. Codes in this list
* override defaults.
* @throws ArrayIndexOutOfBoundsException if any of the additional status codes are out of the
* range 100..599
*/
private RetryStatusCodes(final boolean idempotent, final int... additional) {
if (additional == null || additional.length == 0) {
this.responses = idempotent ? IDEMPOTENT_DEFAULTS : NON_IDEMPOTENT_DEFAULTS;
} else {
this.responses =
Arrays.copyOf(idempotent ? IDEMPOTENT_DEFAULTS : NON_IDEMPOTENT_DEFAULTS, CODES);
for (int r : additional) {
this.responses[r - OFFSET] = true;
}
}
}
/**
* Create an instance that retries only specific codes.
*
* @param only HTTP status codes that should be retried.
* @throws ArrayIndexOutOfBoundsException if any of the retry status codes are out of the range
* 100..599
*/
private RetryStatusCodes(final int... only) {
this.responses = new boolean[CODES];
for (int r : only) {
this.responses[r - OFFSET] = true;
}
}
/**
* Returns a predicate with default decisions for an idempotent service. Idempotent services allow
* retries of 5xx HTTP status codes except for {@link HttpServletResponse#SC_NOT_IMPLEMENTED} and
* {@link HttpServletResponse#SC_HTTP_VERSION_NOT_SUPPORTED}.
*
* @param additional status codes that are expressly allowed for retry. Codes in this list
* override defaults.
* @return predicate with decisions for an idempotent service.
*/
public static RetryStatusCodes idempotent(final int... additional) {
return new RetryStatusCodes(true, additional);
}
/**
* Returns a predicate with default decisions for a non-idempotent service. Non-idempotent
* services do not allow retries of 5xx HTTP status codes.
*
* @param additional status codes that are expressly allowed for retry. Codes in this list
* override defaults.
* @return predicate with decisions for a non-idempotent service.
*/
public static RetryStatusCodes nonIdempotent(final int... additional) {
return new RetryStatusCodes(false, additional);
}
/**
* Returns a predicate that allows retry for only the explicitly provided status codes.
*
* @param retry status codes that are expressly allowed for retry.
* @return predicate that returns {@code true} for the provided response codes
*/
public static RetryStatusCodes only(final int... retry) {
return new RetryStatusCodes(retry);
}
/**
* Returns whether the status code allows a retry.
*
* @param code the status code to check.
* @return whether the status code allows a retry.
*/
public boolean retries(final int code) {
final int index = code - OFFSET;
return index >= 0 && index < this.responses.length && this.responses[index];
}
/**
* Tests whether the HTTP response status allows for a retry.
*
* @param t the HTTP response
* @return whether a retry is allowed based on the status code
*/
@Override
public boolean test(final HttpServletResponse t) {
return retries(t.getStatus());
}
}