fix(update): verify download completeness and retry truncated downloads - #275
Merged
Conversation
The installer streamed the release archive and broke its read loop on the first EOF without ever checking received == Content-Length. A TLS-inspecting proxy / antivirus middlebox cutting a large HTTPS download short ends the body with a clean EOF, so a truncated zip was silently accepted and handed to extraction, which failed with the baffling 'ZipError: invalid Zip archive: Could not find EOCD'. - verify_complete(received, total): pure, unit-tested truncation gate - download_once: one attempt that truncates the file, streams, and verifies the full Content-Length arrived - download_archive: up to DOWNLOAD_ATTEMPTS (3) attempts with linear backoff, since middlebox truncation is typically transient A short download now fails as an honest 'Download incomplete: received N of M bytes' (with a hint to check VPN/proxy/AV) and self-heals on retry, instead of surfacing later as an unexplained extract error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Improves the in-app updater’s robustness by ensuring downloaded update archives are complete before extraction, preventing truncated downloads from surfacing later as confusing zip extraction failures.
Changes:
- Added a
verify_complete(received, total)gate to detect truncated (or otherwise size-mismatched) downloads whenContent-Lengthis known. - Refactored archive download into
download_onceand wrapped it withdownload_archiveretries + linear backoff. - Added unit tests covering the download completeness rules.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Address Copilot review: received > total is not a truncation, so it no longer borrows the 'incomplete/connection truncated' wording — it reports an honest 'Download size mismatch' instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
maui1911
added a commit
that referenced
this pull request
Jun 12, 2026
* test(release): add truncated-download resilience check (§6b) §6 serves the update archive from localhost, which never truncates — it proves extraction works but cannot catch a download cut short on a real network (the v0.5.0 'Could not find EOCD' field failure, fixed in #275). Close that validation gap: - dist/truncating_server.py: serves a file but advertises the full Content-Length while cutting the body short on the first N requests, then serves it in full — a stand-in for a TLS-inspecting proxy clipping a large HTTPS download. - RELEASE-VALIDATION §6b: two runs — truncate-first 2 (the install self-heals on attempt 3) and truncate-first 999 (ends on the honest 'Download incomplete', not EOCD), with the server's attempt count as the proof the verify+retry path ran. Verified end-to-end against the real updater with the driver build: 2 truncated + 1 full -> 'Update installed'; always-truncate -> exactly 3 attempts then give up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(release): make §6/§6b staging self-contained, drop dangling slow_server.py Address Copilot review: §6b no longer inherits §6's reference to the missing dist/slow_server.py. §6b spells out the build + Compress-Archive staging, and §6 now serves via the in-repo truncating_server.py (--truncate-first 0 = serve full) instead of the retired script. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(release): fix §6b staleness + sign-off numbering Address Copilot round 2: drop the now-stale 'do not use §6's slow_server.py' warning (§6 no longer has that step), and fix the sign-off range to '1-6b' since 6b sits outside 1-6. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(release): make byte-progress tick optional in §6 checklist Address Copilot round 3: §6 now recommends a plain localhost server, so the checklist item no longer hard-requires a 'visibly advancing' byte count (that only happens with a throttled server) — it accepts an instant jump to total. The extraction check remains the load-bearing assertion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
maui1911
added a commit
that referenced
this pull request
Jun 12, 2026
Patch release: in-app updater now verifies download completeness and retries truncated downloads (#275), so a TLS-inspecting proxy / AV middlebox clipping the archive no longer surfaces as a baffling 'Could not find EOCD' extract failure. Adds the truncated-download resilience check to RELEASE-VALIDATION §6b (#276). Tag v0.5.1 after this merges to trigger the release pipeline. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
Updating from the in-app toast failed for a user (twice) with:
Root cause
EOCD(End Of Central Directory) is the trailer at the end of every valid zip. "Could not find EOCD" means the archive on disk was truncated — the download never finished, but the failure only surfaced two stages later at extract time.The installer's download loop broke on the first
Read::read→Ok(0)(EOF) and then flushed + extracted without ever checkingreceived == Content-Length. A TLS-inspecting proxy / antivirus middlebox cutting a large HTTPS download short ends the body with a clean EOF, so a short zip was silently accepted.Verified the published v0.5.0 artifact is fine — full download (200,
application/octet-stream, all 5,717,213 bytes, validPK\x03\x04header) extracts cleanly with the exact same code + dep pins. So this is a downloader-robustness bug, not a release or extraction-code bug.Fix
verify_complete(received, total)— pure, unit-tested truncation gate (Nonetotal = no Content-Length → can't verify → accept).download_once— one attempt that truncates the target file, streams with byte-progress, then verifies the full length arrived.download_archive— up toDOWNLOAD_ATTEMPTS(3) attempts with linear backoff, since middlebox truncation is typically transient.A short download now fails as an honest "Download incomplete: received N of M bytes (connection truncated — check a VPN/proxy or antivirus that inspects HTTPS, then retry)" and self-heals on retry.
Tests
4 new unit tests on
verify_complete(full / truncated / unknown-total / over-read).cargo test -p codescopegreen; clippy clean on the changed file.Note on validation gap
RELEASE-VALIDATION §6 serves the archive from localhost, which never truncates — so it proves extraction works but structurally cannot catch a truncating network. This fix makes that failure mode honest + self-healing regardless.
🤖 Generated with Claude Code