Skip to content
GitLab
Projects Groups Snippets
  • /
  • Help
    • Help
    • Support
    • Community forum
    • Submit feedback
    • Contribute to GitLab
  • Sign in / Register
  • O openapi-generator
  • Project information
    • Project information
    • Activity
    • Labels
    • Members
  • Repository
    • Repository
    • Files
    • Commits
    • Branches
    • Tags
    • Contributors
    • Graph
    • Compare
  • Issues 3,476
    • Issues 3,476
    • List
    • Boards
    • Service Desk
    • Milestones
  • Merge requests 402
    • Merge requests 402
  • CI/CD
    • CI/CD
    • Pipelines
    • Jobs
    • Schedules
  • Deployments
    • Deployments
    • Environments
    • Releases
  • Packages and registries
    • Packages and registries
    • Package Registry
    • Infrastructure Registry
  • Monitor
    • Monitor
    • Incidents
  • Analytics
    • Analytics
    • Value stream
    • CI/CD
    • Repository
  • Wiki
    • Wiki
  • Snippets
    • Snippets
  • Activity
  • Graph
  • Create a new issue
  • Jobs
  • Commits
  • Issue Boards
Collapse sidebar
  • OpenAPI Tools
  • openapi-generator
  • Merge requests
  • !8067

[R] Bug - Invalid code generated for POST with no request object

  • Review changes

  • Download
  • Email patches
  • Plain diff
Merged Administrator requested to merge github/fork/robertpyke/robertpyke_r_body_fix into master Dec 02, 2020
  • Overview 0
  • Commits 5
  • Pipelines 0
  • Changes 4

Created by: robertpyke

PR is just a suggestion of how I think you can fix it. I'm not setup to actually run the build system at the moment. i.e. I'm not claiming this is ready to merge as is, it's just what I expect will address the issue based on my observation. I.e. You can think of this more as a bug report with attached code pointer if you like.

Description

Our model has a POST that has no request modeled. i.e. no body. The code generated normally for a PUT with a request generates something akin to:

       if (!missing(`athena.create.table.request`)) {
         body <- `athena.create.table.request`$toJSONString()
       } else {
         body <- NULL
       }

...

      resp <- self$apiClient$CallApi(url = paste0(self$apiClient$basePath, urlPath),
                                 method = "PUT",
                                 queryParams = queryParams,
                                 headerParams = headerParams,
                                 body = body,
                                 ...)

In the case where no request object is modeled, this initial code block (where body is set) isn't generated, so you just end up with something like:

      resp <- self$apiClient$CallApi(url = paste0(self$apiClient$basePath, urlPath),
                                 method = "POST",
                                 queryParams = queryParams,
                                 headerParams = headerParams,
                                 body = body,
                                 ...)

