Skip to content

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

Open
khayashi4337 wants to merge 13 commits into
OpenAPITools:masterfrom
khayashi4337:fix/python-connexion-form-body
Open

khayashi4337 wants to merge 13 commits into
OpenAPITools:masterfrom
khayashi4337:fix/python-connexion-form-body

Conversation

@khayashi4337

@khayashi4337 khayashi4337 commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

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 fix(python-flask, python-aiohttp): name handler arguments the way Connexion passes them #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 python-flask and python-aiohttp so OpenAPI 3 form request bodies are handled the way Connexion actually passes them: one dict argument named by the requestBody's 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.
  • Collapsing applies only to operations with an actual requestBody; legacy formData parameters stay individual arguments.
  • Field wire names are emitted as escaped Python string literals and escaped again in docstring references, so names with 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.
  • Collapsed fields' names, types, and descriptions fold into the dict argument's docstring line; file, header, and cookie docs are unchanged.
  • Webhook operations look up their requestBody, so x-body-name and required are honored there.
  • When the normalized operation path misses the spec key, spec path keys are matched after applying the same backslash normalization codegen uses, so the collapse still applies to escaped paths and to operations without an operationId.
  • 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, parameter/field locals that shadow the aiohttp request context, webhooks, a case-insensitive media type, escaped-backslash paths, 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.

Written for commit 79d7ac4. Summary will update on new commits.

View guided diff

…es the way Connexion passes them

Connexion (2.x for flask and aiohttp, and 3.x) 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. The generated
handlers declared one argument per form field instead. Any operation
with a required form field therefore failed with TypeError (500), and
optional fields were silently dropped. This includes the petstore
samples' update_pet_with_form and upload_file.

For operations that consume form content (the media type is compared
case-insensitively), the non-file form fields are now collapsed into
that single argument. The controller templates first copy it to a
collision-safe local (_form_body), then unpack each field into the same
local variable names as before, looked up by its wire name, so the
following template code (e.g. date conversion) keeps working. File
fields stay separate arguments. The docstring shows the argument as
:type ...: dict. python-blueplanet (Connexion 1.x) is unchanged.

The argument name is the requestBody's x-body-name, then, only for
Connexion 2, the schema-level x-body-name it still reads (Connexion 3
ignores it), then 'body'.

Known limitations, on Connexion's side: Connexion 2 doesn't pass form
bodies of GET/DELETE operations, and file fields are only passed when
their property name is already a valid snake_case Python name. An
x-body-name that is a Python builtin or 'callback' matches Connexion
once handler arguments follow Connexion's naming (OpenAPITools#25450).

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 10 files

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

khayashi4337 and others added 3 commits October 29, 2026 16:39
…ion-form-body

# Conflicts:
#	modules/openapi-generator/src/test/java/org/openapitools/codegen/python/AbstractPythonConnexionServerCodegenTest.java
…stent with what Connexion passes

Connexion passes the form body dict in a single argument whose name comes
from the requestBody x-body-name (or 'body'), overwriting any same-named
query/path argument. Non-file arguments colliding with that name are now
dropped from the signature and docstring with a warning, a same-named file
field is kept (it wins) with a warning, and the form dict initializer uses
an isinstance() check when a colliding name can still deliver a non-dict
value. Form field wire names are emitted as escaped Python string literals
through a new vendor extension rendered with triple braces, so names with
quotes, backslashes, or control characters produce valid code. Webhook
operations now resolve their requestBody too, so x-body-name and required
are honored there.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…nc with collapsed arguments

The collapsed form fields are not handler arguments anymore, so their
:param/:type docstring entries lied about the signature. Fold each
field's wire name, type and description into the dict argument's
:param line instead, and keep file fields documented as before.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 13 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

khayashi4337 and others added 3 commits October 30, 2026 02:20
…psed form field docstrings

A form field whose wire name contains an ASCII control character (e.g. a
NUL byte) made docstringFieldName emit the raw byte into the generated
controller's docstring, leaving source Python cannot load. Escape the
whole C0 range and DEL as \xNN, matching pythonStringLiteral, and cover
both the docstring and the .get() literal with a control-named field.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ion-form-body

# Conflicts:
#	modules/openapi-generator/src/test/java/org/openapitools/codegen/python/AbstractPythonConnexionServerCodegenTest.java
Legacy formData parameters share the form media type but are passed to
the handler individually; gate the dict collapse on the operation
actually declaring a requestBody.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 1 file (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

khayashi4337 and others added 6 commits November 2, 2026 18:04
operation.path is normalized before the exact paths lookup, so
operations whose spec path is rewritten (e.g. escaped backslashes)
lost their requestBody resolution and skipped the form-body collapse.
Fall back to matching the operationId across paths and webhooks.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
DefaultCodegen strips backslashes from operation paths, so a spec path
with escapes could not be found by the direct paths lookup. Replace the
operationId fallback (which misses specs without operationId) with an
exact match that applies the same normalization to spec path keys, and
add an escaped-backslash form-body regression case.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The path-based lookup applies codegen's own normalization to spec keys,
so it works whether or not the spec declares an operationId. Add an
escaped-backslash path without operationId and assert the form collapse
still fires (via the unique field name rather than the generated
operation name).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ion-form-body

Resolve the conflict in AbstractPythonConnexionServerCodegenTest by
keeping both sides' new tests: this branch's form-body collapse test
and upstream's JSON header content tests.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ion-form-body

Resolve conflict in AbstractPythonConnexionServerCodegenTest by keeping
both the form-body collapse tests (OpenAPITools#25456) and the aiohttp request-context
argument tests (OpenAPITools#25461).

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d form body

Under the form-body collapse the 'request' form field is extracted into a
local variable from the body dict instead of becoming a handler argument.
The context argument is still renamed to request_context so the local
variable does not shadow it.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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

Successfully merging this pull request may close these issues.

1 participant