From 063b5bf582d31fb155cc6bc6f88cf512329d0fd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 23 May 2026 20:35:05 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Do=20not=20accept=20unders?= =?UTF-8?q?core=20headers=20when=20using=20`convert=5Funderscores=3DTrue`?= =?UTF-8?q?=20(the=20default)=20(#15589)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 4 +++ ..._query_cookie_header_model_extra_params.py | 35 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 7c6558c695..40dffba64b 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -826,6 +826,10 @@ def request_params_to_args( if value is not None: params_to_process[get_validation_alias(field)] = value processed_keys.add(alias or get_validation_alias(field)) + # For headers with convert_underscores=True, mark both the converted + # header name and the original field alias as processed to avoid + # accepting the original alias as an extra header. + processed_keys.add(get_validation_alias(field)) for key in received_params.keys(): if key not in processed_keys: diff --git a/tests/test_query_cookie_header_model_extra_params.py b/tests/test_query_cookie_header_model_extra_params.py index d361e1e533..3fd84dc00e 100644 --- a/tests/test_query_cookie_header_model_extra_params.py +++ b/tests/test_query_cookie_header_model_extra_params.py @@ -11,6 +11,10 @@ class Model(BaseModel): model_config = {"extra": "allow"} +class AuthHeaders(BaseModel): + x_user_id: str + + @app.get("/query") async def query_model_with_extra(data: Model = Query()): return data @@ -26,6 +30,11 @@ async def cookies_model_with_extra(data: Model = Cookie()): return data +@app.get("/header-requires-hyphen") +async def header_model_requires_hyphen(data: AuthHeaders = Header()): + return data + + def test_query_pass_extra_list(): client = TestClient(app) resp = client.get( @@ -91,6 +100,32 @@ def test_header_pass_extra_single(): assert resp_json["param2"] == "456" +def test_header_model_prefers_hyphenated_header_with_convert_underscores(): + client = TestClient(app) + + resp = client.get( + "/header-requires-hyphen", + headers=[ + ("x-user-id", "hyphenated-value"), + ("x_user_id", "underscore-value"), + ], + ) + + assert resp.status_code == 200 + assert resp.json() == {"x_user_id": "hyphenated-value"} + + +def test_header_model_rejects_underscore_header_with_convert_underscores(): + client = TestClient(app) + + resp = client.get( + "/header-requires-hyphen", headers={"x_user_id": "underscore-value"} + ) + + assert resp.status_code == 422 + assert resp.json()["detail"][0]["loc"] == ["header", "x_user_id"] + + def test_cookie_pass_extra_list(): client = TestClient(app) client.cookies = [