swagger-api / swagger-api/swagger-codegen
generated server code crashes from using `Parameters` to name both function argument and model class
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
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 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