Repository navigation
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
Open
khayashi4337 wants to merge 13 commits into
khayashi4337 wants to merge 13 commits into
Conversation
…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>
Contributor
There was a problem hiding this comment.
All reported issues were addressed across 10 files
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
…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.
Contributor
There was a problem hiding this comment.
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
…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>
Contributor
There was a problem hiding this comment.
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Connexion passes an OpenAPI 3 form request body (
application/x-www-form-urlencodedormultipart/form-data) to the handler as one argument holding the non-file fields, already type-coerced, named by the requestBody'sx-body-name(defaultbody). 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:
TypeError: ... missing 1 required positional argument(500);The petstore samples are affected, e.g.
def update_pet_with_form(pet_id, name=None, status=None)andupload_file(...).Changes
bodydoesn't overwrite the dict:x-body-name. For Connexion 2 only, the schema-levelx-body-namethat it still reads comes next; Connexion 3 ignores it. Otherwise the name isbody. The docstring shows it asdict.pet_controller.pyonly).x-body-name.isinstance(..., dict))._form_body.get('it\'s')), so names with quotes, backslashes or control characters produce valid code and read the right key.webhookstoo, sox-body-nameandrequiredare honored there.:params. Each field's name, type and description is folded into the dict argument's:paramline, e.g.:param body: Form fields: name (str): Updated name of the pet; status (str): Updated status of the pet.Verification
Python*Test/AbstractPython*Testpass. The new tests cover flask, flask withuseConnexion3, and aiohttp: required and optional bodies, requestBody and schema-levelx-body-name, file + fields, a camelCase path parameter with a required body, a field namedbody, 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):name=a&size=3&body=hello: 200, withname='a',size=3(int) andbody='hello'delivered;name=aplus a file: 200, withnameand 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 petstorepet_controller.pyfiles.Known limitations (Connexion's side, unchanged)
x-body-namethat is a Python builtin (e.g.input) orcallbackonly matches Connexion's naming together with fix(python-flask, python-aiohttp): name handler arguments the way Connexion passes them #25450.PR checklist
bin/configs/python*.yamlsamples and committed the three changed controllers. Generator options and docs are unchanged.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-flaskandpython-aiohttpso OpenAPI 3 form request bodies are handled the way Connexion actually passes them: one dict argument named by the requestBody'sx-body-name(defaultbody), with file fields staying separate. Required form fields no longer fail every request with a 500, and optional fields are no longer silently dropped.formDataparameters stay individual arguments.x-body-nameandrequiredare honored there.operationId.python-blueplanet(Connexion 1.x) keeps one argument per form field.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.