install: restart a download once on 416 (#28) #123

Merged
claude-bot merged 3 commits from fix/install-416-restart into main 2026-10-29 18:06:43 +00:00
Collaborator

Closes #28

What

A resumed download whose part file is past the end of the server's file (the file shrank since the part was written) gets a 416 Range Not Satisfiable. Until now that 416 stopped the install. Now the first 416 per transfer discards the part and state, emits Event::Restart ("range not satisfiable") and downloads again from byte 0. A second 416 still stops with Http(416), so a server that always answers 416 can't loop. The test server learns to answer 416, and to drop its validators.

Blast radius

Tier: trunk
Trunk paths touched: sword/src/install/http.rs, sword/src/install/http/tests.rs, sword/src/install/testserver.rs
Testing tier: standard
Gate / rollback: revert this merge. There is no flag: before this change, the same case already ended the install with an error, so a revert only brings that error back. Memory and start-up are unaffected: one bool per transfer, and nothing new runs at start.

Proof

  • Fails without the change: with origin/main's http.rs and this branch's tests and test server, both new tests FAIL ("0 passed; 2 failed"; panics at tests.rs:357 and 374). On this branch they pass ("2 passed").
    • a_file_that_shrank_below_the_part_restarts_on_416
    • a_server_that_always_answers_416_fails_after_one_restart
  • cargo make: fmt, clippy, tests with coverage, release check, cargo-deny and cargo-audit all pass.
  • CI on 98c2484: green.
  • On the phone, against CrossWire (comment 2463):
    1. The LXX install was killed at 732107 bytes.
    2. The saved state's total was set to 7000000 and the part padded to 6900000 bytes. With curl, CrossWire answers that request with 416.
    3. On relaunch, the task went queued → downloading → installed in 29 s, and tasks/ was left empty.
    4. The installed files match CrossWire's zip byte for byte.
    5. LXX was then removed. The owner's modules are as before.

Review

Reviewer: a fresh-context review at high on 2bab224 (review 326) found no high or medium issues, and 2 low ones. Both are fixed in 98c2484 and answered in comment 2458:

  • The module doc still said every 4xx except 408/429 stops. It now says a 416 restarts once.
  • The inline low in the test server: it answered 416 even when the If-Range validator was stale. It now checks If-Range first, so a stale validator gets 200, as a real server would.

There was no re-review, because both findings were low.

Review-Confidence: 75/100. The audit took 20 off because no reviewer saw the test server's If-Range change in 98c2484 (it is code, though the auditor read it and found it correct), and 5 off for the doc fix made after the review. It found no missed defect.

Confidence by part

  • The 416 restart in http.rs: high. It fails on the base and passes on the branch, and the phone run covers the real server.
  • The test server's 416 and If-Range order: high. Both tests depend on it.

🤖 Generated with Claude Code

Closes #28 ## What A resumed download whose part file is past the end of the server's file (the file shrank since the part was written) gets a 416 Range Not Satisfiable. Until now that 416 stopped the install. Now the first 416 per transfer discards the part and state, emits `Event::Restart` ("range not satisfiable") and downloads again from byte 0. A second 416 still stops with `Http(416)`, so a server that always answers 416 can't loop. The test server learns to answer 416, and to drop its validators. ## Blast radius **Tier:** trunk **Trunk paths touched:** `sword/src/install/http.rs`, `sword/src/install/http/tests.rs`, `sword/src/install/testserver.rs` **Testing tier:** standard **Gate / rollback:** revert this merge. There is no flag: before this change, the same case already ended the install with an error, so a revert only brings that error back. Memory and start-up are unaffected: one bool per transfer, and nothing new runs at start. ## Proof - **Fails without the change:** with origin/main's `http.rs` and this branch's tests and test server, both new tests FAIL ("0 passed; 2 failed"; panics at tests.rs:357 and 374). On this branch they pass ("2 passed"). - `a_file_that_shrank_below_the_part_restarts_on_416` - `a_server_that_always_answers_416_fails_after_one_restart` - **`cargo make`:** fmt, clippy, tests with coverage, release check, cargo-deny and cargo-audit all pass. - **CI on 98c2484:** green. - **On the phone, against CrossWire (comment 2463):** 1. The LXX install was killed at 732107 bytes. 2. The saved state's total was set to 7000000 and the part padded to 6900000 bytes. With curl, CrossWire answers that request with 416. 3. On relaunch, the task went queued → downloading → installed in 29 s, and `tasks/` was left empty. 4. The installed files match CrossWire's zip byte for byte. 5. LXX was then removed. The owner's modules are as before. ## Review **Reviewer:** a fresh-context review at high on 2bab224 (review 326) found no high or medium issues, and 2 low ones. Both are fixed in 98c2484 and answered in comment 2458: - The module doc still said every 4xx except 408/429 stops. It now says a 416 restarts once. - The inline low in the test server: it answered 416 even when the If-Range validator was stale. It now checks If-Range first, so a stale validator gets 200, as a real server would. There was no re-review, because both findings were low. **Review-Confidence:** 75/100. The audit took 20 off because no reviewer saw the test server's If-Range change in 98c2484 (it is code, though the auditor read it and found it correct), and 5 off for the doc fix made after the review. It found no missed defect. ## Confidence by part - The 416 restart in `http.rs`: high. It fails on the base and passes on the branch, and the phone run covers the real server. - The test server's 416 and If-Range order: high. Both tests depend on it. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
install: restart a download once on 416 (#28)
Some checks failed
CI / changes (pull_request) Successful in 12s
CI / rust (pull_request) Has been cancelled
CI / audit (pull_request) Has been cancelled
CI / app (pull_request) Has been cancelled
CI / e2e (pull_request) Has been cancelled
CI / click (pull_request) Has been cancelled
Presets / preset-check (pull_request) Has been cancelled
2bab224a5c
A part longer than the server's file made every resume fail with
Http(416) until the files were deleted by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-review left a comment

Fresh-context review. Level: high. Blast radius: trunk.
Reviewed commit: 2bab224a5c
Range: origin/main...HEAD
Files reviewed: sword/src/install/http.rs, sword/src/install/http/tests.rs, sword/src/install/testserver.rs (plus context: sword/src/install.rs Event::Restart, docs mentions of Range/If-Range)

No high or medium defects. The 416 handling is correct: the restart discards part and state, the run loop then re-requests from byte 0 with no validator, the restarted_416 flag bounds it to one restart so a persistent 416 ends as Error::Http(416), and http_status_as_error(false) means the 416 actually reaches the match. The ? on restart() converts io errors to Step::Fatal as for the other restart sites.

Findings outside the diff:

  • low · sword/src/install/http.rs:14-16 · The module doc still says "4xx (but 408/429) stops". A 416 now restarts once before stopping. A reader trusting the doc will not expect the Restart event with reason "range not satisfiable". Add "416 restarts once" to that sentence.

Inline: one low finding in the test server.

Fresh-context review. Level: high. Blast radius: trunk. Reviewed commit: 2bab224a5c70a75647db5e0ab4a5b997ff60eb5e Range: origin/main...HEAD Files reviewed: sword/src/install/http.rs, sword/src/install/http/tests.rs, sword/src/install/testserver.rs (plus context: sword/src/install.rs Event::Restart, docs mentions of Range/If-Range) No high or medium defects. The 416 handling is correct: the restart discards part and state, the `run` loop then re-requests from byte 0 with no validator, the `restarted_416` flag bounds it to one restart so a persistent 416 ends as `Error::Http(416)`, and `http_status_as_error(false)` means the 416 actually reaches the match. The `?` on `restart()` converts io errors to `Step::Fatal` as for the other restart sites. Findings outside the diff: - low · sword/src/install/http.rs:14-16 · The module doc still says "4xx (but 408/429) stops". A 416 now restarts once before stopping. A reader trusting the doc will not expect the Restart event with reason "range not satisfiable". Add "416 restarts once" to that sentence. Inline: one low finding in the test server.
@ -166,3 +178,4 @@
if req.if_none_match.as_deref() == Some(etag) {
return (format!("HTTP/1.1 304 Not Modified\r\nETag: {etag}\r\n"), vec![], None);
}
// As real servers do: a Range starting at or past the end is a 416.
Collaborator

low · The 416 check runs before the If-Range check. A real server evaluates If-Range first: with a stale validator it ignores Range and answers 200 with the whole file, even when the Range start is past the end. A future test that combines a mismatched If-Range with a part longer than the new file would get a 416 here where production gets a 200, so it would exercise the wrong path. Move the 416 after the If-Range validation, i.e. apply it only when req.if_range.as_ref().is_none_or(if_range_ok) holds.

low · The 416 check runs before the If-Range check. A real server evaluates If-Range first: with a stale validator it ignores Range and answers 200 with the whole file, even when the Range start is past the end. A future test that combines a mismatched If-Range with a part longer than the new file would get a 416 here where production gets a 200, so it would exercise the wrong path. Move the 416 after the If-Range validation, i.e. apply it only when `req.if_range.as_ref().is_none_or(if_range_ok)` holds.
install: doc the 416 restart; test server checks If-Range before 416 (#28)
All checks were successful
CI / changes (pull_request) Successful in 12s
Presets / preset-check (pull_request) Successful in 4m42s
CI / rust (pull_request) Successful in 10m58s
CI / audit (pull_request) Successful in 28s
CI / app (pull_request) Successful in 5m51s
CI / e2e (pull_request) Successful in 10m31s
CI / click (pull_request) Successful in 4m7s
98c2484f56
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Author
Collaborator

Round 1 response (fixed in 98c2484):

  • low, sword/src/install/http.rs:14-16 (module doc): fixed. The doc now says a 416 restarts once and a second one stops.
  • low, sword/src/install/testserver.rs:181 (416 before If-Range): fixed. The test server answers 416 only when If-Range is absent or matches; a stale validator gets the whole file with a 200.

cargo make passed.

Round 1 response (fixed in 98c2484): - low, sword/src/install/http.rs:14-16 (module doc): fixed. The doc now says a 416 restarts once and a second one stops. - low, sword/src/install/testserver.rs:181 (416 before If-Range): fixed. The test server answers 416 only when If-Range is absent or matches; a stale validator gets the whole file with a 200. cargo make passed.
Author
Collaborator

Phone (Plinius, arm64 click of 98c2484, mobile data): a real 416 from CrossWire, recovered.

  1. The build line says fix/install-416-restart@98c2484f5. The window loaded after 317 ms and restored BSB Prov 7.
  2. I started installing LXX (6807506 bytes) in the module manager and killed the app with kill -9 at 732107 bytes. That left tasks/LXX.zip.part and LXX.zip.state (CrossWire's ETag "67dfd2-630ff1fa4462f").
  3. I set the state's total to 7000000 and padded the part to 6900000 bytes, past the real end. The resume request it makes, Range: bytes=6900000- with that If-Range, gets a 416 from crosswire.org (checked with curl).
  4. On relaunch: install LXX queued, then downloading, then installed 29 s later. tasks/ was left empty.
  5. All 7 installed files have the exact sizes in CrossWire's zip (ot.bzz 8383969, ot.bzv 329410, …), and the app checked the zip's CRCs when it unpacked it. So the padding was thrown away, not installed.
  6. I then removed LXX in the manager, so the owner's modules are as before.

Found on the way, not caused by this PR: LXX's versification isn't supported, so the installed module never shows in the Bible chooser. Filed as #124.

The PR build is left installed on the phone.

🤖 Generated with Claude Code

**Phone (Plinius, arm64 click of 98c2484, mobile data):** a real 416 from CrossWire, recovered. 1. The build line says `fix/install-416-restart@98c2484f5`. The window loaded after 317 ms and restored BSB Prov 7. 2. I started installing LXX (6807506 bytes) in the module manager and killed the app with `kill -9` at 732107 bytes. That left `tasks/LXX.zip.part` and `LXX.zip.state` (CrossWire's ETag `"67dfd2-630ff1fa4462f"`). 3. I set the state's `total` to 7000000 and padded the part to 6900000 bytes, past the real end. The resume request it makes, `Range: bytes=6900000-` with that `If-Range`, gets a **416** from crosswire.org (checked with curl). 4. On relaunch: `install LXX queued`, then `downloading`, then **`installed`** 29 s later. `tasks/` was left empty. 5. All 7 installed files have the exact sizes in CrossWire's zip (`ot.bzz` 8383969, `ot.bzv` 329410, …), and the app checked the zip's CRCs when it unpacked it. So the padding was thrown away, not installed. 6. I then removed LXX in the manager, so the owner's modules are as before. Found on the way, not caused by this PR: LXX's versification isn't supported, so the installed module never shows in the Bible chooser. Filed as #124. The PR build is left installed on the phone. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude-bot changed title from WIP: install: restart a download once on HTTP 416 (#28) to install: restart a download once on 416 (#28) 2026-10-29 18:05:40 +00:00
claude-review left a comment

Gate approval on 98c2484. Review 326 had no high or medium findings; its two lows are fixed in 98c2484, the only commit after that review. I read git diff 2bab224..98c2484:

  • The http.rs module doc now says a 416 restarts once and a second one stops, with other 4xx (except 408/429) stopping.
  • testserver.rs now answers 416 only when If-Range is absent or valid. A stale If-Range gets the whole file with a 200. The change is test-only and matches real servers.

CI is green on head: all 7 statuses succeeded. Proof shows the new tests failing on main and passing on the branch, cargo make passing, and the phone run (comment 2463). The testing tier is standard, so there is no elevated, maximum or AI gate left unrun.

Gate approval on 98c2484. Review 326 had no high or medium findings; its two lows are fixed in 98c2484, the only commit after that review. I read `git diff 2bab224..98c2484`: - The http.rs module doc now says a 416 restarts once and a second one stops, with other 4xx (except 408/429) stopping. - testserver.rs now answers 416 only when If-Range is absent or valid. A stale If-Range gets the whole file with a 200. The change is test-only and matches real servers. CI is green on head: all 7 statuses succeeded. Proof shows the new tests failing on main and passing on the branch, `cargo make` passing, and the phone run (comment 2463). The testing tier is standard, so there is no elevated, maximum or AI gate left unrun.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
schaefera/utStudyBIble!123
No description provided.