Some checks failed
Audit / cargo audit (backend) (push) Failing after 9m1s
Audit / npm audit (frontend) (push) Successful in 1m10s
Checks / Backend — cargo test + clippy + fmt (push) Failing after 55s
Checks / Keepsake viewer — builds, self-contained, committed artifact in sync (push) Has been cancelled
Checks / E2E — typecheck + lint (push) Has been cancelled
E2E / Playwright E2E (chromium + webkit) (push) Has been cancelled
E2E / Cross-UA smoke matrix (push) Has been cancelled
Checks / Frontend — vitest + svelte-check (push) Has been cancelled
Measured at the reverse proxy during the live event, not inferred: status=201 content_length=6449056 dur=3.473s dev=Android status=400 content_length=0 dur=0.022s dev=iPhone status=400 content_length=0 dur=0.008s dev=iPhone status=400 content_length=0 dur=0.009s dev=iPhone Content-Length ZERO, in 7-22 ms. Nothing was ever put on the wire, which is why this was never a transport problem -- HTTP/3 was disabled first on the theory that QUIC was truncating large POSTs, and it changed nothing. `addToQueue` stored the picked `File` itself: `blob: file`. WebKit persists a File in IndexedDB as a REFERENCE to the OS backing file rather than a copy, and iOS deletes that file soon after the picker closes. What is left is a neutered File: `.name` and `.size` still read correctly, so nothing downstream looks wrong, and `xhr.send()` does NOT throw -- the note at the send site assumed it would -- it sends an empty body. The server cannot parse a multipart with no parts and answers 400 "Error parsing `multipart/form-data` request", which is the same 400 a genuinely truncated upload produces. That collision is what made v0.18.4 make things worse: it reclassified that 400 as retryable, so each dead photo re-sent an empty body five times. 79 failed requests, 1 success, four guests with nothing uploaded. TWO FIXES. Bytes are copied at pick time, so IndexedDB owns data no OS purge can reach. Chunked at 4 MB rather than one `arrayBuffer()`: this queue accepts videos up to 500 MB and pulling that into the JS heap would get the tab killed, trading a failed upload for a crash. Each chunk becomes its own Blob, so peak heap is one chunk and the browser's blob store holds the rest, spilling to disk as it sees fit. An unreadable blob is TERMINAL, never retried. `entry.blob.size` cannot detect it -- a neutered File reports the original size -- so the check probes one real byte before send. Items queued before this fix are still sitting in IndexedDB holding dead references, and this is what stops them retrying forever. The message asks the guest to re-pick the photo, because the photo is fine: it is still in the camera roll, only the browser's copy is gone. The batch keeps draining past one of these: other queued photos may be readable.