OpenAPITools / openapi-generator

OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (v2, v3)
https://openapi-generator.tech
Apache License 2.0
21.23k stars 6.43k forks source link

[BUG] [JAVA] Passed in Content-Type is overwritten as `application/json` #18053

Open brianluisgomez opened 6 months ago

brianluisgomez commented 6 months ago

Bug Report Checklist

Description

We have a spec where one of the request bodies allows for multiple content-types. If a user specifies a particular content type, say image/jpeg, the generated SDK will be default overwrite that content type always to application/json with a selectHeaderContentType function defined below. This should be changed so that passed in Content-Types are not overwritten by this generated function. I couldn't figure out any way to override this with mustache templates since I had no way to pull in the content-type type defined by the user when using the SDK

    /**
     * Select the Content-Type header's value from the given array:
     *   if JSON exists in the given array, use it;
     *   otherwise use the first one of the array.
     *
     * @param contentTypes The Content-Type array to select from
     * @return The Content-Type header to use. If the given array is empty,
     *   returns null. If it matches "any", JSON will be used.
     */
    public String selectHeaderContentType(String[] contentTypes) {
        if (contentTypes.length == 0) {
            return null;
        }

        if (contentTypes[0].equals("*/*")) {
            return "application/json";
        }

        for (String contentType : contentTypes) {
            if (isJsonMime(contentType)) {
                return contentType;
            }
        }

        return contentTypes[0];
    }
openapi-generator version
OpenAPI declaration file content or url

Here is a minimal version that just includes the part that's breaking

openapi: 3.0.3
info:
  title: Messaging
  version: 4.3.0
  contact:
    name: Bandwidth
    url: https://support.bandwidth.com
    email: support@bandwidth.com
  description: |-
    The API Specification for Bandwidth's Messaging Platform

    ## Base URL

    `https://messaging.bandwidth.com/api/v2`
servers:
  - url: https://messaging.bandwidth.com/api/v2
    description: Production
paths:
  /users/{accountId}/media:
    put:
      summary: Upload Media
      description: |-
        Upload a file. You may add headers to the request in order to provide some control to your media file.
      operationId: uploadMedia
      tags:
        - Media
      parameters:
        - $ref: "#/components/parameters/accountId"
        - $ref: "#/components/parameters/mediaId"
        - $ref: "#/components/parameters/contentType"
        - $ref: "#/components/parameters/cacheControl"
      requestBody:
        $ref: "#/components/requestBodies/uploadMediaRequest"
      responses:
        "204":
          description: No Content
components:
  parameters:
    accountId:
      in: path
      name: accountId
      required: true
      schema:
        type: string
      description: Your Bandwidth Account ID.
      example: "9900000"
    mediaId:
      in: path
      name: mediaId
      required: true
      description: Media ID to retrieve.
      example: 14762070468292kw2fuqty55yp2b2/0/bw.png
      schema:
        type: string
    contentType:
      in: header
      name: Content-Type
      style: simple
      explode: false
      description: The media type of the entity-body.
      example: audio/wav
      schema:
        type: string
    cacheControl:
      in: header
      name: Cache-Control
      style: simple
      explode: false
      description: >-
        General-header field is used to specify directives that MUST be obeyed by
        all caching mechanisms along the request/response chain.
      example: no-cache
      schema:
        type: string
  schemas:
  requestBodies:
    uploadMediaRequest:
      content:
        application/json:
          schema:
            type: string
            format: binary
        application/ogg:
          schema:
            type: string
            format: binary
        application/pdf:
          schema:
            type: string
            format: binary
        application/rtf:
          schema:
            type: string
            format: binary
        application/zip:
          schema:
            type: string
            format: binary
        application/x-tar:
          schema:
            type: string
            format: binary
        application/xml:
          schema:
            type: string
            format: binary
        application/gzip:
          schema:
            type: string
            format: binary
        application/x-bzip2:
          schema:
            type: string
            format: binary
        application/x-gzip:
          schema:
            type: string
            format: binary
        application/smil:
          schema:
            type: string
            format: binary
        application/javascript:
          schema:
            type: string
            format: binary
        audio/mp4:
          schema:
            type: string
            format: binary
        audio/mpeg:
          schema:
            type: string
            format: binary
        audio/ogg:
          schema:
            type: string
            format: binary
        audio/flac:
          schema:
            type: string
            format: binary
        audio/webm:
          schema:
            type: string
            format: binary
        audio/wav:
          schema:
            type: string
            format: binary
        audio/amr:
          schema:
            type: string
            format: binary
        audio/3gpp:
          schema:
            type: string
            format: binary
        image/bmp:
          schema:
            type: string
            format: binary
        image/gif:
          schema:
            type: string
            format: binary
        image/jpeg:
          schema:
            type: string
            format: binary
        image/pjpeg:
          schema:
            type: string
            format: binary
        image/png:
          schema:
            type: string
            format: binary
        image/svg+xml:
          schema:
            type: string
            format: binary
        image/tiff:
          schema:
            type: string
            format: binary
        image/webp:
          schema:
            type: string
            format: binary
        image/x-icon:
          schema:
            type: string
            format: binary
        text/css:
          schema:
            type: string
            format: binary
        text/csv:
          schema:
            type: string
            format: binary
        text/calendar:
          schema:
            type: string
            format: binary
        text/plain:
          schema:
            type: string
            format: binary
        text/javascript:
          schema:
            type: string
            format: binary
        text/vcard:
          schema:
            type: string
            format: binary
        text/vnd.wap.wml:
          schema:
            type: string
            format: binary
        text/xml:
          schema:
            type: string
            format: binary
        video/avi:
          schema:
            type: string
            format: binary
        video/mp4:
          schema:
            type: string
            format: binary
        video/mpeg:
          schema:
            type: string
            format: binary
        video/ogg:
          schema:
            type: string
            format: binary
        video/quicktime:
          schema:
            type: string
            format: binary
        video/webm:
          schema:
            type: string
            format: binary
        video/x-ms-wmv:
          schema:
            type: string
            format: binary
      required: true
  securitySchemes:
    Basic:
      type: http
      scheme: basic
      description: |-
        Basic authentication is a simple authentication scheme built into the
        HTTP protocol. To use it, send your HTTP requests with an Authorization
        header that contains the word Basic followed by a space and a
        base64-encoded string `username:password`Example: `Authorization: Basic
        ZGVtbZpwQDU1dzByZA==`
