← oracle_full

fastapi_14303

failed WRONG_FIX UNSUBMITTED wrong_fix_unsubmitted(None) · None tool calls · 0 s · fastapi/fastapi

Task input

🐛 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 calls (0)

#ToolArgumentsResult
No trace captured.

Patch

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

Test output

show
==================================== 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