fix(kotlin-client): actually send the jvm-vertx form body
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
MultiMapand send it with.sendForm(form). - multipart operations build an
io.vertx.ext.web.multipart.MultipartFormand send it with.sendMultipartForm(form).- file parameters use
form.binaryFileUpload(baseName, it.name, it.absolutePath, contentType). WithmapFileBinaryToByteArray=truethe parameter is aByteArray, so the bytes are sent through theBufferoverload instead:form.binaryFileUpload(baseName, baseName, Buffer.buffer(it), contentType).contentTypeis the part's encodingcontentTypewhen the spec sets one, otherwiseapplication/octet-stream. - other parameters use
form.attribute(baseName, value).
- file parameters use
- array form parameters go through the client's existing
toMultiValue(list, collectionFormat)helper:form.add(name, values)for urlencoded and oneform.attribute(name, value)per returned value for multipart.toMultiValuereturns one value per item only forcollectionFormatmulti. For other formats, including thecsvthat the generator assigns to OAS 3 form arrays without anencodingentry, 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 wirevalueis passed totoMultiValue, as the single-enum case already does. - form operations no longer call
request.putHeader("Content-Type", …).sendFormandsendMultipartFormset the header themselves. With a preset header, Vert.x 5.1.5 (the version in the samples'build.gradle) throwsIllegalStateExceptionwhen the media type is anything other than exactlyapplication/x-www-form-urlencodedormultipart/form-data, e.g.application/x-www-form-urlencoded; charset=UTF-8. A multipart operation is therefore always sent asmultipart/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.
| Test | Asserts |
|---|---|
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 run | Result | |
|---|---|---|
| current master merged + this PR | 36 | 0 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 theirputHeader("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.shnot run: no generator option or metadata changes.
(For Windows users, please run the script in WSL)./mvnw clean package || exit ./bin/generate-samples.sh ./bin/configs/*.yaml || exit ./bin/utils/export_docs_generators.sh || exit
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