xai-org/grok-build · error

send_with_retry_escaping_pool ran at least one attempt

Error message

send_with_retry_escaping_pool ran at least one attempt

What it means

send_with_retry_escaping_pool runs a retry loop tracking last_err and at the end returns Err(last_err.expect("send_with_retry_escaping_pool ran at least one attempt")). The expect is an invariant assertion: if the loop exits without any recorded error, zero attempts ran, which the API contract forbids. It panics rather than returning a bogus Ok/Err.

Source

Thrown at crates/codegen/xai-grok-http/src/lib.rs:553

                        pooled.clone()
                    }
                },
            }
        } else {
            pooled.clone()
        };
        match op(client).await {
            Ok(value) => return Ok(value),
            Err(e) if is_retryable(&e) => {
                // A silent retry would hide a degrading pool
                tracing::debug!(attempt, error = %e, "send_with_retry_escaping_pool: retrying after transient failure");
                last_err = Some(e);
            }
            Err(e) => return Err(e),
        }
    }

    Err(last_err.expect("send_with_retry_escaping_pool ran at least one attempt"))
}

/// Shared blocking client for startup fetches.
/// Carries `STARTUP_FETCH_TIMEOUT` as the connect and read ceiling; do not reuse for long-lived requests.
///
/// This avoids redundant TLS certificate loading for blocking HTTP calls (e.g., model prefetching during startup).
/// The blocking client is separate from the async `shared_client()` because reqwest's blocking client creates its own internal tokio runtime.
///
/// Mirrors `shared_client()`'s pool self-healing for the same reason: this client is reused (settings, prefetch).
/// Idle and TCP eviction drops a connection before the upstream idle window (~60-100s; 30s is a conservative default) closes it.
/// The HTTP/2 keepalive-ping setters that `shared_client()` uses are not exposed on reqwest's blocking `ClientBuilder` (0.12).
/// Only the idle and TCP eviction half applies here.
pub fn shared_startup_blocking_client() -> reqwest::blocking::Client {
    static BLOCKING_CLIENT: OnceLock<reqwest::blocking::Client> = OnceLock::new();
    BLOCKING_CLIENT
        .get_or_init(|| {
            let _timer = startup_timer!("startup.http_blocking_client_build");
            xai_grok_extra_ca::build_blocking_reqwest_client(|builder| {

View on GitHub (pinned to bc7f02eddd)

Solutions

  1. Clamp the retry policy to at least one attempt before calling (max(1))
  2. Fix the config/env source so max_attempts is >= 1 and validate it at load time
  3. If you truly want zero attempts, skip the call instead of invoking the retry wrapper
  4. Add an assertion/log where the policy is constructed to catch 0-attempt policies early

Example fix

// before
let policy = RetryPolicy { max_attempts: cfg.max_attempts }; // may be 0
send_with_retry_escaping_pool(&req, policy)?;
// after
let policy = RetryPolicy { max_attempts: cfg.max_attempts.max(1) };
send_with_retry_escaping_pool(&req, policy)?;
Defensive patterns

Strategy: validation

Validate before calling

assert!(retry_policy.max_attempts >= 1, "retry policy must allow at least one attempt");
let policy = RetryPolicy { max_attempts: retry_policy.max_attempts.max(1) };

Type guard

fn attempts_valid(p: &RetryPolicy) -> bool { p.max_attempts >= 1 }

Try / catch

let result = std::panic::catch_unwind(|| send_with_retry_escaping_pool(&req, policy))
    .map_err(|_| anyhow!("retry policy yielded zero attempts"))?;

Prevention

When it happens

Trigger: Calling send_with_retry_escaping_pool with a retry policy whose attempt count is 0 (or a config that computes zero attempts), so the loop body never executes and last_err is still None at return.

Common situations: A retry policy loaded from config/env where max_attempts defaults to 0 or parses to 0; an off-by-one in custom retry logic (attempts-1); wiring a policy object intended for a different function.

Related errors


AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31). Data as JSON: /api/errors/810e8e65415255db. Report an issue: GitHub.