security:
  - Basic: []
tags:
  - name: Media
Steps to reproduce

Reproducible by using the generated SDK and designating a specific content-type in the UploadMedia call. If you check what content-type header is actually being sent out it is different from what was specified by the user

brianluisgomez commented 6 months ago

Here is the generated code in the API call itself. Toward the end you'll see the selectHeaderContentType method being called. This is where the content-type gets overwritten

    public okhttp3.Call uploadMediaCall(String accountId, String mediaId, File body, String contentType, String cacheControl, final ApiCallback _callback) throws ApiException {
        String basePath = null;
        // Operation Servers
        String[] localBasePaths = new String[] { "https://messaging.bandwidth.com/api/v2" };

        // Determine Base Path to Use
        if (localCustomBaseUrl != null){
            basePath = localCustomBaseUrl;
        } else if ( localBasePaths.length > 0 ) {
            basePath = localBasePaths[localHostIndex];
        } else {
            basePath = null;
        }

        Object localVarPostBody = body;

        // create path and map variables
        String localVarPath = "/users/{accountId}/media/{mediaId}"
            .replace("{" + "accountId" + "}", localVarApiClient.escapeString(accountId.toString()))
            .replace("{" + "mediaId" + "}", localVarApiClient.escapeString(mediaId.toString()));

        List<Pair> localVarQueryParams = new ArrayList<Pair>();
        List<Pair> localVarCollectionQueryParams = new ArrayList<Pair>();
        Map<String, String> localVarHeaderParams = new HashMap<String, String>();
        Map<String, String> localVarCookieParams = new HashMap<String, String>();
        Map<String, Object> localVarFormParams = new HashMap<String, Object>();

        if (contentType != null) {
            localVarHeaderParams.put("Content-Type", localVarApiClient.parameterToString(contentType));
        }

        if (cacheControl != null) {
            localVarHeaderParams.put("Cache-Control", localVarApiClient.parameterToString(cacheControl));
        }

        final String[] localVarAccepts = {
            "application/json"
        };
        final String localVarAccept = localVarApiClient.selectHeaderAccept(localVarAccepts);
        if (localVarAccept != null) {
            localVarHeaderParams.put("Accept", localVarAccept);
        }

        final String[] localVarContentTypes = {
            "application/json",
            "application/ogg",
            "application/pdf",
            "application/rtf",
            "application/zip",
            "application/x-tar",
            "application/xml",
            "application/gzip",
            "application/x-bzip2",
            "application/x-gzip",
            "application/smil",
            "application/javascript",
            "audio/mp4",
            "audio/mpeg",
            "audio/ogg",
            "audio/flac",
            "audio/webm",
            "audio/wav",
            "audio/amr",
            "audio/3gpp",
            "image/bmp",
            "image/gif",
            "image/jpeg",
            "image/pjpeg",
            "image/png",
            "image/svg+xml",
            "image/tiff",
            "image/webp",
            "image/x-icon",
            "text/css",
            "text/csv",
            "text/calendar",
            "text/plain",
            "text/javascript",
            "text/vcard",
            "text/vnd.wap.wml",
            "text/xml",
            "video/avi",
            "video/mp4",
            "video/mpeg",
            "video/ogg",
            "video/quicktime",
            "video/webm",
            "video/x-ms-wmv"
        };
        final String localVarContentType = localVarApiClient.selectHeaderContentType(localVarContentTypes);
        if (localVarContentType != null) {
            localVarHeaderParams.put("Content-Type", localVarContentType);
        }

        String[] localVarAuthNames = new String[] { "Basic" };
        return localVarApiClient.buildCall(basePath, localVarPath, "PUT", localVarQueryParams, localVarCollectionQueryParams, localVarPostBody, localVarHeaderParams, localVarCookieParams, localVarFormParams, localVarAuthNames, _callback);
    }
jakubsimacek commented 1 month ago

Possibly the same issue: ...

    get:
      tags:
        - Anotace
      summary: ....
      operationId: getPlainStringOperation
      responses:
        "200":
          description: ...
          content:
            text/plain; charset=utf-8:
              schema:
                type: string

leads to:

        final String[] localVarAccepts = { 
            "text/plain; charset=utf-8", "application/json"
         };

and results in: Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of typejava.lang.Stringfrom Array value (tokenJsonToken.START_ARRAY)

jakubsimacek commented 1 month ago

So in my case it's not a bug apparently. It added the "application/json" because (here missing) 400 and 500 responses containing structured JSON. However the API client should handle 200 as non JSON payload. Helped me to add the converted in front of the Jackson one.

    @Bean
    public ApiClient apiClient(RestTemplate restTemplate) {
        restTemplate.getMessageConverters().add(0, new StringHttpMessageConverter());
        return new ApiClient(restTemplate).setBasePath(wsUrl);
    }