Multipart and file uploads
This page covers Mokksy's multipart body-matching APIs: body { form { ... } }, file-part matchers, FormEncoding, and multipart(...) for non-form bodies.
Use these matchers when your client sends more than JSON. File uploads, mixed metadata-plus-binary requests, and strict form-encoding checks all work through the same request DSL.
Match multipart form fields and file uploads
body { form { ... } } matches both application/x-www-form-urlencoded and multipart/form-data by default. Add field(...) matchers for text parts and file(...) matchers for uploaded files.
1mokksy.post {
2 path("/upload")
3 body {
4 form {
5 field("description", "Mokksy upload")
6 file("avatar") {
7 filename("photo.bin")
8 contentType("application/octet-stream")
9 bytes { it?.contentEquals(uploadFile.readBytes()) == true }
10 }
11 }
12 }
13} respondsWith {
14 body = "file-upload-ok"
15}
16
17val response = client.post("/upload") {
18 setBody(
19 MultiPartFormDataContent(
20 formData {
21 append("description", "Mokksy upload")
22 append(
23 "avatar",
24 uploadFile.readBytes(),
25 Headers.build {
26 append(
27 HttpHeaders.ContentDisposition,
28 "form-data; name=\"avatar\"; filename=\"photo.bin\"",
29 )
30 append(HttpHeaders.ContentType, "application/octet-stream")
31 },
32 )
33 },
34 ),
35 )
36}
37
38response.status shouldBe HttpStatusCode.OK
39response.bodyAsText() shouldBe "file-upload-ok" 1var uploadFile = Files.createTempFile("avatar", ".bin");
2Files.writeString(uploadFile, "expected");
3
4mokksy.post(spec -> spec
5 .path("/upload")
6 .body(body -> body.form(form -> form
7 .field("description", "Mokksy upload")
8 .file("avatar", file -> file
9 .filename("photo.bin")
10 .contentType("application/octet-stream")
11 .bytesMatches(bytes -> {
12 try {
13 org.assertj.core.api.Assertions.assertThat(bytes)
14 .containsExactly(Files.readAllBytes(uploadFile));
15 return true;
16 } catch (IOException e) {
17 throw new UncheckedIOException(e);
18 }
19 })
20 )
21 ))
22).respondsWith(rb -> rb.body("file-upload-ok"));For file parts, Mokksy supports filename(...), contentType(...), text(...), and bytes(...). Each matcher adds specificity, so a stub that checks field values and file content automatically outranks a looser fallback stub.
Restrict accepted form encoding
Use FormEncoding.MULTIPART or FormEncoding.URL_ENCODED when a stub must reject the other form style. Leave the default AUTO when either encoding is acceptable.
1mokksy.post {
2 path("/multipart-only")
3 body {
4 form(FormEncoding.MULTIPART) {
5 field("key", "value")
6 }
7 }
8} respondsWith {
9 body = "multipart-only-ok"
10}
11
12val multipartResult = client.post("/multipart-only") {
13 setBody(
14 MultiPartFormDataContent(
15 formData { append("key", "value") },
16 ),
17 )
18}
19
20val urlEncodedResult =
21 client.submitForm(
22 url = "/multipart-only",
23 formParameters = parameters { append("key", "value") },
24 )
25
26multipartResult.status shouldBe HttpStatusCode.OK
27urlEncodedResult.status shouldBe HttpStatusCode.NotFound1mokksy.post(spec -> spec
2 .path("/multipart-only")
3 .body(body -> body.form(FormEncoding.MULTIPART, form -> form
4 .field("key", "value")
5 ))
6).respondsWith(rb -> rb.body("multipart-only-ok"));Match non-form multipart bodies
Not every multipart request is multipart/form-data. Use body { multipart(...) { ... } } for payloads such as multipart/mixed, where each part has its own content type and semantic role.
1mokksy.post {
2 path("/multipart-mixed")
3 body {
4 multipart("multipart/mixed") {
5 boundary("WebAppBoundary")
6 part("metadata") {
7 contentType("application/json")
8 text { it?.contains("Ktor logo") == true }
9 }
10 part("image") {
11 contentType("image/png")
12 bytes { it?.isNotEmpty() == true }
13 }
14 }
15 }
16} respondsWith {
17 body = "multipart-mixed-ok"
18}
19
20val response = client.post("/multipart-mixed") {
21 setBody(
22 MultiPartFormDataContent(
23 formData {
24 append(
25 "metadata",
26 """{"description":"Ktor logo"}""".encodeToByteArray(),
27 Headers.build {
28 append(HttpHeaders.ContentDisposition, "form-data; name=\"metadata\"")
29 append(HttpHeaders.ContentType, "application/json")
30 },
31 )
32 append(
33 "image",
34 "png-data".encodeToByteArray(),
35 Headers.build {
36 append(HttpHeaders.ContentDisposition, "form-data; name=\"image\"")
37 append(HttpHeaders.ContentType, "image/png")
38 },
39 )
40 },
41 boundary = "WebAppBoundary",
42 contentType =
43 ContentType.MultiPart.Mixed.withParameter(
44 "boundary",
45 "WebAppBoundary",
46 ),
47 ),
48 )
49}
50
51response.status shouldBe HttpStatusCode.OK
52response.bodyAsText() shouldBe "multipart-mixed-ok" 1mokksy.post(spec -> spec
2 .path("/multipart-mixed")
3 .body(body -> body.multipart("multipart/mixed", multipart -> multipart
4 .boundary("WebAppBoundary")
5 .part("metadata", part -> part
6 .contentType("application/json")
7 .textMatches(text -> text != null && text.contains("Ktor logo"))
8 )
9 .part("image", part -> part
10 .contentType("image/png")
11 .bytesMatches(bytes -> bytes != null && bytes.length > 0)
12 )
13 ))
14).respondsWith(rb -> rb.body("multipart-mixed-ok"));