Every point below cost between a few hours and two days, and none of them could be predicted from the documentation.
The iPhone quietly degrades what it uploads
A video recorded in 4K arrived as a short 480 by 360 file, and photos came in smaller than the originals. Nothing in the application did that. The WebKit sources make it clear: when a file is picked from the photo library, Safari on iOS always re-encodes videos, remembers the size chosen in the picker on any other site, and hands over a Live Photo as its short low-resolution clip. A web page cannot switch that off. So the application tells the truth instead: after the choice it shows the real resolution and length, warns when it sees signs of downscaling, and offers the way through the Files app, which delivers the original. The dimensions of every file are stored on the server, so the question why a video is poor is settled by a record, not by guesswork.
Stuck at 96 percent: a response that arrives but is never seen
Uploads from iPhones reached 96 to 99 percent and stopped. The server logs showed everything received and written, with a response time of a second and a half. The phone waited forever. Two causes, both in Apple's networking layer: the browser lost its way when the request body was read straight from a file on disk, and with several uploads at once a request received a complete HTTP 200 but never reported that it had finished. Chunks are now read into memory first and sent as a finished buffer; if a complete, valid response is visible, the chunk counts as delivered and the zombie request is dropped; an idle timer of 30 seconds triggers a retry with a growing interval. The phone also sends a short event log at the end, stored with the file's metadata, so the behaviour of a stranger's phone can be read later from the server.
A modern build, an old phone, a white page
On a phone a few years old with an outdated browser the page appeared without any styles and the upload did not work. The framework builds for browsers of the last two years, the styling library wraps everything in a modern CSS construct that an older engine skips together with the whole stylesheet, and a few new units and syntax shortcuts stop the whole script. Two traps on top: the CSS minifier removed the older of two fallback values as a duplicate, and the framework's official mechanism for loading a script first loaded it after its own bundles. The fix: an explicit list of supported browsers set back several years, the modern construct flattened at build time, fallback units, polyfills, and a few lines of script written directly into the document head.
A big file against the server's defaults
At 5 GB per file most defaults work against you. The reverse proxy buffers the whole request before passing it on, writing gigabytes to its own disk only to hand them over, and size limits and timeouts are short. The upload now streams through every layer without intermediate buffering, every chunk lands in place in a sparse file, completion is atomic, and a repeated completion after a dropped connection does not create a duplicate. The standard form library in Python decoded file names in an old western encoding, so Polish characters could break the whole request; a short reader of the header with the correct encoding replaced it.
Testing a browser from six years ago
Modern test tools can no longer drive browsers from that period. Instead of assuming it should work, archived engine versions were downloaded and driven directly over a lower-level protocol, each through the full path: open the page, upload a real file, compare its checksum with the original, open the gallery and the sub-pages.