connexion icon indicating copy to clipboard operation
connexion copied to clipboard

Processing multipart/form-data with file fails if openAPI spec uses allOf and $ref to define properties

Open chrisinmtown opened this issue 1 year ago • 1 comments

Description

While migrating a Flask app I discovered that the connexion v3 multipart/form-data processing seems to get tripped up if a form's schema properties are defined with allOf and $ref. The use case is a file-upload interaction, basically send some metadata and contents of a single file. I'm testing with the SwaggerUI. This is a regression, the feature works fine in Connexion v2.

First I will show what works. This simple OpenAPI spec path accepts a list of two properties, name and file. Connexion v3 and python_multipart work together successfully to parse the input and invoke my function listed in the operationId, where I can retrieve the file content.

  /my/file/validate:
    post:
      operationId: my.controller.validate_file
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 100
                file:
                  type: string
                  format: binary
              required:
                - file
      responses:
        200:
          description: Validation passed

For completeness, here's the curl command that the SwaggerUI builds when I use the "Try it out" feature and submit the request:

url -X 'POST' \
  'http://localhost:5000/my/file/validate' \
  -H 'accept: application/json' \
  -H 'X-Forwarded-User: me' \
  -H 'Content-Type: multipart/form-data' \
  -F 'name=name' \
  -F '[email protected];type=text/plain'

Now that I've shown you what works, here's what doesn't work. This OpenAPI spec path uses allOf and a $ref to build the same list of properties as in the previous spec, name and file. The spec is valid according to openapi-spec-validator, the app starts OK, the SwaggerUI shows the exact same fields as above, and generates the same curl command as above. However, when connexion and python_multipart process the input, connexion emits the validation message that's shown below, and does not invoke the mapped function.

paths:
  /my/file/validate2:
    post:
      operationId: my.controller.validate_file2
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              allOf:
                - $ref: '#/components/schemas/header_name'
                - properties:
                    file:
                      type: string
                      format: binary
                  required:
                    - file
      responses:
        200:
          description: Validation passed

schemas:
    header_name:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100

Here's the complete output:

DEBUG python_multipart.multipart.callback Calling on_part_begin with no data
DEBUG python_multipart.multipart.callback Calling on_header_field with data[61:80]
DEBUG python_multipart.multipart.callback Calling on_header_value with data[82:104]
DEBUG python_multipart.multipart.callback Calling on_header_end with no data
DEBUG python_multipart.multipart.callback Calling on_headers_finished with no data
DEBUG python_multipart.multipart.callback Calling on_part_data with data[108:111]
DEBUG python_multipart.multipart.callback Calling on_part_end with no data
DEBUG python_multipart.multipart.callback Calling on_part_begin with no data
DEBUG python_multipart.multipart.callback Calling on_header_field with data[174:193]
DEBUG python_multipart.multipart.callback Calling on_header_value with data[195:257]
DEBUG python_multipart.multipart.callback Calling on_header_end with no data
DEBUG python_multipart.multipart.callback Calling on_header_field with data[259:271]
DEBUG python_multipart.multipart.callback Calling on_header_value with data[273:283]
DEBUG python_multipart.multipart.callback Calling on_header_end with no data
DEBUG python_multipart.multipart.callback Calling on_headers_finished with no data
DEBUG python_multipart.multipart.callback Calling on_part_data with data[287:10011]
DEBUG python_multipart.multipart.callback Calling on_part_end with no data
DEBUG python_multipart.multipart.callback Calling on_end with no data
ERROR connexion.validators.form_data._validate Validation error: [''] is not of type 'string' - 'file'
WARNING connexion.middleware.exceptions.problem_handler BadRequestProblem(status_code=400, detail="[''] is not of type 'string' - 'file'")
INFO uvicorn.access.send 127.0.0.1:63277 - "POST /my/file/validate2 HTTP/1.1" 400

Expected behaviour

Connexion v3 processes file-upload parameters as well as v2 when allOf and a $ref are used to compose the form properties.

Actual behaviour

Connexion v3 rejects a file-upload attempt with a mysterious message when allOf and a $ref are used.

Steps to reproduce

Here's app.py:

import connexion
from pathlib import Path


def validate_file():
    return {"valid": True}


def validate_file2():
    return {"valid": True}


app = connexion.FlaskApp(__name__, specification_dir="spec/")
app.add_api("openapi.yaml", arguments={"title": "Multipart"})


if __name__ == "__main__":
    app.run(f"{Path(__file__).stem}:app", port=8080)

Here's the spec openapi.yml:

openapi: "3.0.0"

info:
  title: Multipart
  version: "0.0"
servers:
  - url: /openapi

paths:

  /my/file/validate:
    post:
      operationId: app.validate_file
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 100
                file:
                  type: string
                  format: binary
              required:
                - file
      responses:
        200:
          description: Validation passed

  /my/file/validate2:
    post:
      operationId: app.validate_file2
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              allOf:
                - $ref: '#/components/schemas/header_name'
                - properties:
                    file:
                      type: string
                      format: binary
                  required:
                    - file
      responses:
        200:
          description: Validation passed

components:
  schemas:
    header_name:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100

Start the app and use Swagger UI to try out the endpoints.

Additional info:

Python 3.12 Connexion 3.1.0

chrisinmtown avatar Dec 13 '24 14:12 chrisinmtown

I have the same problem with the migration to connection 3

jhuot9 avatar Jan 22 '25 20:01 jhuot9