Skip to content

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

Description

@rohan-97

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

Activity

  1. jreinhar3dt commented on Jul 31, 2023

    @jreinhar3dt

    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!

  2. 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[/+] on Jul 31, 2023
  3. p1c2u commented on Oct 21, 2023

    @p1c2u
    Collaborator

    This should work now

  4. seththoburn-cc commented on Jan 15, 2024

    @seththoburn-cc

    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

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind/bugIndicates an issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions