File Uploads and Multipart Requests

8 min read

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:

ScenarioExpected
Valid file within size limit201 with file metadata
File exactly at the size limit201
File 1 byte over the limit413 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 attached400
Multiple files when only one is allowed400
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 contentdepends on server — image decode fails → 400
Right file, wrong field name400

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 size matches the bytes you sent.
  • The reported contentType matches 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-Type matches the file's actual type.
  • Content-Disposition is set if the browser should treat it as a download (attachment; filename="report.pdf").
  • Content-Length is 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 .jpg extension 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/json Content-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.

  1. Find an API that accepts file uploads — your own product, or a public API like Imgur or any Slack workspace's files.upload endpoint.
  2. Read the docs: what's the size limit, what types are accepted, where does the response point?
  3. Run a happy-path upload with curl -F and confirm the response includes a usable URL.
  4. Run a follow-up GET on that URL — confirm the bytes you get back are byte-identical to what you uploaded (md5sum is your friend).
  5. Try at least four negative tests: oversized file, wrong type, empty file, missing file part. Note the actual status code for each.
  6. Try one security test: a .jpg file whose contents are actually a small text file. Does the server detect the mismatch?
  7. 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.

// tip to track lessons you complete and pick up where you left off across devices.