jdx/mise · error
attestation requests must not have a streaming body
Error message
attestation requests must not have a streaming body
What it means
The sigstore attestation client retries HTTP requests, which requires cloning the reqwest::RequestBuilder between attempts. If the request body is streaming (non-replayable), try_clone returns None and this panic fires. It is an internal invariant: attestation requests must always use in-memory (cloneable) bodies.
Source
Thrown at crates/mise-sigstore/src/lib.rs:439
reqwest::Url::parse_with_params(&url, query_params)
.map_err(|e| AttestationError::Api(format!("Invalid GitHub attestations URL: {e}")))
}
/// Send a request and read its body, retrying transient failures (5xx, 429,
/// timeouts, connection errors, and mid-body-read errors) with exponential
/// backoff. A `429`'s `Retry-After` header is honored in preference to the
/// computed backoff. The body is buffered here so a transient failure during
/// the body read is retried too, rather than escaping the retry boundary.
///
/// The request must have no streaming body so it can be cloned per attempt —
/// true for all GET calls here. Non-transient responses (incl. 4xx like 404)
/// are returned as-is for the caller to interpret.
async fn send_with_retry(&self, request: reqwest::RequestBuilder) -> Result<HttpResponse> {
let mut attempt = 1;
loop {
let req = request
.try_clone()
.expect("attestation requests must not have a streaming body");
let last = attempt >= self.max_attempts;
// A labeled block so the `reqwest::Response` is dropped before the
// backoff sleep — holding it would pin its body/connection for the
// whole delay. Each attempt either returns, errors out, or breaks
// with the delay to wait before the next attempt.
let delay = 'attempt: {
match req.send().await {
Ok(response) => {
let status = response.status();
if !last && is_retryable_status(status) {
break 'attempt retry_after_delay(response.headers())
.unwrap_or_else(|| backoff_delay(self.backoff_base, attempt));
}
let headers = response.headers().clone();
match response.bytes().await {
Ok(body) => {
return Ok(HttpResponse {View on GitHub (pinned to afd2eddd3a)
Solutions
- This is a mise-internal bug: report it at github.com/jdx/mise/issues with the command and version
- As a workaround, keep the request payload small/in-memory (it should be by design)
- Pin to an earlier mise version if a recent release introduced the regression
Example fix
// before let req = client.post(url).body(some_stream); // after let req = client.post(url).body(bytes.to_vec()); // cloneable fixed body
Defensive patterns
Strategy: try-catch
Type guard
fn body_is_cloneable(req: &reqwest::RequestBuilder) -> bool { req.try_clone().is_some() } Try / catch
// panic is by design; guard by asserting cloneability before building the request assert!(request.try_clone().is_some(), "attestation body must be in-memory");
Prevention
- Never attach streaming bodies to sigstore attestation requests
- Keep attestation payloads as byte buffers/JSON
- Update mise when sigstore-related panics appear after an upgrade
When it happens
Trigger: fetch_attestations or fetch_bundle_url building a request whose body is a stream (e.g. passing a reader/stream body instead of a fixed byte buffer), then send_with_retry calling try_clone.
Common situations: Code changes that swap a JSON/byte body for a streaming upload; large attestation payloads fed via stream; a library bug rather than user misconfiguration.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- retry loop should always return
- cloned request builder should remain cloneable
- BootstrapPart values have clap names
- bootstrap command is registered
- affected project exists in graph
AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09).
Data as JSON: /api/errors/62a9caa5f45d6dcb.
Report an issue: GitHub.