{"record":{"id":"1e4eb633b0ac40d7","repo":"cjpais/Handy","slug":"server-sent-more-than-the-expected-bytes","errorCode":null,"errorMessage":"server sent more than the expected {} bytes","messagePattern":"server sent more than the expected (.+?) bytes","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src-tauri/src/managers/model/download.rs","lineNumber":347,"sourceCode":"                        DOWNLOAD_STALL_TIMEOUT.as_secs()\n                    )),\n                    Ok(None) => break,\n                    Ok(Some(chunk)) => chunk?,\n                },\n                _ = cancel_token.cancelled() => {\n                    // Keep the partial for resume; caller handles state cleanup.\n                    return Ok(HttpDownloadOutcome::Cancelled);\n                }\n            };\n            // An untrusted server must not be able to fill the disk: cut the\n            // transfer at the first byte past the known total instead of\n            // trusting it to eventually close the stream. Everything written\n            // so far is tainted by a provably-misbehaving server — clear it.\n            if let Some(cap) = known_total {\n                if downloaded + chunk.len() as u64 > cap {\n                    drop(file);\n                    let _ = fs::remove_file(partial_path);\n                    return Err(anyhow::anyhow!(\n                        \"server sent more than the expected {} bytes\",\n                        cap\n                    ));\n                }\n            }\n            file.write_all(&chunk)?;\n            downloaded += chunk.len() as u64;\n            if last_emit.elapsed() >= throttle {\n                emit_progress(downloaded);\n                last_emit = Instant::now();\n            }\n        }\n        file.flush()?;\n        drop(file);\n        emit_progress(downloaded);\n\n        if let Some(expected) = known_total {\n            let actual = partial_path.metadata()?.len();","sourceCodeStart":329,"sourceCodeEnd":365,"githubUrl":"https://github.com/cjpais/Handy/blob/c89b7bf38974ad317177389f5bfe28236f0d64e3/src-tauri/src/managers/model/download.rs#L329-L365","documentation":"Raised when a server sends more bytes than the known total (catalog-pinned size or advertised Content-Length) — the cap check fires on the first chunk that would exceed it. This is a defense against misbehaving or hostile servers trying to fill the disk through an unbounded stream. Everything already written is considered tainted because the server provably lied, so the file handle is dropped and the partial deleted.","triggerScenarios":"Server ignores Range and streams a full/different body while a size is pinned; server keeps the connection open past Content-Length (infinite/broken stream); a malicious mirror padding the response.","commonSituations":"Misconfigured CDNs appending trailers or serving error pages mid-stream; compromised or buggy mirrors; proxies injecting content.","solutions":["Retry — the tainted partial is already removed and the next attempt restarts cleanly.","If one host consistently triggers it, switch to the other source (mirror <-> primary); this guard failing means that endpoint's responses cannot be trusted.","Verify the URL serves the exact expected byte count with curl -I.","Report the endpoint; over-long responses on a pinned-size object indicate a server-side fault."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"match manager.download_model(model_id, None).await {\n    Ok(_) => Ok(()),\n    Err(e) if e.to_string().contains(\"more than the expected\") => {\n        // over-long body: tainted partial deleted; clean retry, then switch source if repeated\n        manager.download_model(model_id, None).await\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Enforce a hard byte cap from the known total on every download loop (as this code does) so unbounded streams can't fill disks.","Delete rather than bless any bytes written by a server that provably lied about size.","Prefer hash-pinned catalogs so tampering servers are caught at verification even if size checks pass."],"tags":["download","size-cap","server-misbehavior","security","http"],"backgroundTag":"response-size-mismatch","analyzedSha":"c89b7bf38974ad317177389f5bfe28236f0d64e3","analyzedAt":"2026-08-17T10:29:55.597Z","contentChangedAt":"2026-08-17T10:29:55.597Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}