Processing multipart/form-data with file fails if openAPI spec uses allOf and $ref to define properties
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
I have the same problem with the migration to connection 3