JSON is for data. When you need to send a file — a profile photo, a PDF report, a CSV import — JSON isn't a good fit. The API Testing Masterclass lesson on file uploads covered the conceptual side and the security tests every upload endpoint deserves; this lesson is the Rest Assured way to send files (and download them) without dropping out of the BDD chain. Two methods do most of the work — multiPart() for upload, extract().asByteArray() for download — and a small set of test cases catch the bugs that hide in upload code paths.
A simple file upload
multiPart(name, file) adds one part to a multipart/form-data request. The library sets the Content-Type header (with the boundary) automatically:
import java.io.File;
@Test
public void uploadProfilePhoto() {
given()
.multiPart("file", new File("src/test/resources/testdata/photo.jpg"))
.multiPart("description", "Profile photo")
.multiPart("userId", "42")
.when()
.post("/upload")
.then()
.statusCode(200)
.body("fileName", equalTo("photo.jpg"))
.body("size", greaterThan(0));
}Three parts in one request: a binary file plus two text fields. Rest Assured detects that you're using multiPart and sets Content-Type: multipart/form-data; boundary=----XYZ for you — don't set it yourself or you'll fight the library.
Specifying the file's content type
Some servers reject parts whose declared content type doesn't match the file's actual format. multiPart(name, file, contentType) lets you pin it:
given()
.multiPart("file", new File("report.pdf"), "application/pdf")
.multiPart("title", "Q1 Report")
.when()
.post("/documents");Rest Assured infers the content type from the file extension when you don't pass it explicitly — usually fine for .jpg, .png, .pdf, but worth being explicit on production tests where the server validates strictly.
Multiple files in one request
When the API accepts an array under one part name (a common shape: files[]), call multiPart more than once with the same name:
given()
.multiPart("files", new File("doc1.pdf"))
.multiPart("files", new File("doc2.pdf"))
.multiPart("files", new File("doc3.pdf"))
.when()
.post("/documents/upload")
.then()
.statusCode(200)
.body("uploaded", hasSize(3));Different APIs use different shapes (files, files[], documents); read your API's contract, then mirror it in the part name.
Uploading a JSON sidecar with a file
Many endpoints accept a file plus a JSON metadata blob. multiPart accepts a third overload that takes a String value plus a content type:
String metadata = """
{ "type": "report", "year": 2024, "department": "QA" }
""";
given()
.multiPart("file", new File("data.csv"))
.multiPart("metadata", metadata, "application/json")
.when()
.post("/imports")
.then()
.statusCode(201);The wire format is one multipart request with two parts — one file, one JSON blob. The server parses each by its own content type.
Downloading and verifying a file
The mirror image: a GET that returns binary data. Use extract().asByteArray() to capture the bytes:
import java.nio.file.Files;
import java.nio.file.Paths;
@Test
public void downloadAndVerifyReport() throws Exception {
byte[] fileContent = given()
.when()
.get("/reports/123/download")
.then()
.statusCode(200)
.header("Content-Type", "application/pdf")
.header("Content-Disposition", containsString("attachment"))
.extract()
.asByteArray();
// Basic shape checks
Assert.assertTrue(fileContent.length > 0, "File should not be empty");
Assert.assertTrue(fileContent.length < 10_000_000, "File should be under 10MB");
// PDF magic number — every valid PDF starts with %PDF
String header = new String(fileContent, 0, 4);
Assert.assertEquals(header, "%PDF", "Should be a valid PDF");
// Save to disk if needed for further inspection
Files.write(Paths.get("target/downloaded-report.pdf"), fileContent);
}Three tiers of validation: size (the file isn't empty), type (the magic number proves it's actually a PDF, not an HTML error page renamed .pdf), and optionally a saved copy under target/ for manual inspection. The magic-number check is the one that catches the real upload bugs.
The upload lifecycle
Step 1 of 6
Build parts
Call multiPart() for each file + text field. Rest Assured composes the multipart body, picks a boundary, sets Content-Type: multipart/form-data; boundary=...
The bugs hide in steps 3 and 4 — multipart parsing and safe file storage. The test cases in the next section are designed to surface exactly those.
Test cases worth writing for every upload endpoint
| Scenario | Expected outcome |
|---|---|
| Valid file within size limit | 201 with file metadata |
| File at the size boundary | 201 |
| File 1 byte over the limit | 413 Payload Too Large |
| Empty (0-byte) file | 400 |
| No file part attached | 400 |
| Wrong field name | 400 |
Disallowed extension (.exe) | 400 or 415 |
Allowed extension, wrong actual content (.jpg containing PHP) | 400 — server should sniff bytes, not trust extension |
Filename with special chars (café.jpg, report (final).pdf) | accepted, stored safely |
Filename with traversal (../../etc/passwd) | sanitised; never written outside the upload dir |
| Very long filename (300 chars) | 400 or 200, never 500 |
| Concurrent uploads of the same name | both succeed (server should generate unique keys) |
The two security-critical cases — content-type spoofing and path traversal — have caused real production incidents. Don't skip them in any suite that runs against pre-prod environments.
Asserting on upload responses
A successful upload usually returns metadata. A representative assertion chain:
given()
.multiPart("file", new File("photo.jpg"), "image/jpeg")
.header("Authorization", "Bearer " + token)
.when()
.post("/upload")
.then()
.statusCode(201)
.body("id", notNullValue())
.body("filename", equalTo("photo.jpg"))
.body("size", greaterThan(0))
.body("mimeType", equalTo("image/jpeg"))
.body("url", matchesPattern("^https?://.+/photos/[a-f0-9-]+$"));The regex on url matches a UUID-style key — a sign the server has generated its own key rather than trusting the original filename. That alone is a security check worth keeping.
End-to-end: upload then re-download
The most thorough integration test uploads a file, downloads it back, and verifies the bytes round-trip:
@Test
public void uploadThenDownloadRoundTrip() throws Exception {
File source = new File("src/test/resources/testdata/photo.jpg");
byte[] originalBytes = Files.readAllBytes(source.toPath());
String fileId = given()
.multiPart("file", source, "image/jpeg")
.when()
.post("/upload")
.then()
.statusCode(201)
.extract()
.path("id");
byte[] downloadedBytes = given()
.pathParam("fileId", fileId)
.when()
.get("/files/{fileId}/download")
.then()
.statusCode(200)
.extract()
.asByteArray();
Assert.assertEquals(downloadedBytes.length, originalBytes.length, "Sizes should match");
Assert.assertEquals(
java.util.Arrays.hashCode(downloadedBytes),
java.util.Arrays.hashCode(originalBytes),
"Bytes should be identical"
);
}This proves the file actually round-tripped — not just that the upload returned 201 and the download returned 200. Servers that re-encode images, strip EXIF, or compress on upload will fail this test, which is often the desired behaviour to catch (or to formalise as expected, depending on the API's contract).
⚠️ Common mistakes
- Setting
contentType("multipart/form-data")manually. Rest Assured detects multipart and sets the header with the boundary for you. Hardcoding the content type without the boundary string produces a request the server can't parse — and the error message rarely points at the real cause. - Trusting file extension instead of magic number on download. A
Content-Type: application/pdfresponse is just a header — the actual bytes might be an HTML error page. Validate the file's magic number (the first few bytes) for any download where correctness matters. - Skipping the "no file attached" negative test. It's the easiest test to forget and one of the easiest cases for a server to misbehave on (
500instead of400). One line in your suite catches a regression that might otherwise sit in production for months.
🎯 Practice task
Wire upload and download end-to-end. 30–40 minutes. Use httpbin.org — its /post endpoint accepts multipart and echoes back what it received.
- Drop a small file under
src/test/resources/testdata/sample.txtwith any text content. Drop another filephoto.jpg(any small image). - Set
RestAssured.baseURI = "https://httpbin.org". - Write
uploadSingleFile()— multipart POST to/postwith one file part. Assert status200and that the response'sfilesfield is non-null. (httpbin echoes everything; the structure of its response is documented at httpbin.org/post.) - Write
uploadFileWithFormFields()— POST a file plus two text parts (description,userId). Assert that all three appear in the response. - Write
uploadFileWithJsonMetadata()— POST a file plus a JSON metadata part (multiPart("metadata", "{...}", "application/json")). Assert the metadata round-trips. - Download path. Use Picsum (e.g.,
https://picsum.photos/200/300). GET it, assert200andContent-Type: image/jpeg, extractasByteArray(), assert the array is non-empty and starts with the JPEG magic bytes (0xFF 0xD8 0xFF). - Save it. Add
Files.write(Paths.get("target/downloaded.jpg"), bytes). Run the test, then open the file from yourtarget/directory — it should be a real, viewable image. - Negative test. POST to
/postwith no file part attached. Assert200(httpbin is permissive, but a real upload API would return400). Note how a real test would assert on the structured error message. - Stretch: time a 1MB upload. Generate a 1MB file (
new byte[1_000_000]filled with random bytes), POST it, and assert.time(lessThan(10000L)). Note how the time scales with file size — important for capacity planning.
That's request building covered. Chapter 3 goes deep on response validation: JsonPath expressions, Hamcrest matchers, JSON Schema validation, and the XML cousin of all of the above.