Skip to content

Multipart handling is very confusing when files are involved; can the docs be clarified? #1811

@danielbprice

Description

@danielbprice

Description

Multipart handling is very confusing in Connexion 3.x. I found that:

  • If a client passes a non-file object to something which is expected to be a file, then the object winds up in the body parameter as a string.
  • If a developer declares a file-like parameter to be required, and makes it a positional argument (makes sense?!?), but the client passes a non-file under that parameter name, then the code will fail, because it will fail to supply a positional parameter.

Expected behaviour

The following code illustrates what seems like strange (or at least, not-documented) behavior:

openapi: "3.0.1"

info:
  title: Form Data
  version: "1.0"
servers:
  - url: /openapi

paths:
  /greeting:
    post:
      summary: Generate greeting
      operationId: hello.post_greeting
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                name:
                  type: string
                selfie:
                  type: string
                  format: binary
              required:
                - name
                - selfie
      responses:
        200:
          description: OK
from pathlib import Path
import connexion
from starlette.responses import PlainTextResponse, Response


async def post_greeting(body, selfie=None) -> Response:
    return PlainTextResponse(f"Hello body={body} selfie={selfie}!", status_code=201)


app = connexion.AsyncApp(__name__, specification_dir="spec")
app.add_api("openapi.yaml", arguments={"title": "Hello World Example"})


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

Actual behaviour

# pass non-file for selfie; required param validation still passes, but selfie param is "None"
$ curl -F name=dave -F selfie=nah http://localhost:8080/openapi/greeting
Hello body={'name': 'dave', 'selfie': 'nah'} selfie=None!

$ curl -F name=dave -F selfie=@128.png http://localhost:8080/openapi/greeting
Hello body={'name': 'dave'} selfie=[<starlette.datastructures.UploadFile object at 0x7f4587243df0>]!

# pass file for both, now body is empty, but required param validation still passes
$ curl -F name=@128.png -F selfie=@128.png http://localhost:8080/openapi/greeting
Hello body={} selfie=[<starlette.datastructures.UploadFile object at 0x7f4587243df0>]!

This makes for a very confusing development experience. I had mistakenly assumed that either:

  • The selfie param would have type starlette.datastructures.UploadFile | str OR
  • body.selfie would have type starlette.datastructures.UploadFile | str

I'm not saying that the current behavior is wrong, and, now that I have completely worked it out, I can program against it, but it feels non-obvious to me. Some greater guidance in the documentation would really help. I had to write about 6 test programs to sort out the behavior here. The documentation doesn't really say what the semantics of "body" is.

It also seems really confusing that string-type form parameters like name only seem to show up in the "body" parameter, but are not expanded into function params. This disparate treatment of these two kinds of things is really not clearly explained.

Steps to reproduce

See above

Additional info:

Output of the commands:

  • python --version: Python 3.10.13
  • pip show connexion | grep "^Version\:" 3.0.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions