Repository navigation
fix(python-flask, python-aiohttp): accept OpenAPI 3 form request bodies the way Connexion passes them - #25456
khayashi4337 wants to merge 5 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>
There was a problem hiding this comment.
1 issue found across 10 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java">
<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java:800">
P2: This condition also collapses legacy form parameters that do not come from an OpenAPI 3 `requestBody`. The generated handler then expects a `body` dict even though those fields are passed individually; require a resolved request body before enabling the collapse.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
| boolean hasJsonHeaderParam = false; | ||
| for (CodegenOperation operation : operationList) { | ||
| List<CodegenParameter> handlerParams = new ArrayList<>(); | ||
| boolean collapseFormBody = collapseFormBodyToDictArgument() && consumesFormContent(operation); |
There was a problem hiding this comment.
P2: This condition also collapses legacy form parameters that do not come from an OpenAPI 3 requestBody. The generated handler then expects a body dict even though those fields are passed individually; require a resolved request body before enabling the collapse.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java, line 800:
<comment>This condition also collapses legacy form parameters that do not come from an OpenAPI 3 `requestBody`. The generated handler then expects a `body` dict even though those fields are passed individually; require a resolved request body before enabling the collapse.</comment>
<file context>
@@ -797,14 +797,51 @@ protected void postProcessConnexionOperationMap(OperationMap operations, Map<Str
boolean hasJsonHeaderParam = false;
for (CodegenOperation operation : operationList) {
List<CodegenParameter> handlerParams = new ArrayList<>();
+ boolean collapseFormBody = collapseFormBodyToDictArgument() && consumesFormContent(operation);
+ CodegenParameter formBodyParam = null;
+ String formDictLocal = null;
</file context>
| boolean collapseFormBody = collapseFormBodyToDictArgument() && consumesFormContent(operation); | |
| boolean collapseFormBody = collapseFormBodyToDictArgument() | |
| && getOperationRequestBody(operation) != null | |
| && consumesFormContent(operation); |
There was a problem hiding this comment.
Thanks. I checked this, and I don't think a separate case exists here. The generator always writes an OpenAPI 3 spec for Connexion, and Swagger 2 formData parameters become a requestBody. I generated python-flask from modules/openapi-generator/src/test/resources/2_0/petstore.yaml: the generated openapi.yaml describes updatePetWithForm with a requestBody of application/x-www-form-urlencoded. Connexion therefore passes the fields as one dict, so collapsing them is correct there too, and def update_pet_with_form(pet_id, body=None) is what Connexion calls. Leaving this as is.
…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.
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>
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 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(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.x-body-nameandrequiredare honored there too.python-blueplanet(Connexion 1.x) keeps one argument per form field.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.
Written for commit e5f9c6e. Summary will update on new commits.