E2E download investigation, 2026-09-16
Reproducer: node tests/e2e-browser-probe.mjs. Isolated HTTP fixture serves
AES-GCM chunks to the production download service worker. Playwright saves
the download and Node compares SHA-256 against generated plaintext.
Observed results:
- Original Firefox: 3,221,225,609-byte input reported downloaded but only 1,164,183,192 bytes saved. Chromium saved all bytes with matching SHA-256.
- Pull-based stream alone, retaining iframe, and byte stream alone did not prevent Firefox truncation.
- Explicit fetch-event lifetime improved duration but still truncated.
- 16,777,353 bytes with 4-second request latency and iframe retained: Firefox saved only 13,631,124 bytes. This also reproduces with small files.
- With a page message to the service worker every second, Firefox downloaded
3,221,225,609 bytes with matching SHA-256:
39405a645b45edba1e32f75e54c603ec5d48666e617766cb8d4e2f0efcd0b162. - Bounded range retries recover a single HTTP 503 or socket disconnect on Firefox and Chromium (16,777,353 bytes, matching SHA-256).
- Integrated shared download helper: Firefox and Chromium each saved all 3,221,225,609 bytes with the SHA-256 above.
- Permanent 503: four attempts; Chromium cancels, Firefox’s download manager can remain pending. The page now reports failure explicitly on all three engines. A failed/partial download must be discarded, not treated as valid.
Implemented: page heartbeat until worker completion/error, before-unload warning, no iframe removal while streaming, pull-based byte stream, explicit worker lifetime, bounded retries and 60-second timeout per ciphertext range attempt. AES-GCM authentication failures are never retried or bypassed. The sandbox’s worker wrapper uses the same implementation as production.
Verified matrix (Playwright, Linux; WebKit is not a physical Safari test):
| Case | Firefox | Chromium | WebKit |
|---|---|---|---|
| 3 GiB + 137 B download, real helper, SHA-256 | pass | pass | pass (full round trip) |
| 1 B, chunk boundary -1/exact/+1, two full chunks | pass | pass | pass |
| 16 MiB + 137 B; transient 503, disconnect, short body | pass | pass | pass |
| Persistent 503, corrupt GCM tag, wrong Content-Range, 404: explicit UI error | pass | pass | pass |
| Three simultaneous ranges including a chunk boundary; invalid range rejected | pass | pass | pass |
| Actual uploader + download page, password/KDF/sentinel, 16 MiB + 137 B | pass | pass | pass |
| Upload transient 503/disconnect: retry, final SHA-256 | pass | pass | pass |
| Upload 403: rejected without finalization | pass | pass | pass |
| Zero-byte upload explicitly rejected, never finalized | pass | pass | pass |
| Download request stalled 60 seconds: timeout/retry, final SHA-256 | pass | pass | pass |
| Slow download (>60 seconds), another page brought to foreground | pass | pass | pass |
| Full 3 GiB + 137 B upload/download round trip | pass | pass | pass |
| Initial header request HTTP 503: retry, final SHA-256 | pass | pass | pass |
The upload fixture runs the actual static/upclass.js, the download EJS view,
and the actual download app/worker against a local mock chunk API. It checks
reassembly length, wrong-password rejection, and downloaded SHA-256. It does
not exercise production databases, reverse proxies, or cold storage.
Reproduction commands:
E2E_REAL_UI=1 node tests/e2e-browser-probe.mjs
E2E_REAL_UI=1 E2E_SIZE=3221225609 E2E_BROWSERS=firefox,chromium node tests/e2e-browser-probe.mjs
E2E_REAL_UI=1 E2E_FAULT=permanent node tests/e2e-browser-probe.mjs
E2E_REAL_UI=1 E2E_RANGES=1 node tests/e2e-browser-probe.mjs
E2E_REAL_UI=1 E2E_BACKGROUND=1 E2E_DELAY=4000 node tests/e2e-browser-probe.mjs
node tests/e2e-upload-browser-probe.mjs
E2E_UPLOAD_FAULT=disconnect node tests/e2e-upload-browser-probe.mjs
E2E_SIZE=3221225609 E2E_BROWSERS=firefox node tests/e2e-upload-browser-probe.mjs
Fixtures exit nonzero on failed assertions and retain generated artifacts in
the printed /tmp/ax-e2e-* directories. Large upload tests require substantial
temporary disk space. Run large tests sequentially on memory-limited machines.
Manual Safari/mobile/sleep tests are outside this Linux fixture. Bringing another headless page to the foreground is not equivalent to OS sleep or mobile app suspension. Closing/suspending the page is not supported during an active download; the page explicitly warns users to keep it open. Firefox may retain a pending browser download after an unrecoverable error even though the page reports failure; cancel/discard that partial download.
The new shared script and versioned worker wrappers were checked via HTTPS on
file.ax. Views are not cached by the app, and these static/template changes do
not require a backend restart. Reload the E2E page to pick up the updated worker.
Custom-domain views load the shared page helper from mainBaseUrl; the worker
and ciphertext remain same-origin, preserving the existing runtime allowlist.