Six Upload Bugs That Only Show Up With Exact-Size Test Files
The usual way to test an upload feature is to find a file on your laptop that is "about the right size" and drag it in. It works, the feature ships, and the bugs show up later in support tickets. The problem is not effort; it is that a random file cannot hit the edges where upload code actually fails.
Below are six failure patterns we keep running into, each with the file you need to reproduce it. None of them need anything exotic: just a file of a known type at a known byte count.
1. The limit is enforced in more than one place, with different numbers
A typical web upload passes through several layers, each with its own limit and its own defaults:
- nginx:
client_max_body_sizedefaults to 1 MB. Anything larger gets a413 Request Entity Too Largebefore your application sees it. - PHP:
upload_max_filesizedefaults to 2M andpost_max_sizeto 8M. - Express:
express.json()defaults to a 100 KB body limit; multer has no file size limit unless you setlimits.fileSize. - Your own validation, in the browser and again on the server.
If the product says 25 MB and nginx is still at a lower value, users see a raw 413 page or a generic network error instead of your friendly message. Reproduce: generate a file one byte over each configured limit and one byte under the product limit. Every "under" file must succeed end to end; every "over" file must produce your application's error, not the proxy's.
2. MB versus MiB
The UI says "Max 5 MB". The client checks file.size > 5_000_000. The server checks size > 5 * 1024 * 1024. A 5,100,000-byte file is rejected in the browser, but the same file sent with curl is accepted. Or the other way round, which is worse: the browser lets it through and the server rejects it after a long upload.
Reproduce: generate 4,999,999, 5,000,000, 5,000,001, 5,242,879, 5,242,880 and 5,242,881 bytes. Six files, one table, and you know exactly which definition each layer uses. The generator accepts an exact byte count in the URL, for example /generator?ext=pdf&bytes=5242881.
3. HTTP 200, but the stored file is the wrong size
This one is invisible unless you check. An intermediate step (an image optimiser, a virus scanner that rewrites files, a multipart parser with a buffer bug) changes the bytes, and the API still reports success. The only reliable check is to compare sizes and hashes after the round trip:
sha256sum dummy.pdf
curl -s -o roundtrip.pdf https://staging.example.test/files/ID
sha256sum roundtrip.pdf
cmp dummy.pdf roundtrip.pdf && echo "identical"
If the hashes differ, something in the pipeline is modifying files it should only be storing. Our worked example on a 5 MB PDF boundary walks through what that looks like.
4. Zero-byte and tiny files
Empty files break code that assumes there is at least a header to read. Common symptoms: an unhandled exception in a thumbnailer, a division by zero in a progress calculation (loaded / total with total = 0), or an S3 multipart upload that is started and never completed. Test with a 0-byte file and with a file smaller than the format's signature (for example a 3-byte "PDF"). The expected result is a clear validation error, not a 500.
# 0-byte and 3-byte files
: > empty.pdf
printf '%%PD' > tiny.pdf
wc -c empty.pdf tiny.pdf
5. The extension and the contents disagree
Users rename files. A .jpg that is really a PNG is common and usually harmless; a .pdf that is really a ZIP is less so. If the server trusts the extension or the browser-supplied Content-Type, downstream workers get input they were not written for.
Reproduce: generate a valid PNG, rename it to .jpg, and upload it. Then do the same with a text file renamed to .png. Decide in advance what should happen (accept and correct the type, or reject) and check that every layer agrees. The extension versus sniffed MIME scenario has a full matrix, and the magic-byte sniffer shows you what a file really is before you upload it.
6. Large files that are fine on localhost
On a local machine a 100 MB upload takes a second or two. On a real connection it can take minutes, which exposes timeouts that were never hit in development: load balancer idle timeouts, serverless function limits, signed upload URLs that expire before the upload finishes, or session tokens that expire mid-request.
Reproduce: generate a file large enough to take several minutes at a realistic upload speed and test through the same path production uses. As a rough guide, at 10 Mbit/s upstream, 100 MiB (104,857,600 bytes) takes about 84 seconds; at 2 Mbit/s it takes about 7 minutes. The 24 MB long-running upload scenario is a good starting point.
Why not just keep a folder of real files?
You can, and for content-sensitive tests (image resizing, OCR, media transcoding) you should. For limit, type and transport tests, real files have three problems:
- Sizes are arbitrary. A real PDF is never exactly 5,242,881 bytes, so you cannot test the boundary.
- They contain things. Screenshots, invoices and exports tend to carry names, addresses and internal data that should not sit in a test bucket or a bug tracker.
- They are hard to share. "Use the big PDF from my laptop" does not reproduce on anyone else's machine. A generator link with an exact byte count does.
A minimal routine
- List every limit in the upload path, in bytes, with where it is configured.
- For each limit, generate files at limit - 1, limit and limit + 1.
- Add a 0-byte file and a renamed-extension file for each accepted type.
- After each upload, compare the stored size and hash with the original.
- Save the generator links in your test plan so the next person gets the same files.
For the step-by-step version of this, see the file upload testing guide.
