Files
EventSnap/frontend
fabi 0fba8defc2
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
fix(upload): iPhone sent an EMPTY body -- the queue stored a file reference, not bytes
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.
2026-08-22 15:54:06 +00:00
..
2026-03-31 20:15:44 +02:00