Skip to content

[BUG][KOTLIN][RESTCLIENT] Multiple consumes media types overwrite Content-Type header, last one always wins #24500

Description

@bugs84

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
  1. Save the spec above as openapi.yaml.
  2. Generate with generatorName=kotlin, library=jvm-spring-restclient (config as
    above).
  3. Open the generated NodesApi.kt and inspect createNodeRequestConfig().
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:

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:

  1. Minimal/safe: only emit the assignment for the first consumes entry
    ({{#consumes.0}}...{{/consumes.0}} instead of {{#consumes}}...{{/consumes}}),
    matching the existing hasFormParams branch just above it which already uses
    {{#consumes.0}}.
  2. More correct: skip media types that don't match the body actually being
    serialized (e.g. skip multipart/form-data/application/x-www-form-urlencoded
    in this branch, since this branch by definition runs only when
    hasFormParams is 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions