python-restx / flask-restx

Fork of Flask-RESTPlus: Fully featured framework for fast, easy and documented API development with Flask
https://flask-restx.readthedocs.io/en/latest/
Other
2.16k stars 335 forks source link

Not rendering swagger schema when multifile param and body is present #471

Open mkorycinski opened 2 years ago

mkorycinski commented 2 years ago

Hi,

I am developing an API with a POST method endpoint accepting:

Params are defined in the following way:

def authorization_param(ns: Namespace, parser: Optional[RequestParser] = None) -> RequestParser:
    if not parser:
        parser = ns.parser()
    parser.add_argument('Authorization', location='headers', required=False, default='Bearer ')
    return parser

def multiple_file_param(arg_name: str, ns: Namespace, parser: Optional[RequestParser] = None) -> RequestParser:
    if not parser:
        parser = ns.parser()
    parser.add_argument(arg_name, type=FileStorage, location='files', required=True, action='append')
    return parser

Model:

some_form_model = api.model('form', {'field': fields.String())

In the endpoint module:

ns = Namespace('sth', description='Some stuff'))
auth_param =  authorization_param(ns=ns)
file_param = multiple_file_param(arg_name='File', ns=ns)

@ns.route('/files')
class PreprocessFiles(Resource):

    @ns.expect(auth_param, file_param, some_form_model)
    def post(self):
        payload = request.get_json()
        # do some stuff..
        return {'text': 'ok'}, 201

Error Messages/Stack Trace

2022-08-27 13:06:46.642 ERROR flask_restx.api api.__schema__: Unable to render schema
Traceback (most recent call last):
  File "D:\project\venv\lib\site-packages\flask_restx\api.py", line 571, in __schema__
    self._schema = Swagger(self).as_dict()
  File "D:\project\venv\lib\site-packages\flask_restx\swagger.py", line 239, in as_dict
    serialized = self.serialize_resource(
  File "D:\project\venv\lib\site-packages\flask_restx\swagger.py", line 446, in serialize_resource
    path[method] = self.serialize_operation(doc, method)
  File "D:\project\venv\lib\site-packages\flask_restx\swagger.py", line 469, in serialize_operation
    if any(p["type"] == "file" for p in all_params):
  File "D:\project\venv\lib\site-packages\flask_restx\swagger.py", line 469, in <genexpr>
    if any(p["type"] == "file" for p in all_params):
KeyError: 'type'

The error is raised by the following method from flask_restx.swagger:

    def serialize_operation(self, doc, method):
        operation = {
            "responses": self.responses_for(doc, method) or None,
            "summary": doc[method]["docstring"]["summary"],
            "description": self.description_for(doc, method) or None,
            "operationId": self.operation_id_for(doc, method),
            "parameters": self.parameters_for(doc[method]) or None,
            "security": self.security_for(doc, method),
        }
        # Handle 'produces' mimetypes documentation
        if "produces" in doc[method]:
            operation["produces"] = doc[method]["produces"]
        # Handle deprecated annotation
        if doc.get("deprecated") or doc[method].get("deprecated"):
            operation["deprecated"] = True
        # Handle form exceptions:
        doc_params = list(doc.get("params", {}).values())
        all_params = doc_params + (operation["parameters"] or [])
        if all_params and any(p["in"] == "formData" for p in all_params):
            if any(p["type"] == "file" for p in all_params):
                operation["consumes"] = ["multipart/form-data"]
            else:
                operation["consumes"] = [
                    "application/x-www-form-urlencoded",
                    "multipart/form-data",
                ]
        operation.update(self.vendor_fields(doc, method))
        return not_none(operation)

specifically:

 if all_params and any(p["in"] == "formData" for p in all_params):
            if any(p["type"] == "file" for p in all_params):
                operation["consumes"] = ["multipart/form-data"]
            else:
                operation["consumes"] = [
                    "application/x-www-form-urlencoded",
                    "multipart/form-data",
                ]

It is raised since body parameter does not have type key. If I list here all_param variable it will be:

[
  {
    "name": "File",
    "in": "formData",
    "type": "array",
    "required": true,
    "items": {
      "type": "file"
    },
    "collectionFormat": "multi"
  },
  {
    "name": "Authorization",
    "in": "header",
    "type": "string",
    "default": "Bearer "
  },
  {
    "name": "payload",
    "required": true,
    "in": "body",
    "schema": {
      "$ref": "#/definitions/PipelineConfig"
    }
  }
]

Same error happens if I change the way I decorate endpoint class and method:

@ns.route('/files')
@ns.expect(auth_param, file_param)
class PreprocessFiles(Resource):

    @ns.expect(some_form_model)
    def post(self):
        payload = request.get_json()
        # do some stuff..
        return {'text': 'ok'}, 201

As I have started testing following modification to the 'faulty' code shall be enough as it allows to render schema and have documentation with all required parameters.

all_params = doc_params + (operation["parameters"] or [])
        if all_params and any(p["in"] == "formData" for p in all_params):
                if any(p.get("type", None) == "file" for p in all_params):
                    operation["consumes"] = ["multipart/form-data"]
                else:
                    operation["consumes"] = [
                        "application/x-www-form-urlencoded",
                        "multipart/form-data",
                    ]

I will happily make a pull request if such a solution is good enough. However, perhaps this issue can be handled in another way.

Environment

peter-doggart commented 2 years ago

I think the issue is that api.expect() and namespace.expect() expect you to pass a single expected model (or a list of the same expected model) or request parser? I can't exactly recreate the issue on my side for some reason.

My suggestion would be to document the authorizations separately, as described here: https://flask-restx.readthedocs.io/en/latest/swagger.html#documenting-authorizations

Then you can also build models that are composed of other models using fields.nested().