swagger-api / swagger-api/swagger-codegen

generated server code crashes from using `Parameters` to name both function argument and model class

Open
#6,906 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Mustache
Stars
17.8k
Forks
6k
PR merge metrics
No merged PRs in 30d

Description

Description

The server code generated from a small swagger file contains a name space collision that causes the server to crash on reasonable requests. It's easy to fix manually, but because always an error it shouldn't be generated.

Swagger-codegen version

Following https://github.com/swagger-api/swagger-codegen/issues/4548, I believe I'm using version 2.2.3

Swagger declaration file content or url
swagger: '2.0'
info:
  version: '0.1'
  title: 'error demo'
schemes:
  - http
host: localhost
paths:
  /ep1:
    post:
      description: >-
        first end point
      parameters:
        - in: body
          name: Parameters
          schema:
            type: object
            properties:
              a:
                type: number
                format: float
                description: a param
      responses:
        '200':
          description: things went well
        '400':
          description: things went poorly
  /ep2:
    post:
      description: >-
        first end point
      parameters:
        - in: body
          name: Parameters
          schema:
            type: object
            properties:
              b:
                type: string
                description: b param
      responses:
        '200':
          description: things went well
        '400':
          description: things went poorly
Command line used for generation

swagger-codegen generate -i error-small.yaml -o error-small -l python-flask

Steps to reproduce
Modify default_controller.py to use the parameters

The generated controller is

import connexion
from swagger_server.models.parameters import Parameters
from swagger_server.models.parameters1 import Parameters1
from datetime import date, datetime
from typing import List, Dict
from six import iteritems
from ..util import deserialize_date, deserialize_datetime


def ep1_post(Parameters=None):
    """                                                                                                                                
    ep1_post                                                                                                                           
    first end point                                                                                                                    
    :param Parameters:                                                                                                                 
    :type Parameters: dict | bytes                                                                                                     
                                                                                                                                       
    :rtype: None                                                                                                                       
    """
    if connexion.request.is_json:
        Parameters = Parameters.from_dict(connexion.request.get_json())
    return 'do some magic!'


def ep2_post(Parameters=None):
    """                                                                                                                                
    ep2_post                                                                                                                           
    first end point                                                                                                                    
    :param Parameters:                                                                                                                 
    :type Parameters: dict | bytes                                                                                                     
                                                                                                                                       
    :rtype: None                                                                                                                       
    """
    if connexion.request.is_json:
        Parameters = Parameters1.from_dict(connexion.request.get_json())
    return 'do some magic!'

This builds and runs cleanly, since it doesn't touch Parameters in either end point, and therefore doesn't expose the renaming of the argument. If we use both even trivially, though, that's enough to show the difference:

import connexion
from swagger_server.models.parameters import Parameters
from swagger_server.models.parameters1 import Parameters1
from datetime import date, datetime
from typing import List, Dict
from six import iteritems
from ..util import deserialize_date, deserialize_datetime


def ep1_post(Parameters=None):
    """                                                                                                                                
    ep1_post                                                                                                                           
    first end point                                                                                                                    
    :param Parameters:                                                                                                                 
    :type Parameters: dict | bytes                                                                                                     
                                                                                                                                       
    :rtype: None                                                                                                                       
    """
    if connexion.request.is_json:
        Parameters = Parameters.from_dict(connexion.request.get_json())
    return "a value was: %s " % Parameters.a ## CHANGED ##


def ep2_post(Parameters=None):
    """                                                                                                                                
    ep2_post                                                                                                                           
    first end point                                                                                                                    
    :param Parameters:                                                                                                                 
    :type Parameters: dict | bytes                                                                                                     
                                                                                                                                       
    :rtype: None                                                                                                                       
    """
    if connexion.request.is_json:
        Parameters = Parameters1.from_dict(connexion.request.get_json())
    return "b value was: %s " % Parameters.b ## CHANGED ##

(Only the two return statements differ between the two snippets.)

Build and run the Docker container to stand up the server
% docker build -t swagger_server .  && docker run -p 8080:8080 swagger_server
Sending build context to Docker daemon  33.28kB
Step 1/9 : FROM python:3-alpine
 ---> a6beab4fa70b
Step 2/9 : RUN mkdir -p /usr/src/app
 ---> Using cache
 ---> cf9fb907b434
Step 3/9 : WORKDIR /usr/src/app
 ---> Using cache
 ---> 31b21fb96261
Step 4/9 : COPY requirements.txt /usr/src/app/
 ---> Using cache
 ---> 64f10e126ee6
Step 5/9 : RUN pip3 install --no-cache-dir -r requirements.txt
 ---> Using cache
 ---> 4c5ecd5e85ab
