fix(kotlin-client): actually send the jvm-vertx form body

KH
21h ago
+223-106

The kotlin client with library=jvm-vertx builds a form for operations with form parameters but never sends it, so every application/x-www-form-urlencoded and multipart/form-data request goes out with an empty body.

Reproducer

Any operation with form parameters, e.g. updatePetWithForm and uploadFile in the petstore samples. On master, samples/client/petstore/kotlin-jvm-vertx-jackson/.../PetApi.kt:

request.putHeader("Content-Type", "multipart/form-data")
...
val form = io.vertx.core.MultiMap.caseInsensitiveMultiMap();
additionalMetadata?.let { form.add("additionalMetadata", additionalMetadata) }
file?.let { form.add("file", file.toString()) }
...
return request
    .send()

The form is populated and then dropped: .send() sends no body. In the multipart case the file part would also be wrong even if it were sent, because file.toString() is the file's path, not its content. Array form parameters are added as a single toString() of the whole list.

Fix

In kotlin-client/libraries/jvm-vertx/api.mustache:

  • urlencoded operations keep the MultiMap and send it with .sendForm(form).
  • multipart operations build an io.vertx.ext.web.multipart.MultipartForm and send it with .sendMultipartForm(form).
    • file parameters use form.binaryFileUpload(baseName, it.name, it.absolutePath, contentType). With mapFileBinaryToByteArray=true the parameter is a ByteArray, so the bytes are sent through the Buffer overload instead: form.binaryFileUpload(baseName, baseName, Buffer.buffer(it), contentType). contentType is the part's encoding contentType when the spec sets one, otherwise application/octet-stream.
    • other parameters use form.attribute(baseName, value).
  • array form parameters go through the client's existing toMultiValue(list, collectionFormat) helper: form.add(name, values) for urlencoded and one form.attribute(name, value) per returned value for multipart. toMultiValue returns one value per item only for collectionFormat multi. For other formats, including the csv that the generator assigns to OAS 3 form arrays without an encoding entry, it returns one joined value (tags=a,b). That default is existing generator behavior and is unchanged here. For arrays of enums, each item's wire value is passed to toMultiValue, as the single-enum case already does.
  • form operations no longer call request.putHeader("Content-Type", …). sendForm and sendMultipartForm set the header themselves. With a preset header, Vert.x 5.1.5 (the version in the samples' build.gradle) throws IllegalStateException when the media type is anything other than exactly application/x-www-form-urlencoded or multipart/form-data, e.g. application/x-www-form-urlencoded; charset=UTF-8. A multipart operation is therefore always sent as multipart/form-data.
  • operations without form parameters still use .send(), and operations with a body parameter still use .sendBuffer(...). Neither path changes.

After regeneration, the multipart operation in the sample reads:

val form = io.vertx.ext.web.multipart.MultipartForm.create()
additionalMetadata?.let { form.attribute("additionalMetadata", additionalMetadata) }
file?.let { form.binaryFileUpload("file", it.name, it.absolutePath, "application/octet-stream") }
...
return request
    .sendMultipartForm(form)

Tests

modules/openapi-generator/src/test/resources/3_0/kotlin/kotlin-vertx-form-send.yaml (added by this PR) has a urlencoded operation with an array field tags and an enum array field, a second urlencoded operation declared as application/x-www-form-urlencoded; charset=UTF-8, and a multipart operation with a file myFile (whose encoding declares contentType: image/jpeg) and an array field labels.

TestAsserts
testJvmVertxFormBodyIsSent (new)inside each generated operation: the urlencoded one ends with .sendForm(form) and the multipart one with .sendMultipartForm(form); form.binaryFileUpload("myFile" with "image/jpeg" and no form.add("myFile"; no putHeader("Content-Type" anywhere in the file; form.add("tags", toMultiValue(it.toList(), "csv")); the complete toMultiValue(…).forEach { form.attribute("labels", it) } expression; enum array items sent as .value
testJvmVertxFormFileByteArray (new)with mapFileBinaryToByteArray=true, the file part is sent with Buffer.buffer(it)

./mvnw -nsu -Denforcer.skip=true -pl modules/openapi-generator -am -Dtest=KotlinClientCodegenApiTest -Dsurefire.failIfNoSpecifiedTests=false test

Tests runResult
current master merged + this PR360 failures, 0 skipped
this PR with the api.mustache change reverted (tests kept; each new test run on its own)—testJvmVertxFormBodyIsSent and testJvmVertxFormFileByteArray fail

Samples

12 files changed in kotlin-jvm-vertx-gson, kotlin-jvm-vertx-jackson, kotlin-jvm-vertx-jackson-coroutines and kotlin-jvm-vertx-moshi:

  • PetApi.kt: the two form operations now send their form, and their putHeader("Content-Type", …) lines are gone.
  • PetApi.kt, StoreApi.kt, UserApi.kt: operations without form parameters lose an empty line that the removed template line used to leave. This is whitespace only.

All four directories are compiled by samples-kotlin-client.yaml. The petstore samples have no array form parameter, so the array branches are covered by the generator test above.

This PR and its sibling PRs #25622, #25624 and #25625 ($ escaping in wire names) all touch KotlinClientCodegenApiTest and neighboring kotlin-client templates; whichever merges first, I will rebase the others.

cc @wing328 @karismann @Zomzog @andrewemery @4brunu @yutaka0m @stefankoppier @e5l (Kotlin technical committee)

PR checklist

  • Read the contribution guidelines.
  • Run the following to build the project and update samples — scoped to this change: built the CLI (./mvnw -pl modules/openapi-generator-cli -am package -DskipTests) and regenerated the jvm-vertx configs with ./bin/generate-samples.sh; the resulting diff is committed. export_docs_generators.sh not run: no generator option or metadata changes.
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

🤖 Generated with Claude Code