This in turn means self$apiClient$CallApi is called with body set as the vale of the built in function base::body (as body isn't a local variable).

base::body
==>
function (fun = sys.function(sys.parent())) 
{
    if (is.character(fun)) 
        fun <- get(fun, mode = "function", envir = parent.frame())
    .Internal(body(fun))
}

This results in a strange error indicating that body was unexpected:

api.instance <- DefaultApi$new()
result <- api.instance$TokenExchangeStsPost()

where TokenExchangeStsPost is a POST with no request body modeled.

Error: Unknown type of `body`: must be NULL, FALSE, character, raw or list
Traceback:

1. api.instance$TokenExchangeStsPost()
2. self$TokenExchangeStsPostWithHttpInfo(...)
3. self$apiClient$CallApi(url = paste0(self$apiClient$basePath, 
 .     urlPath), method = "POST", queryParams = queryParams, headerParams = headerParams, 
 .     body = body, ...)
4. httr::POST(url, query = queryParams, headers, body = body, httr::content_type("application/json"), 
 .     httpTimeout, httr::user_agent(self$userAgent), ...)
5. request_build("POST", hu$url, body_config(body, match.arg(encode)), 
 .     as.request(config), ...)
6. body_config(body, match.arg(encode))
7. stop("Unknown type of `body`: must be NULL, FALSE, character, raw or list", 
 .     call. = FALSE)
Swagger-codegen version

Build package: org.openapitools.codegen.languages.RClientCodegen

  <dependency>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-cli</artifactId>
    <version>4.2.3</version>
  </dependency>
Swagger declaration file content or url

Note: I removed some of our model from what does the repro (to not expose certain info). The important bit is that the token_exchange_sts is a post, and has no request defined.

openapi: "3.0.1"
info:
    title: "${title}"
    version: "1.0"
    description: "${description}"
x-amazon-apigateway-policy: ${policy}
servers:
    -
        url: "${endpoint}"
        x-amazon-apigateway-endpoint-configuration:
            vpcEndpointIds:
                - "${vpc_ep_id}"

paths:
    /token_exchange_sts:
        post:
            responses:
                '200':
                    description: Success
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/tokenExchangeSTSResponse'
            x-amazon-apigateway-integration:
                httpMethod: POST
                type: aws_proxy
                uri: arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${service_function_arn}:${alias}/invocations
                credentials: ${service_invoker_role_arn}

  

security:
    - CustomAuthorizer: []
components:
    securitySchemes:
        CustomAuthorizer:
            in: header
            type: apiKey
            name: Authorization
            x-amazon-apigateway-authorizer:
                type: token
                authorizerResultTtlInSeconds: 300
                authorizerUri: arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${authorizer_function_arn}:${alias}/invocations
                identityValidationExpression: Bearer .+
                authorizerCredentials: ${auth_invoker_role_arn}
            x-amazon-apigateway-authtype: custom
    schemas:
        # Response Models
        tokenExchangeSTSResponse:
            type: object
            properties:
                token:
                    $ref: '#/components/schemas/stsCredentials'

            required:
                - token


        # Object Models
        stsCredentials:
            description: Represents STS credentials for a Session (An assumed role)
            type: object
            properties:
                access_key_id:
                    type: string
                    readOnly: true
                secret_access_key:
                    format: password  # Hint that this shouldn't be exposed.
                    type: string
                    readOnly: true
                session_token:
                    type: string
                    readOnly: true
            required:
                - access_key_id
                - secret_access_key
                - session_token
Command line used for generation
        "-J-Dmodels",
        "-J-Dapis",
        "-J-DsupportingFiles",
        "-J-DmodelTests=false",
        "-J-DapiTests=false",
        "generate",
        "-i",
        "MY FILE.yaml",
        "-g",
        "r",
        "-o",
        "MY OUTPUT PATH",
        "-p",
        "packageName=rproxyserviceclient"
Steps to reproduce
  1. Generate the client in R for a POST model with no request body (as shown in the example).
  2. Observe that the file default_api.R has the following:
TokenExchangeStsPostWithHttpInfo = function(...){
      args <- list(...)
      queryParams <- list()
      headerParams <- c()

      urlPath <- "/token_exchange_sts"
      # API key authentication
      if ("Authorization" %in% names(self$apiClient$apiKeys) && nchar(self$apiClient$apiKeys["Authorization"]) > 0) {
        headerParams['Authorization'] <- paste(unlist(self$apiClient$apiKeys["Authorization"]), collapse='')
      }

      resp <- self$apiClient$CallApi(url = paste0(self$apiClient$basePath, urlPath),
                                 method = "POST",
                                 queryParams = queryParams,
                                 headerParams = headerParams,
                                 body = body,
                                 ...)

      if (httr::status_code(resp) >= 200 && httr::status_code(resp) <= 299) {
        deserializedRespObj <- tryCatch(
          self$apiClient$deserialize(resp, "TokenExchangeSTSResponse", loadNamespace("rproxyserviceclient")),
          error = function(e){
             stop("Failed to deserialize response")
          }
        )
        ApiResponse$new(deserializedRespObj, resp)
      } else if (httr::status_code(resp) >= 300 && httr::status_code(resp) <= 399) {
        ApiResponse$new(paste("Server returned " , httr::status_code(resp) , " response status code."), resp)
      } else if (httr::status_code(resp) >= 400 && httr::status_code(resp) <= 499) {
        ApiResponse$new("API client error", resp)
      } else if (httr::status_code(resp) >= 500 && httr::status_code(resp) <= 599) {
        ApiResponse$new("API server error", resp)
      }
    }

You can see this code is invalid:

                                 body = body,

is "broken".

You can validate it by running the client like so:

library(rproxyserviceclient)
api.instance <- DefaultApi$new()
result <- api.instance$TokenExchangeStsPost()

api.instance$TokenExchangeStsPost() will break (Unknown type of body: must be NULL, FALSE, character, raw or list").

Suggest a fix/enhancement

I think you should either set

body <- NULL

above in the case where no request is defined. I believe that will fix it.

Setting it here:

https://github.com/OpenAPITools/openapi-generator/blob/634c4c09e46271c5baba28439f12424b562a5e34/modules/openapi-generator/src/main/resources/r/api.mustache#L180

would effectively default it to NULL, and then the rest of the code generation could override it.

Alternatively if you weren't using a variable name that maps to the base::body, then that might avoid it as well.

I made a local change to api_client.R

      # Default body to NULL when incorrectly set to a function.
      # Body is incorrectly set to base::body (a built-in R function) in
      # some cases due to a bug in the OpenAPI code generator
      # (it assumes it has generated a local variable body when it hasn't).
      # This workaround sets body to NULL if it's ever set as function by the time it gets here.
      if (is.function(body)) {
          body = NULL
      }

but that's clearly a hack.. it works because the body is assigned to the built in function in this special case, is otherwise not a function. I only made the change in this file, as it doesn't change as we update the model, unlike default_api.R, so I changed api_client.R and added it to the ignore file for the generator (so the fix sticks).

FYI : the same template works fine in Python without any changes/hacks.

Assignee
Assign to
Reviewers
Request review from
Time tracking
Source branch: github/fork/robertpyke/robertpyke_r_body_fix