python-openapi/openapi-core

[Bug]: Unable to send files through multipart/form in Flask when integrated with Openapi-Core

rohan-97 opened this issue ยท 3 comments

Actual Behavior

Hello,

I am trying to Implement an API,
The API Takes a file as input in a REST API using multipart/form

I am using Flask to implement the REST API and following is the flask script

#!/usr/bin/python3
"""Test server."""

import os
import random
from flask import Flask, request, jsonify
from openapi_core.contrib.flask.decorators import FlaskOpenAPIViewDecorator
from openapi_core import Spec

SPEC = "file_reader.yaml"
openapi = FlaskOpenAPIViewDecorator.from_spec(Spec.from_file_path(SPEC))

app = Flask(__name__)

def __save_file_locally(file_object:object) -> str:
    if file_object.filename:
        filepath = os.path.join("/tmp/", f"{file_object.filename}.{random.randint(1,100)}")
        if os.path.exists(filepath):
            os.unlink(filepath)
        file_object.save(filepath)
        return filepath
    return None

@app.route("/test", methods=["POST"])
@openapi
def read_permission():
    """Test function"""
    # print(f"request OpenAPI dir : {request.openapi.body.items()}")
    __save_file_locally(request.files['inputFile'])
    return jsonify({"key": "Value"})

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=567, debug=True)

    # curl -X POST -H  "Content-type: application/json" --data '{"flag":"ttF"}' http://localhost:567/test

and following is the file_reader.yaml file

openapi: '3.0.2'
info:
  title: Test Title
  version: '1.0'
servers:
  - url: http://localhost:567/
paths:
  /test:
    post:
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                inputFile:
                  type: string 
                  format: binary
      responses:
        200:
          description: Sample response
          content:
            application/json:
              schema:
                type: object
                properties:
                  key:
                    type: string
                    minLength: 6
                    maxLength: 20

# curl -X POST -H  "Content-type: multipart/form-data" -F "file=@file_reader.py" http://localhost:567/test

However when I hit the API I get following validation error

root@ip-10-31-1-221:~/openapi_core_POC# curl -X POST -H  "Content-type: multipart/form-data" -F "file=@file_reader.py" http://localhost:567/test | python3 -m json.tool
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2884  100  1588  100  1296   258k   210k --:--:-- --:--:-- --:--:--  469k
{
    "errors": [
        {
            "class": "<class 'openapi_core.deserializing.media_types.exceptions.MediaTypeDeserializeError'>",
            "status": 400,
            "title": "Failed to deserialize value with multipart/form-data mimetype: --------------------------2c4bf22387dcbcbc\r\nContent-Disposition: form-data; name=\"file\"; filename=\"file_reader.py\"\r\nContent-Type: application/octet-stream\r\n\r\n#!/usr/bin/python3\n\"\"\"Test server.\"\"\"\n\nimport os\nimport random\nfrom flask import Flask, request, jsonify\nfrom openapi_core.contrib.flask.decorators import FlaskOpenAPIViewDecorator\nfrom openapi_core import Spec\n\nSPEC = \"file_reader.yaml\"\nopenapi = FlaskOpenAPIViewDecorator.from_spec(Spec.from_file_path(SPEC))\n\napp = Flask(__name__)\n\ndef __save_file_locally(file_object:object) -> str:\n    if file_object.filename:\n        filepath = os.path.join(\"/tmp/\", f\"{file_object.filename}.{random.randint(1,100)}\")\n        if os.path.exists(filepath):\n            os.unlink(filepath)\n        file_object.save(filepath)\n        return filepath\n    return None\n\n@app.route(\"/test\", methods=[\"POST\"])\n@openapi\ndef read_permission():\n    \"\"\"Test function\"\"\"\n    # print(f\"request OpenAPI dir : {request.openapi.body.items()}\")\n    __save_file_locally(request.files['inputFile'])\n    return jsonify({\"key\": \"Value\"})\n\nif __name__ == \"__main__\":\n    app.run(host=\"0.0.0.0\", port=567, debug=True)\n\n    # curl -X POST -H  \"Content-type: application/json\" --data '{\"flag\":\"ttF\"}' http://localhost:567/test\n\r\n--------------------------2c4bf22387dcbcbc--\r\n"
        }
    ]
}

Expected Behavior

It is expected that there should be no validation errors as we have specified mime type as multipart/form.

Also as per OpenAPI docs, OpenAPI 3.0 does not support type:file field,
and as per docs, while taking input we should specify type:string and format:binary

so just wanted to confirm whether the request body is correct or not

      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                inputFile:
                  type: string 
                  format: binary

Steps to Reproduce

Write the provided python3 file as file_reader.py and provided yaml file as file_reader.yaml

Execute file_reader.py file
and use following CURL command to make a request

curl -X POST -H  "Content-type: multipart/form-data" -F "file=@file_reader.py" http://localhost:567/test

OpenAPI Core Version

0.17.1

OpenAPI Core Integration

Flask

Affected Area(s)

Deserializing

References

No response

Anything else we need to know?

No response

Would you like to implement a fix?

Yes

Hi,

I've observed the same problem. It seems that the error is caused in data_form_loads(value) in deserializing/media_types/utils.py where the email.Parser parser expects the Content-type and boundary in the body value.

I am not sure if I am just using it wrong but with plain Python request.Response the value ends up being e.g.

--0ca931645faf0b3b0a25c2b2699a3959
Content-Disposition: form-data; name="file"; filename="test1.txt"

abcdefg
--0ca931645faf0b3b0a25c2b2699a3959--

which is not parsed correctly and raises the exception, instead of

Content-Type: multipart/form-data; boundary="0ca931645faf0b3b0a25c2b2699a3959"

--0ca931645faf0b3b0a25c2b2699a3959
Content-Disposition: form-data; name="file"; filename="test1.txt"

abcdefg
--0ca931645faf0b3b0a25c2b2699a3959--

what would be parsed correctly.

I don't know what's the appropriate way to fix this, unless I am using it wrong the parsing would need to have access to the header information describing the Content-Type and boundary.

Otherwise the library works flawlessly, nice project!

p1c2u commented

This should work now

I am getting a similar issue on 0.18.2. Mine is a RequestBodyValidationError. It's thrown by a MediaTypeDeserializeError further up. Here are the relevant parts of the stacktrace:

  File "openapi_core/deserializing/media_types/util.py", line 24, in <dictcomp>
    part.get_param("name", header="content-disposition"): part.get_payload(
AttributeError: 'str' object has no attribute 'get_param'

  File "openapi_core/deserializing/media_types/deserializers.py", line 31, in deserialize
    raise MediaTypeDeserializeError(self.mimetype, value)
openapi_core.deserializing.media_types.exceptions.MediaTypeDeserializeError: Failed to deserialize value with multipart/form-data mimetype: --------------------------5ccec95762968d0b
Content-Disposition: form-data; name="file"; filename="Rectangle 40044.png"
Content-Type: image/png


  File "openapi_core/validation/decorators.py", line 58, in _raise_error
    raise init(**kw) from exc
openapi_core.validation.request.exceptions.RequestBodyValidationError: Request body validation error

I have basically the same endpoint structure as the example above, and I'm using the same curl request.