elastic/elasticsearch · error · IllegalArgumentException

baseBackoffMillis must be >= 0 but was [{}]

Error message

baseBackoffMillis must be >= 0 but was [{}]

What it means

HttpUtils.readHttpBytesWithRetry validates baseBackoffMillis >= 0 because the backoff is computed as baseBackoffMillis * (attempt-1) and a negative base would produce nonsensical (negative) sleep durations. The guard surfaces the invalid value directly.

Source

Thrown at build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/util/HttpUtils.java:36

    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()) {
                return in.readAllBytes();
            } catch (IOException e) {
                lastException = e;

View on GitHub (pinned to db6a809a66)

Solutions

  1. Pass baseBackoffMillis = 0 to disable backoff, or a positive value to enable it.
  2. Clamp configured values: Math.max(0, configuredBackoff) before calling.
  3. Audit the source of the negative value — it usually indicates a config-resolution bug.

Example fix

// before
HttpUtils.readHttpBytesWithRetry(url, attempts, -100L, sleeper);
// after
HttpUtils.readHttpBytesWithRetry(url, attempts, 0L, sleeper); // 0 disables backoff
Defensive patterns

Strategy: validation

Validate before calling

long safeBackoff = Math.max(0, configuredBackoff);
HttpUtils.readHttpBytesWithRetry(url, attempts, safeBackoff, sleeper);

Try / catch

try { HttpUtils.readHttpBytesWithRetry(url, attempts, backoff, sleeper); } catch (IllegalArgumentException e) { HttpUtils.readHttpBytesWithRetry(url); }

Prevention

When it happens

Trigger: Calling readHttpBytesWithRetry with a negative baseBackoffMillis argument; a config typo or sign error feeding a negative long.

Common situations: Misconfigured retry backoff property; passing -1 as a 'disable backoff' sentinel instead of 0; arithmetic producing a negative value.

Related errors


AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12). Data as JSON: /api/errors/b853f66f62c159a4. Report an issue: GitHub.