Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
rohan-97 opened this issue Jul 28, 2023 · 3 comments
Labels
kind/bug Indicates an issue

Comments

@rohan-97
Copy link

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\[email protected](\"/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

@rohan-97 rohan-97 added the kind/bug Indicates an issue label Jul 28, 2023
@jreinhar3dt
Copy link

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!

@rohan-97 rohan-97 changed the title [Bug]: Unable to send files through multipart/form if Flask when integrated with Openapi-Core [Bug]: Unable to send files through multipart/form in Flask when integrated with Openapi-Core Jul 31, 2023
@p1c2u
Copy link
Collaborator

p1c2u commented Oct 21, 2023

This should work now

@p1c2u p1c2u closed this as completed Nov 3, 2023
@seththoburn-cc
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/bug Indicates an issue
Projects
None yet
Development

No branches or pull requests

4 participants