OpenAPITools / OpenAPITools/openapi-generator
[BUG][KOTLIN][RESTCLIENT] Multiple consumes media types overwrite Content-Type header, last one always wins
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 26.8k
- Forks
- 7.7k
- PR merge metrics
- PR metrics pending
Description
Bug Report Checklist
- Have you provided a full/minimal spec to reproduce the issue?
- Have you validated the input using an OpenAPI validator?
- Have you tested with the latest master to confirm the issue still exists?
- Have you searched for related issues/PRs?
- What's the actual output vs expected output?
- Suggested fix included below
Description
For an operation whose consumes list has more than one media type (e.g.
application/json and multipart/form-data) but that has no formData
parameters — i.e. it uses a plain body parameter and multipart is only listed
because the same endpoint also accepts a file upload variant — the
kotlin generator with library=jvm-spring-restclient emits one
localVariableHeaders["Content-Type"] = "..." assignment per declared consumes
entry, all writing to the same map key. The last entry in consumes always
wins at runtime, regardless of which content type actually matches the body being
sent.
This means every call to the generated method sends the same Content-Type
(whichever media type happens to be listed last in the spec) even when the caller
supplies a JSON body object, not a multipart form. Against a real server this causes
the server to attempt to parse a JSON payload as multipart form data and fail (in our
case, against Alfresco Content Services, it throws a ClassCastException trying to
cast the JSON-deserialized body to a Map).
We hit this generating a client for Alfresco's public
POST /nodes/{nodeId}/children operation
(https://github.com/Alfresco/rest-api-explorer), which legitimately declares:
consumes:
- "application/json"
- "multipart/form-data"
because the endpoint supports both a JSON body and a multipart file-upload body,
but the generated client only ever builds the JSON-body call in our usage — yet
the header is force-set to multipart/form-data.
openapi-generator version
Confirmed present in 7.24.0 (latest stable release) and 7.21.0
(byte-for-byte identical generated output between the two). Also independently
rebuilt and tested against the latest master, commit
c3cde9bd5469e47ed58500b6235e54cc368ce432 / version 7.25.0-SNAPSHOT — again
byte-for-byte identical; the bug is still present.
OpenAPI declaration file content or url
Minimal Swagger 2.0 spec that reproduces the issue (mirrors the real Alfresco
operation shape — body param, multiple consumes, no formData params):
swagger: "2.0"
info:
title: "Multi-consumes Content-Type bug repro"
version: "1.0"
basePath: "/api"
paths:
/nodes/{nodeId}/children:
post:
tags:
- "nodes"
summary: "Create a node"
operationId: "createNode"
parameters:
- name: "nodeId"
in: "path"
required: true
type: "string"
- in: "body"
name: "nodeBodyCreate"
description: "The node information to create."
required: true
schema:
$ref: "#/definitions/NodeBodyCreate"
consumes:
- "application/json"
- "multipart/form-data"
produces:
- "application/json"
responses:
"201":
description: "Successful response"
schema:
$ref: "#/definitions/NodeEntry"
definitions:
NodeBodyCreate:
type: "object"
properties:
name:
type: "string"
nodeType:
type: "string"
NodeEntry:
type: "object"
properties:
id:
type: "string"
Generation Details
Gradle plugin invocation (equivalent CLI: generate -g kotlin --library jvm-spring-restclient -i openapi.yaml -o out --additional-properties serializationLibrary=jackson,enumPropertyNaming=UPPERCASE,useSpringBoot3=true):
plugins {
id("org.openapi.generator") version "7.21.0"
}
openApiGenerate {
generatorName = "kotlin"
library = "jvm-spring-restclient"
inputSpec.set("$projectDir/openapi.yaml")
outputDir.set("$projectDir/out")
apiPackage = "repro.api"
modelPackage = "repro.model"
packageName = "repro"
configOptions = mapOf(
"serializationLibrary" to "jackson",
"enumPropertyNaming" to "UPPERCASE",
"useSpringBoot3" to "true",
)
}
Steps to reproduce
- Save the spec above as
openapi.yaml. - Generate with
generatorName=kotlin,library=jvm-spring-restclient(config as
above). - Open the generated
NodesApi.ktand inspectcreateNodeRequestConfig().
Actual output
fun createNodeRequestConfig(nodeId: kotlin.String, nodeBodyCreate: NodeBodyCreate) : RequestConfig<NodeBodyCreate> {
val localVariableBody = nodeBodyCreate
val localVariableQuery = mutableMapOf<kotlin.String, kotlin.collections.List<kotlin.String>>()
val localVariableHeaders: MutableMap<String, String> = mutableMapOf()
localVariableHeaders["Content-Type"] = "application/json"
localVariableHeaders["Content-Type"] = "multipart/form-data"
localVariableHeaders["Accept"] = "application/json"
...
Content-Type is unconditionally sent as multipart/form-data, even though the
method takes a single JSON-serializable NodeBodyCreate body and no form/multipart
parameters exist at all.
Expected output
Since the operation has no formData parameters, only one Content-Type should be
emitted — the one that actually matches the body being constructed, e.g.:
localVariableHeaders["Content-Type"] = "application/json"
(the first non-multipart entry in consumes, or otherwise a single, well-defined
choice — see "Suggest a fix" below for options).
Related issues/PRs
Searched open and closed issues; the closest related reports are about different
generators/symptoms and are not duplicates of this one:
- #21306
[BUG][kotlin-spring] Multipart with JSON array broken - #21548
[BUG][Kotlin-Spring] Wrong parameter type for multipart-form-data file upload when using kotlin-spring with reactive - #15251
[BUG][kotlin-spring] wrong field name and type for multipart - #23885
[JAVA][RESTCLIENT] ... Maven plugin 7.22.0 produces invalid client code to get application/octet-stream(responseAcceptside,javagenerator) - #9258
[BUG][JAVA][webclient] UnsupportedMediaTypeException if multiple consumes media type and unknown type in first position(java/webclientlibrary, different template)
None address the kotlin/jvm-spring-restclient request-side Content-Type
overwrite for multi-consumes operations without form params.
Suggest a fix
Root cause is in
modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-restclient/api.mustache,
around:
{{^hasFormParams}}{{#hasConsumes}}{{#consumes}}localVariableHeaders["Content-Type"] = "{{{mediaType}}}"
{{/consumes}}{{/hasConsumes}}{{/hasFormParams}}
This iterates over all consumes entries and assigns Content-Type
unconditionally for each, so the last one always wins. Two possible fixes:
- Minimal/safe: only emit the assignment for the first
consumesentry
({{#consumes.0}}...{{/consumes.0}}instead of{{#consumes}}...{{/consumes}}),
matching the existinghasFormParamsbranch just above it which already uses
{{#consumes.0}}. - More correct: skip media types that don't match the body actually being
serialized (e.g. skipmultipart/form-data/application/x-www-form-urlencoded
in this branch, since this branch by definition runs only when
hasFormParamsis false and thus no multipart/form body is being built).
Happy to open a PR with option 1 (smallest change, consistent with the sibling
hasFormParams branch's existing use of consumes.0) if maintainers confirm the
preferred approach.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-restclient/api.mustache, focusing on the hasConsumes branch for operations without form parameters. Regenerate the provided minimal spec and inspect NodesApi.kt and createNodeRequestConfig(); done means a multi-consumes JSON-body operation emits one appropriate Content-Type assignment instead of repeated assignments that let the last media type win.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- kotlin, openapi
- Domain
- api, tooling
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100