JSON is the format for data. When you need to send a file — an image, a PDF, a CSV — JSON isn't a good fit. The standard way to upload files over HTTP is multipart/form-data, a format originally designed for HTML forms with <input type="file">. As a tester, you'll encounter file uploads in profile pictures, document attachments, CSV imports, and report uploads. This lesson teaches the wire format, the test cases that catch upload bugs, and the security tests every upload endpoint deserves.
What multipart looks like on the wire
A multipart request is a single HTTP request whose body is split into named parts, each separated by a boundary string. Roughly:
POST /api/upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----abc123
------abc123
Content-Disposition: form-data; name="file"; filename="photo.jpg"
Content-Type: image/jpeg
<binary file bytes here>
------abc123
Content-Disposition: form-data; name="description"
Profile photo for Alice
------abc123
Content-Disposition: form-data; name="userId"
123
------abc123--You almost never construct this by hand. Every HTTP client builds it for you when you tell it "here's a file, here are some text fields."
Uploading with curl
curl uses the -F flag to add a multipart part. The @ prefix says "this is a file path, send the contents":
curl -X POST https://api.example.com/upload \
-F "file=@photo.jpg" \
-F "description=Profile photo" \
-F "userId=123"Three parts in one request: the file, plus two text fields. curl sets the Content-Type header and the boundary automatically.
If you also need to send headers like auth:
curl -X POST https://api.example.com/upload \
-H "Authorization: Bearer eyJhbGciOiJI..." \
-F "file=@photo.jpg" \
-F "description=Profile photo"The upload lifecycle
Step 1 of 6
Client picks file
Either a user-selected file in a UI, or a path passed to a script. The client reads its contents and notes its name and type.
Six steps, each with its own failure modes. The bugs hide in steps 4 and 5: parsing the multipart format and storing the file safely.
Test cases for file uploads
For every upload endpoint, work through this list:
| Scenario | Expected |
|---|---|
| Valid file within size limit | 201 with file metadata |
| File exactly at the size limit | 201 |
| File 1 byte over the limit | 413 Payload Too Large |
| File far over the limit (1GB when limit is 10MB) | 413, fast — server doesn't accept the whole thing |
| Empty file (0 bytes) | 400 |
| No file part attached | 400 |
| Multiple files when only one is allowed | 400 |
File with a disallowed extension (.exe) | 400 or 415 |
File with allowed extension but wrong actual content (.jpg containing PHP) | 400 (server should sniff content, not trust the extension) |
Filename with special characters (café.jpg, report (final).pdf) | accepted, stored safely |
Filename with traversal characters (../../etc/passwd) | sanitised, never written outside the upload dir |
| Very long filename (300 chars) | accepted or 400, never 500 |
| Corrupted file content | depends on server — image decode fails → 400 |
| Right file, wrong field name | 400 |
The two security-critical tests — content-type spoofing and path traversal in filenames — have caused real production incidents. Don't skip them.
Asserting on upload results
A successful upload usually returns metadata you can assert on:
{
"id": "f_abc123",
"url": "https://cdn.example.com/uploads/f_abc123.jpg",
"size": 245760,
"contentType": "image/jpeg",
"uploadedAt": "2026-05-01T10:00:00Z"
}Strong upload tests verify:
- The status code is the expected success code.
- The response has a usable id or URL for the new file.
- The reported
sizematches the bytes you sent. - The reported
contentTypematches what you sent. - A follow-up
GET <url>actually returns the bytes you uploaded — round-trip verified.
That round-trip step catches a class of bug where the upload succeeds at the API layer but the file isn't actually persisted (failed background job, wrong storage path).
Base64 file uploads
Some APIs accept files as Base64-encoded strings inside a JSON body, instead of multipart:
{
"filename": "photo.jpg",
"contentType": "image/jpeg",
"data": "<base64-encoded bytes>"
}This is simpler to send (it's just JSON) but has two costs: Base64 inflates the payload by ~33%, and very large uploads stress JSON parsers. You'll see this pattern most often in mobile-first APIs and in CSP-strict browser environments.
Tests are similar — same boundaries, same content-type checks — but you also need a test for malformed Base64 (data: "not-valid-base64" → 400).
Download testing
The mirror image of upload testing. For a GET /files/{id} that returns a file:
- Status code is 200 (or 206 Partial Content if range requests are supported).
Content-Typematches the file's actual type.Content-Dispositionis set if the browser should treat it as a download (attachment; filename="report.pdf").Content-Lengthis correct.- Body bytes match what was uploaded.
- Auth is enforced — anonymous request → 401, wrong-user request → 403.
That last point matters: a common bug is upload endpoints that require auth but download URLs that don't, leaving private files publicly readable.
Performance and resilience
Uploads are slow operations. A few considerations:
- Timeouts in your tests should be longer than for normal API calls — 30s minimum, 60s+ for big files.
- Streaming uploads — many APIs accept chunked transfer; large uploads should not require buffering the whole file in memory.
- Resumable uploads — Google Drive, S3, and some custom APIs support resuming a partially-failed upload. If yours does, test the resume path.
- Concurrent uploads — fire two simultaneously to the same endpoint. Both should succeed, neither should clobber the other.
⚠️ Common mistakes
- Trusting the file extension. A
.jpgextension is just text after a dot — actual contents could be anything. Servers must inspect the bytes (magic numbers) to verify the type. - Forgetting to test a missing-file part. Many tests pass when the file is present but break when it's missing. Servers shouldn't 500 on a missing field.
- Using
application/jsonContent-Type for multipart bodies. A surprisingly common mistake when hand-building requests. Result: the server tries to parse a multipart blob as JSON and 400s confusingly.
🎯 Practice task
Build an upload test plan. 30 minutes.
- Find an API that accepts file uploads — your own product, or a public API like Imgur or any Slack workspace's
files.uploadendpoint. - Read the docs: what's the size limit, what types are accepted, where does the response point?
- Run a happy-path upload with curl
-Fand confirm the response includes a usable URL. - Run a follow-up GET on that URL — confirm the bytes you get back are byte-identical to what you uploaded (
md5sumis your friend). - Try at least four negative tests: oversized file, wrong type, empty file, missing file part. Note the actual status code for each.
- Try one security test: a
.jpgfile whose contents are actually a small text file. Does the server detect the mismatch? - Stretch: try a filename with
../in it. Confirm the server sanitises it (the file should be stored under a safe name, not written outside the upload directory).
You can now design comprehensive upload tests. The next lesson moves to a different family of bugs — those that hide in pagination, filtering, and sorting.