failed NO_PATCH NO_PATCH UNSUBMITTED empty_patch(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. | |||
.....F
=================================== FAILURES ===================================
____________________________ test_extra_param_list _____________________________
def test_extra_param_list():
response = client.post(
"/form-extra-allow/",
data={
"param": "123",
"extra_params": ["456", "789"],
},
)
assert response.status_code == 200, response.text
> assert response.json() == {
"param": "123",
"extra_params": ["456", "789"],
}
E AssertionError: assert {'param': '12...arams': '789'} == {'param': '12...'456', '789']}
E
E Omitting 1 identical items, use -vv to show
E Differing items:
E {'extra_params': '789'} != {'extra_params': ['456', '789']}
E Use -v to get more diff
tests/test_forms_single_model.py:177: AssertionError
=============================== 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 !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 5 passed, 1 warning in 0.64s