Skip to content
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

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

Open
chrisinmtown opened this issue Dec 13, 2024 · 0 comments

Comments

@chrisinmtown
Copy link
Contributor

chrisinmtown commented Dec 13, 2024

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

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

No branches or pull requests

1 participant