{"record":{"id":"62a9caa5f45d6dcb","repo":"jdx/mise","slug":"attestation-requests-must-not-have-a-streaming-bod","errorCode":null,"errorMessage":"attestation requests must not have a streaming body","messagePattern":"attestation requests must not have a streaming body","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/mise-sigstore/src/lib.rs","lineNumber":439,"sourceCode":"        reqwest::Url::parse_with_params(&url, query_params)\n            .map_err(|e| AttestationError::Api(format!(\"Invalid GitHub attestations URL: {e}\")))\n    }\n\n    /// Send a request and read its body, retrying transient failures (5xx, 429,\n    /// timeouts, connection errors, and mid-body-read errors) with exponential\n    /// backoff. A `429`'s `Retry-After` header is honored in preference to the\n    /// computed backoff. The body is buffered here so a transient failure during\n    /// the body read is retried too, rather than escaping the retry boundary.\n    ///\n    /// The request must have no streaming body so it can be cloned per attempt —\n    /// true for all GET calls here. Non-transient responses (incl. 4xx like 404)\n    /// are returned as-is for the caller to interpret.\n    async fn send_with_retry(&self, request: reqwest::RequestBuilder) -> Result<HttpResponse> {\n        let mut attempt = 1;\n        loop {\n            let req = request\n                .try_clone()\n                .expect(\"attestation requests must not have a streaming body\");\n            let last = attempt >= self.max_attempts;\n\n            // A labeled block so the `reqwest::Response` is dropped before the\n            // backoff sleep — holding it would pin its body/connection for the\n            // whole delay. Each attempt either returns, errors out, or breaks\n            // with the delay to wait before the next attempt.\n            let delay = 'attempt: {\n                match req.send().await {\n                    Ok(response) => {\n                        let status = response.status();\n                        if !last && is_retryable_status(status) {\n                            break 'attempt retry_after_delay(response.headers())\n                                .unwrap_or_else(|| backoff_delay(self.backoff_base, attempt));\n                        }\n                        let headers = response.headers().clone();\n                        match response.bytes().await {\n                            Ok(body) => {\n                                return Ok(HttpResponse {","sourceCodeStart":421,"sourceCodeEnd":457,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/crates/mise-sigstore/src/lib.rs#L421-L457","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nlet req = client.post(url).body(some_stream);\n// after\nlet req = client.post(url).body(bytes.to_vec()); // cloneable fixed body","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"fn body_is_cloneable(req: &reqwest::RequestBuilder) -> bool { req.try_clone().is_some() }","tryCatchPattern":"// panic is by design; guard by asserting cloneability before building the request\nassert!(request.try_clone().is_some(), \"attestation body must be in-memory\");","preventionTips":["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"],"tags":["http","sigstore","panic","invariant"],"backgroundTag":"internal-invariant-violation","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}