Step 6/9 : COPY . /usr/src/app
 ---> cd1178a9cc15
Step 7/9 : EXPOSE 8080
 ---> Running in 856990e4b02c
 ---> 6a19015794cc
Removing intermediate container 856990e4b02c
Step 8/9 : ENTRYPOINT python3
 ---> Running in 90681a6fd577
 ---> 8c4f22ea3334
Removing intermediate container 90681a6fd577
Step 9/9 : CMD -m swagger_server
 ---> Running in f8c112f34ff7
 ---> 2df47282f108
Removing intermediate container f8c112f34ff7
Successfully built 2df47282f108
Successfully tagged swagger_server:latest
 * Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
Make simple requests against the two end points using curl
% curl -i -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{"a" : 5}' localhost:8080/ep1 
HTTP/1.0 500 INTERNAL SERVER ERROR
Content-Type: application/problem+json
Content-Length: 252
Server: Werkzeug/0.12.2 Python/3.6.2
Date: Tue, 07 Nov 2017 19:45:28 GMT

{
  "detail": "The server encountered an internal error and was unable to complete your request.  Either the server is overloaded or there is an error in the application.",
  "status": 500,
  "title": "Internal Server Error",
  "type": "about:blank"
}
% curl -i -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{"b" : "5"}' localhost:8080/ep2
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 18
Server: Werkzeug/0.12.2 Python/3.6.2
Date: Tue, 07 Nov 2017 19:45:39 GMT

"b value was: 5 "
iev@iev-mbp iev % 

The corresponding output from the Docker container is:

[2017-11-07 19:45:28,152] ERROR in app: Exception on /ep1 [POST]
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/flask/app.py", line 1982, in wsgi_app
    response = self.full_dispatch_request()
  File "/usr/local/lib/python3.6/site-packages/flask/app.py", line 1614, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/usr/local/lib/python3.6/site-packages/flask/app.py", line 1517, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/local/lib/python3.6/site-packages/flask/_compat.py", line 33, in reraise
    raise value
  File "/usr/local/lib/python3.6/site-packages/flask/app.py", line 1612, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/local/lib/python3.6/site-packages/flask/app.py", line 1598, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/usr/local/lib/python3.6/site-packages/connexion/decorators/decorator.py", line 66, in wrapper
    response = function(request)
  File "/usr/local/lib/python3.6/site-packages/connexion/decorators/validation.py", line 122, in wrapper
    response = function(request)
  File "/usr/local/lib/python3.6/site-packages/connexion/decorators/validation.py", line 293, in wrapper
    return function(request)
  File "/usr/local/lib/python3.6/site-packages/connexion/decorators/decorator.py", line 42, in wrapper
    response = function(request)
  File "/usr/local/lib/python3.6/site-packages/connexion/decorators/parameter.py", line 195, in wrapper
    return function(**kwargs)
  File "/usr/src/app/swagger_server/controllers/default_controller.py", line 20, in ep1_post
    Parameters = Parameters.from_dict(connexion.request.get_json())
AttributeError: 'dict' object has no attribute 'from_dict'
172.17.0.1 - - [07/Nov/2017 19:45:28] "POST /ep1 HTTP/1.1" 500 -
172.17.0.1 - - [07/Nov/2017 19:45:39] "POST /ep2 HTTP/1.1" 200 -

which reveals the issue.

Suggest a fix/enhancement

The problem is that the name Parameters is being used for both the arguments to the function that processes the POST for ep1 and the module that encodes the arguments to that end point. So the name is used twice, once as a dict and once as an object.

I can fix this manually by renaming the module Parameters to Parameters0 -- and then updating the file name from models/parameters.py to models/parameters0.py and all the imports similarly. Doing that in codegen could be one fix -- just always postpend a number to the name when creating module names, but never for arugments to a function, making the namespaces disjoint. This also makes the naming of the modules a little bit more uniform, which is good -- right now, if you have n endpoints that take parameters, you get n-1 numbered parameter classes and only one non-numbered ones. You could also start counting at 1; I picked 0 just to not need to rename other modules and make a smaller change.

You could also refer to one as "Params" and the other as "Parameters", or similar, and get the same effect.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the generated swagger_server/controllers/default_controller.py shown in the issue and reproduce the failure using the provided Swagger definition, python-flask generation command, and curl requests. Trace the corresponding generator template or entry point that produces the controller, then verify that requests using a body parameter named Parameters no longer crash while the other endpoint remains functional.

Written by the indexing model from the issue text.

Assessment

Tech stack
docker, flask, python
Domain
api, backend, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
28/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.