elastic/elasticsearch · error · IllegalArgumentException
maxAttempts must be >= 1 but was [{}]
Error message
maxAttempts must be >= 1 but was [{}] What it means
HttpUtils.readHttpBytesWithRetry validates its maxAttempts argument up front. The retry loop iterates attempt = 1..maxAttempts, so maxAttempts <= 0 would mean zero attempts and silent fall-through; the guard rejects non-positive values with the actual invalid value embedded.
Source
Thrown at build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/util/HttpUtils.java:33
public final class HttpUtils {
private static final int HTTP_READ_MAX_ATTEMPTS = 3;
private static final long HTTP_READ_RETRY_BACKOFF_MILLIS = 1000L;
private HttpUtils() {}
@FunctionalInterface
public interface Sleeper {
void sleep(long millis) throws InterruptedException;
}
public static byte[] readHttpBytesWithRetry(String url) throws IOException {
return readHttpBytesWithRetry(url, HTTP_READ_MAX_ATTEMPTS, HTTP_READ_RETRY_BACKOFF_MILLIS, Thread::sleep);
}
public static byte[] readHttpBytesWithRetry(String url, int maxAttempts, long baseBackoffMillis, Sleeper sleeper) throws IOException {
if (maxAttempts <= 0) {
throw new IllegalArgumentException("maxAttempts must be >= 1 but was [" + maxAttempts + "]");
}
if (baseBackoffMillis < 0) {
throw new IllegalArgumentException("baseBackoffMillis must be >= 0 but was [" + baseBackoffMillis + "]");
}
IOException lastException = null;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
if (attempt > 1 && baseBackoffMillis > 0) {
long backoff = baseBackoffMillis * (attempt - 1);
try {
sleeper.sleep(backoff);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while retrying download from: " + url, e);
}
}
try (InputStream in = URI.create(url).toURL().openStream()) {View on GitHub (pinned to db6a809a66)
Solutions
- Pass maxAttempts >= 1; use the built-in default HTTP_READ_MAX_ATTEMPTS when unsure.
- Clamp configured values: Math.max(1, configuredAttempts) before calling.
- If the 0 came from a missing config, fix the config resolution default rather than masking it.
Example fix
// before HttpUtils.readHttpBytesWithRetry(url, configuredAttempts, backoff, sleeper); // configuredAttempts=0 // after HttpUtils.readHttpBytesWithRetry(url, Math.max(1, configuredAttempts), backoff, sleeper);
Defensive patterns
Strategy: validation
Validate before calling
int safeAttempts = Math.max(1, configuredAttempts); HttpUtils.readHttpBytesWithRetry(url, safeAttempts, backoff, sleeper);
Try / catch
try { HttpUtils.readHttpBytesWithRetry(url, attempts, backoff, sleeper); } catch (IllegalArgumentException e) { /* log misconfig and fall back to defaults */ HttpUtils.readHttpBytesWithRetry(url); } Prevention
- Default to the no-arg overload readHttpBytesWithRetry(url) when you don't need custom retry tuning.
- Validate retry config at load time and fail fast with a clear message.
- Clamp externally supplied retry counts to >= 1.
When it happens
Trigger: Calling readHttpBytesWithRetry(url, maxAttempts, ...) with maxAttempts = 0 or negative, typically via a computed/passed retry count from configuration that resolved to zero.
Common situations: A Gradle build property or env var for HTTP retry count defaulting to 0 when unset; passing a literal 0 during testing; arithmetic that underflows to <= 0.
Related errors
- baseBackoffMillis must be >= 0 but was [{}]
- classname is a required setting for esplugin
- classname is a forbidden for stable esplugin
- invalid deploymentTarget '{}', expected one of {}
- unrecognized classpath entry: {}
AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12).
Data as JSON: /api/errors/0c46974625ce21bc.
Report an issue: GitHub.