failed WRONG_FIX UNSUBMITTED wrong_fix_unsubmitted(None) · None tool calls · 0 s · fastapi/fastapi
🐛 Fix parsing extra `Form` parameter list
## Description
Currently if we declare Form parameters as model with `extra="allow"` and then pass extra parameter with multiple values, only last value of such parameter will be passed to model validator:
```py
from fastapi import FastAPI, Form
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class Model(BaseModel):
param: str
model_config = {"extra": "allow"}
@app.post("/")
async def model_with_extra(data: Model = Form()):
return data
def test_pass_extra_list():
client = TestClient(app)
resp = client.post(
"/",
data={
"param": "123",
"param2": ["456", "789"], # Pass a list of values as extra parameter
},
)
assert resp.status_code == 200
assert resp.json() == {
"param": "123",
"param2": ["456", "789"],
}
# AssertionError: assert
# {'param': '123', 'param2': '789'}
# ==
# {'param': '123', 'param2': ['456', '789']}
```
The part of code that adds extra parameters is:
https://github.com/fastapi/fastapi/blob/972a967d5d3fa24d450637c36c6b7ad1f1d8c1b6/fastapi/dependencies/utils.py#L901-L903
The problem is in `received_body.items()` - it returns only last value for each key.
To fix this we need to iterate over all keys in `received_body` and get the values using `received_body.getlist(key)`:
```py
for key in received_body.keys():
if key not in values:
param_values = received_body.getlist(key)
if len(param_values) == 1:
values[key] = param_values[0]
else:
values[key] = param_values
```
## Alternative implementation
This solution parses single value as string
`extra=string` -> `{"extra": "string"}`
but multiple values as list of strings
`extra=string1&extra=string2` -> `{"extra": ["string1", "string2"]}`
We can consider always parse extra parameters as lists
```py
for key in received_body.keys():
if key not in values:
values[key] = received_body.getlist(key)
```
This way for single value we would also have list with one element:
`extra=string` -> `{"extra": ["string"]}`
It's more consistent, but probably not what people expect for single value.
I think we should go with current implementation (single value -> single string, multiple values -> list of strings)
## Other parameters
Query\Header\Cookie parameters likely have the same issue, I will look at it a bit later and open separate PR to fix this| # | Tool | Arguments | Result |
|---|---|---|---|
| No trace captured. | |||
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -903,9 +903,13 @@ async def process_fn(
if value is not None:
values[field.alias] = value
field_aliases = {field.alias for field in body_fields}
- for key, value in received_body.items():
+ for key in received_body.keys():
if key not in field_aliases:
- values[key] = value
+ param_values = received_body.getlist(key)
+ if len(param_values) == 1:
+ values[key] = param_values[0]
+ else:
+ values[key] = param_values
return values
==================================== ERRORS ====================================
______________ ERROR collecting tests/test_forms_single_model.py _______________
tests/test_forms_single_model.py:32: in <module>
@app.post("/form/")
^^^^^^^^^^^^^^^^^^
fastapi/routing.py:1078: in decorator
self.add_api_route(
fastapi/routing.py:1017: in add_api_route
route = route_class(
fastapi/routing.py:636: in __init__
self.dependant = get_dependant(
fastapi/dependencies/utils.py:259: in get_dependant
param_details = analyze_param(
fastapi/dependencies/utils.py:496: in analyze_param
ensure_multipart_is_installed()
fastapi/dependencies/utils.py:121: in ensure_multipart_is_installed
raise RuntimeError(multipart_not_installed_error) from None
E RuntimeError: Form data requires "python-multipart" to be installed.
E You can install "python-multipart" with:
E
E pip install python-multipart
=============================== warnings summary ===============================
../../../../../../../Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45
/Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45: DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead.
_PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]]
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.57s