fix(python-flask, python-aiohttp): accept OpenAPI 3 form request bodies the way Connexion passes them

KH
7h ago
+952-28

Connexion passes an OpenAPI 3 form request body (application/x-www-form-urlencoded or multipart/form-data) to the handler as one argument holding the non-file fields, already type-coerced, named by the requestBody's x-body-name (default body). Binary file fields are passed as separate arguments. I checked this on Connexion 2.14.2 (flask and aiohttp) and 3.3.0 (flask).

The generated python-flask and python-aiohttp handlers declare one argument per form field instead:

  • an operation with a required form field fails on every request with TypeError: ... missing 1 required positional argument (500);
  • optional form fields are silently dropped.

The petstore samples are affected, e.g. def update_pet_with_form(pet_id, name=None, status=None) and upload_file(...).

Changes

  • For operations that consume form content (the media type is compared case-insensitively), the non-file form fields are collapsed into that single argument. File fields stay separate arguments.
  • The controller templates copy the argument to a collision-safe local and unpack each field by its wire name into the same local variable names as before. So the rest of the template code (date conversion etc.) is unchanged, and a form field named body doesn't overwrite the dict:
    def update_pet_with_form(pet_id, body=None):
        _form_body = body or {}
        name = _form_body.get('name')
        status = _form_body.get('status')
    
  • The argument name is the requestBody's x-body-name. For Connexion 2 only, the schema-level x-body-name that it still reads comes next; Connexion 3 ignores it. Otherwise the name is body. The docstring shows it as dict.
  • python-blueplanet (Connexion 1.x) is unchanged.
  • Regenerated the petstore samples for python-flask, python-flask (Connexion 3) and python-aiohttp (pet_controller.py only).
  • The dict argument can still collide with another handler argument of the same Python name, because Connexion passes the body dict, the path and query parameters, and the file fields all as keyword arguments. Connexion 2.14.2 and 3.3.0 both behave as follows:
    • Path or query parameter with the same name: the dict overwrites it, so the parameter never reaches the handler. It is dropped from the generated signature, and a warning suggests a distinct x-body-name.
    • File field with the same name: the file wins over the dict, so the file stays an argument, and a warning is logged.
    • Header or cookie parameter with the same name: it is bound to a local variable of the same name, so a warning is logged and the form dict is read defensively (isinstance(..., dict)).
  • Form field names are emitted as escaped Python string literals (_form_body.get('it\'s')), so names with quotes, backslashes or control characters produce valid code and read the right key.
  • Webhook operations resolve their requestBody from webhooks too, so x-body-name and required are honored there.
  • The docstrings of the collapsed handlers no longer list the form fields as :params. Each field's name, type and description is folded into the dict argument's :param line, e.g. :param body: Form fields: name (str): Updated name of the pet; status (str): Updated status of the pet.

Verification

  • Python*Test / AbstractPython*Test pass. The new tests cover flask, flask with useConnexion3, and aiohttp: required and optional bodies, requestBody and schema-level x-body-name, file + fields, a camelCase path parameter with a required body, a field named body, an upper-case media type. They also check that blueplanet keeps its per-field arguments. The tests fail without the change.

  • Generated servers and sent real requests, on Connexion 2.14.2 (flask), Connexion 3.3.0 (flask, useConnexion3) and Connexion 2.14.2 (aiohttp):

    • urlencoded name=a&size=3&body=hello: 200, with name='a', size=3 (int) and body='hello' delivered;
    • multipart name=a plus a file: 200, with name and the file delivered.

    With the generator from master, the urlencoded request returns 500, and the multipart one returns 200 but loses name.

  • bin/generate-samples.sh bin/configs/python*.yaml: the only changes are the three petstore pet_controller.py files.

Known limitations (Connexion's side, unchanged)

  • Connexion 2 doesn't pass form bodies of GET/DELETE operations.
  • A file field is only passed when its property name is already a valid snake_case Python name: Connexion compares the raw multipart key with the function's argument names.
  • An x-body-name that is a Python builtin (e.g. input) or callback only matches Connexion's naming together with #25450.
  • The generated controller tests for form operations are still skipped, as before.

PR checklist

  • Read the contribution guidelines.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    Regenerated all bin/configs/python*.yaml samples and committed the three changed controllers. Generator options and docs are unchanged.
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

cc @cbornet @tomplus @krjakbrjak @fa0311 (Python technical committee)

Parts of this change were drafted with AI coding assistants (Devin, Claude Code, with reviews by Codex and Fable) and reviewed and verified by me.

šŸ¤– Generated with Claude Code


Summary by cubic

Fixes the python-flask and python-aiohttp generators to match how Connexion passes OpenAPI 3 form request bodies: one dict argument named by x-body-name (default body), with file fields staying separate. Required form fields no longer fail every request with a 500, and optional fields are no longer silently dropped.

  • Generated handlers declare that dict argument and unpack each form field into the same local variable names as before, so the rest of the template code is unchanged.
  • Field wire names are emitted as escaped Python string literals and escaped again in docstring references, so names containing quotes, backslashes, or control characters generate valid, loadable code.
  • Non-file parameters that collide with the dict argument name are dropped from the signature and docstring with a warning, since Connexion overwrites them; a same-named file field wins and the dict is read defensively.
  • The collapsed fields' names, types, and descriptions fold into the dict argument's docstring line; file, header, and cookie docs are unchanged.
  • Webhook operations now look up their requestBody, so x-body-name and required are honored there too.
  • python-blueplanet (Connexion 1.x) keeps one argument per form field.
  • Tests cover required/optional bodies, requestBody- and schema-level x-body-name (Connexion 2 only), file + fields, parameter collisions, webhooks, a case-insensitive media type, and blueplanet's per-field arguments.

Known Connexion-side limitations remain: GET/DELETE form bodies are not passed, and file fields only arrive when their property name is a valid snake_case Python name.