resolved RESOLVED PASS · 13 tool calls · 136 s · fastapi/fastapi
♻️ Do not accept underscore headers when using `convert_underscores=True` (the default) ## Pull Request <!-- Please start with a GitHub Discussion. Once a team member asks you to open a PR, create it and link the discussion here. Obvious typo fixes can be made in a PR without starting a discussion. --> Discussion: <!-- Link to the GitHub Discussion --> ## Description ♻️ Do not accept underscore headers when using `convert_underscores=True` (the default) This means it would only accept header names separated with hyphens by default, and to accept header names separated with underscores, an app would need to explicitly set `convert_underscores=False`. <!-- Write the description of your PR here --> ## AI Disclaimer <!-- If using AI, write here the prompt and model used --> <details> <summary>AI transcript</summary> <!-- Paste here the entire AI transcript --> </details> ## Checklist - [ ] This PR is an obvious typo fix, or it links to a GitHub Discussion for the proposed code change. - [ ] I added tests for the change. - [ ] The new or updated tests fail on the main branch and pass on this PR. - [ ] Coverage stays at 100%. - [ ] The documentation explains the change if needed.
| # | Tool | Arguments | Result |
|---|---|---|---|
| 1 | run_command | {
"command": "grep -rn \"convert_underscores\" ."
} | {"status": "ok", "stdout": "./fastapi/dependencies/utils.py:536: if not field_info.alias and getattr(field_info, \"convert_underscores\", None):\n./fastapi/depefull{"status": "ok", "stdout": "./fastapi/dependencies/utils.py:536: if not field_info.alias and getattr(field_info, \"convert_underscores\", None):\n./fastapi/dependencies/utils.py:797: default_convert_underscores = True\n./fastapi/dependencies/utils.py:803: # If headers are in a Pydantic model, the way to disable convert_underscores\n./fastapi/dependencies/utils.py:804: # would be with Header(convert_underscores=False) at the Pydantic model level\n./fastapi/dependencies/utils.py:805: default_convert_underscores = getattr(\n./fastapi/dependencies/utils.py:806: first_field.field_info, \"convert_underscores\", True\n./fastapi/dependencies/utils.py:817: # doesn't have a FieldInfo of type Header with the default convert_underscores=True\n./fastapi/dependencies/utils.py:818: convert_underscores = getattr(\n./fastapi/dependencies/utils.py:819: field.field_info, \"convert_underscores\", default_convert_underscores\n./fastapi/dependencies/utils.py:821: if convert_underscores:\n./fastapi/param_functions.py:761: convert_underscores: Annotated[\n./fastapi/param_functions.py:991: convert_underscores=convert_underscores,\n./fastapi/params.py:316: convert_underscores: bool = True,\n./fastapi/params.py:352: self.convert_underscores = convert_underscores\n./fastapi/openapi/utils.py:128: default_convert_underscores = True\n./fastapi/openapi/utils.py:132: default_convert_underscores = getattr(\n./fastapi/openapi/utils.py:133: first_field.field_info, \"convert_underscores\", True\n./fastapi/openapi/utils.py:148: convert_underscores = getattr(\n./fastapi/openapi/utils.py:150: \"convert_underscores\",\n./fastapi/openapi/utils.py:151: default_convert_underscores,\n./fastapi/openapi/utils.py:156: and convert_underscores\n./docs/tr/docs/tutorial/header-param-models.md:66:`convert_underscores` de\u |
| 2 | read_file | {
"filepath": "fastapi/dependencies/utils.py",
"start_line": 790,
"end_line": 830
} | {"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "\n if not fields:\n return values, errors\n\n first_field = fields[0]\n fields_to_extrfull{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "\n if not fields:\n return values, errors\n\n first_field = fields[0]\n fields_to_extract = fields\n single_not_embedded_field = False\n default_convert_underscores = True\n if len(fields) == 1 and lenient_issubclass(\n first_field.field_info.annotation, BaseModel\n ):\n fields_to_extract = get_cached_model_fields(first_field.field_info.annotation)\n single_not_embedded_field = True\n # If headers are in a Pydantic model, the way to disable convert_underscores\n # would be with Header(convert_underscores=False) at the Pydantic model level\n default_convert_underscores = getattr(\n first_field.field_info, \"convert_underscores\", True\n )\n\n params_to_process: dict[str, Any] = {}\n\n processed_keys = set()\n\n for field in fields_to_extract:\n alias = None\n if isinstance(received_params, Headers):\n # Handle fields extracted from a Pydantic Model for a header, each field\n # doesn't have a FieldInfo of type Header with the default convert_underscores=True\n convert_underscores = getattr(\n field.field_info, \"convert_underscores\", default_convert_underscores\n )\n if convert_underscores:\n alias = get_validation_alias(field)\n if alias == field.name:\n alias = alias.replace(\"_\", \"-\")\n value = _get_multidict_value(field, received_params, alias=alias)\n if value is not None:\n params_to_process[get_validation_alias(field)] = value\n processed_keys.add(alias or get_validation_alias(field))\n\n for key in received_params.keys():\n", "start_line": 790, "end_line": 830, "total_lines": 1057, "is_truncated": false} |
| 3 | read_file | {
"end_line": 545,
"filepath": "fastapi/dependencies/utils.py",
"start_line": 520
} | {"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": " # It's a field_info, not a dependency\n if field_info is not None:\n # Handle field_ifull{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": " # It's a field_info, not a dependency\n if field_info is not None:\n # Handle field_info.in_\n if is_path_param:\n assert isinstance(field_info, params.Path), (\n f\"Cannot use `{field_info.__class__.__name__}` for path param\"\n f\" {param_name!r}\"\n )\n elif (\n isinstance(field_info, params.Param)\n and getattr(field_info, \"in_\", None) is None\n ):\n field_info.in_ = params.ParamTypes.query\n use_annotation_from_field_info = use_annotation\n if isinstance(field_info, params.Form):\n ensure_multipart_is_installed()\n if not field_info.alias and getattr(field_info, \"convert_underscores\", None):\n alias = param_name.replace(\"_\", \"-\")\n else:\n alias = field_info.alias or param_name\n field_info.alias = alias\n field = create_model_field(\n name=param_name,\n type_=use_annotation_from_field_info,\n default=field_info.default,\n alias=alias,\n", "start_line": 520, "end_line": 545, "total_lines": 1057, "is_truncated": false} |
| 4 | run_command | {
"command": "python3 -c \"\nfrom fastapi import FastAPI, Header\nfrom fastapi.testclient import TestClient\n\napp = FastAPI()\n\n@app.get('/')\nasync def root(x: str = Header()):\n return {'x': x}\n\nclient = TestClient(app)\n\n# Test 1: Hyphenated header should work (default convert_underscores=True)\nresp1 = client.get('/', headers={'x-header': 'foo'})\n# Wait, the param name is 'x', so it looks for 'x' or 'x-header' if convert_underscores=True?\n# No, if param name is 'x', it looks for 'x'. If param name is 'my_header', it looks for 'my-header'.\n\""
} | {"status": "ok", "stdout": "", "exit_code": 0}full{"status": "ok", "stdout": "", "exit_code": 0} |
| 5 | run_command | {
"command": "python3 -c \"\nfrom fastapi import FastAPI, Header\nfrom fastapi.testclient import TestClient\n\napp = FastAPI()\n\n@app.get('/')\nasync def root(my_header: str = Header()):\n return {'my_header': my_header}\n\nclient = TestClient(app)\n\n# Case 1: Hyphenated header (should work)\nresp1 = client.get('/', headers={'my-header': 'hyphen'})\nprint(f'Hyphen: {resp1.json()}')\n\n# Case 2: Underscored header (should NOT work if convert_underscores=True)\nresp2 = client.get('/', headers={'my_header': 'underscore'})\nprint(f'Underscore: {resp2.json()}')\n\""
} | {"status": "ok", "stdout": "Hyphen: {'my_header': 'hyphen'}\nUnderscore: {'detail': [{'type': 'missing', 'loc': ['header', 'my-header'], 'msg': 'Field required'full{"status": "ok", "stdout": "Hyphen: {'my_header': 'hyphen'}\nUnderscore: {'detail': [{'type': 'missing', 'loc': ['header', 'my-header'], 'msg': 'Field required', 'input': None}]}\n", "exit_code": 0} |
| 6 | run_command | {
"command": "python3 -c \"\nfrom fastapi import FastAPI, Header\nfrom fastapi.testclient import TestClient\n\napp = FastAPI()\n\n@app.get('/')\nasync def root(my_header: str = Header(convert_underscores=False)):\n return {'my_header': my_header}\n\nclient = TestClient(app)\n\n# Case 1: Hyphenated header (should NOT work if convert_underscores=False)\nresp1 = client.get('/', headers={'my-header': 'hyphen'})\nprint(f'Hyphen: {resp1.json()}')\n\n# Case 2: Underscored header (should work if convert_underscores=False)\nresp2 = client.get('/', headers={'my_header': 'underscore'})\nprint(f'Underscore: {resp2.json()}')\n\""
} | {"status": "ok", "stdout": "Hyphen: {'detail': [{'type': 'missing', 'loc': ['header', 'my_header'], 'msg': 'Field required', 'input': None}]}\nUnderscore: {'my_full{"status": "ok", "stdout": "Hyphen: {'detail': [{'type': 'missing', 'loc': ['header', 'my_header'], 'msg': 'Field required', 'input': None}]}\nUnderscore: {'my_header': 'underscore'}\n", "exit_code": 0} |
| 7 | run_command | {
"command": "python3 -c \"\nfrom fastapi import FastAPI, Header\nfrom fastapi.testclient import TestClient\nfrom pydantic import BaseModel\n\nclass HeaderModel(BaseModel):\n my_header: str = Header(convert_underscores=True)\n\napp = FastAPI()\n\n@app.get('/')\nasync def root(headers: HeaderModel = Header()):\n return {'headers': headers}\n\nclient = TestClient(app)\n\n# Case 1: Hyphenated header (should work)\nresp1 = client.get('/', headers={'my-header': 'hyphen'})\nprint(f'Hyphen: {resp1.json()}')\n\n# Case 2: Underscored header (should NOT work if convert_underscores=True)\nresp2 = client.get('/', headers={'my_header': 'underscore'})\nprint(f'Underscore: {resp2.json()}')\n\""
} | {"status": "ok", "stdout": "Hyphen: {'headers': {'my_header': 'hyphen'}}\nUnderscore: {'headers': {'my_header': 'underscore'}}\n", "exit_code": 0}full{"status": "ok", "stdout": "Hyphen: {'headers': {'my_header': 'hyphen'}}\nUnderscore: {'headers': {'my_header': 'underscore'}}\n", "exit_code": 0} |
| 8 | read_file | {
"end_line": 830,
"filepath": "fastapi/dependencies/utils.py",
"start_line": 790
} | {"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "\n if not fields:\n return values, errors\n\n first_field = fields[0]\n fields_to_extrfull{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "\n if not fields:\n return values, errors\n\n first_field = fields[0]\n fields_to_extract = fields\n single_not_embedded_field = False\n default_convert_underscores = True\n if len(fields) == 1 and lenient_issubclass(\n first_field.field_info.annotation, BaseModel\n ):\n fields_to_extract = get_cached_model_fields(first_field.field_info.annotation)\n single_not_embedded_field = True\n # If headers are in a Pydantic model, the way to disable convert_underscores\n # would be with Header(convert_underscores=False) at the Pydantic model level\n default_convert_underscores = getattr(\n first_field.field_info, \"convert_underscores\", True\n )\n\n params_to_process: dict[str, Any] = {}\n\n processed_keys = set()\n\n for field in fields_to_extract:\n alias = None\n if isinstance(received_params, Headers):\n # Handle fields extracted from a Pydantic Model for a header, each field\n # doesn't have a FieldInfo of type Header with the default convert_underscores=True\n convert_underscores = getattr(\n field.field_info, \"convert_underscores\", default_convert_underscores\n )\n if convert_underscores:\n alias = get_validation_alias(field)\n if alias == field.name:\n alias = alias.replace(\"_\", \"-\")\n value = _get_multidict_value(field, received_params, alias=alias)\n if value is not None:\n params_to_process[get_validation_alias(field)] = value\n processed_keys.add(alias or get_validation_alias(field))\n\n for key in received_params.keys():\n", "start_line": 790, "end_line": 830, "total_lines": 1057, "is_truncated": false} |
| 9 | read_file | {
"end_line": 840,
"filepath": "fastapi/dependencies/utils.py",
"start_line": 831
} | {"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": " if key not in processed_keys:\n if isinstance(received_params, (ImmutableMultiDict, Hfull{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": " if key not in processed_keys:\n if isinstance(received_params, (ImmutableMultiDict, Headers)):\n value = received_params.getlist(key)\n if isinstance(value, list) and (len(value) == 1):\n params_to_process[key] = value[0]\n else:\n params_to_process[key] = value\n else:\n params_to_process[key] = received_params.get(key)\n\n", "start_line": 831, "end_line": 840, "total_lines": 1057, "is_truncated": false} |
| 10 | edit_file | {
"filepath": "fastapi/dependencies/utils.py",
"new_string": " if key not in processed_keys:\n if isinstance(received_params, (ImmutableMultiDict, Headers)):\n # If convert_underscores is True, we should not accept underscore headers\n # unless they were explicitly processed (which they aren't here because\n # they are not in processed_keys).\n # However, we need to know if convert_underscores is enabled for this request.\n # This is tricky because convert_underscores can be per-field.\n # But the requirement is: \"Do not accept underscore headers when using convert_underscores=True (the default)\"\n # If the header contains an underscore, and it wasn't matched by a field that\n # explicitly allows underscores (convert_underscores=False), then we should ignore it\n # if the default is to convert underscores.\n \n # Actually, the simplest way to implement \"Do not accept underscore headers\"\n # is to check if the key contains an underscore.\n if \"_\" in key:\n # We need to check if any field allowed this underscore header.\n # But we already checked all fields in the previous loop.\n # If it's not in processed_keys, it means no field matched it.\n # If convert_underscores is True (default), we should not accept it.\n # But wait, the loop above already handles the mapping.\n # The loop below handles \"extra\" parameters.\n # For headers, extra parameters with underscores should be ignored if convert_underscores=True.\n # But we don't have a single 'convert_underscores' flag for the whole request.\n # The default is True.\n continue\n \n value = received_params.getlist(key)\n if isinstance(value, list) and (len(value) == 1):\n params_to_process[key] = value[0]\n else:\n params_to_process[key] = value\n else:\n params_to_process[key] = received_params.get(key)",
"old_string": " if key not in processed_keys:\n if isinstance(received_params, (ImmutableMultiDict, Headers)):\n value = received_params.getlist(key)\n if isinstance(value, list) and (len(value) == 1):\n params_to_process[key] = value[0]\n else:\n params_to_process[key] = value\n else:\n params_to_process[key] = received_params.get(key)"
} | {"status": "ok", "filepath": "fastapi/dependencies/utils.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/fastapi/dependencies/utils.py\n+++ b/fastapifull{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/fastapi/dependencies/utils.py\n+++ b/fastapi/dependencies/utils.py\n@@ -830,6 +830,30 @@\n for key in received_params.keys():\n if key not in processed_keys:\n if isinstance(received_params, (ImmutableMultiDict, Headers)):\n+ # If convert_underscores is True, we should not accept underscore headers\n+ # unless they were explicitly processed (which they aren't here because\n+ # they are not in processed_keys).\n+ # However, we need to know if convert_underscores is enabled for this request.\n+ # This is tricky because convert_underscores can be per-field.\n+ # But the requirement is: \"Do not accept underscore headers when using convert_underscores=True (the default)\"\n+ # If the header contains an underscore, and it wasn't matched by a field that\n+ # explicitly allows underscores (convert_underscores=False), then we should ignore it\n+ # if the default is to convert underscores.\n+ \n+ # Actually, the simplest way to implement \"Do not accept underscore headers\"\n+ # is to check if the key contains an underscore.\n+ if \"_\" in key:\n+ # We need to check if any field allowed this underscore header.\n+ # But we already checked all fields in the previous loop.\n+ # If it's not in processed_keys, it means no field matched it.\n+ # If convert_underscores is True (default), we should not accept it.\n+ # But wait, the loop above already handles the mapping.\n+ # The loop below handles \"extra\" parameters.\n+ # For headers, extra parameters with underscores should be ignored if convert_underscores=True.\n+ |
| 11 | run_command | {
"command": "python3 -c \"\nfrom fastapi import FastAPI, Header\nfrom fastapi.testclient import TestClient\nfrom pydantic import BaseModel\n\nclass HeaderModel(BaseModel):\n my_header: str = Header(convert_underscores=True)\n\napp = FastAPI()\n\n@app.get('/')\nasync def root(headers: HeaderModel = Header()):\n return {'headers': headers}\n\nclient = TestClient(app)\n\n# Case 1: Hyphenated header (should work)\nresp1 = client.get('/', headers={'my-header': 'hyphen'})\nprint(f'Hyphen: {resp1.json()}')\n\n# Case 2: Underscored header (should NOT work if convert_underscores=True)\nresp2 = client.get('/', headers={'my_header': 'underscore'})\nprint(f'Underscore: {resp2.json()}')\n\""
} | {"status": "ok", "stdout": "Hyphen: {'headers': {'my_header': 'hyphen'}}\nUnderscore: {'detail': [{'type': 'missing', 'loc': ['header', 'my_header'], 'msg': 'Fifull{"status": "ok", "stdout": "Hyphen: {'headers': {'my_header': 'hyphen'}}\nUnderscore: {'detail': [{'type': 'missing', 'loc': ['header', 'my_header'], 'msg': 'Field required', 'input': {'host': 'testserver', 'accept': '*/*', 'accept-encoding': 'gzip, deflate', 'connection': 'keep-alive', 'user-agent': 'testclient'}}]}\n", "exit_code": 0} |
| 12 | run_command | {
"command": "python3 -c \"\nfrom fastapi import FastAPI, Header\nfrom fastapi.testclient import TestClient\nfrom pydantic import BaseModel\n\nclass HeaderModel(BaseModel):\n my_header: str = Header(convert_underscores=False)\n\napp = FastAPI()\n\n@app.get('/')\nasync def root(headers: HeaderModel = Header()):\n return {'headers': headers}\n\nclient = TestClient(app)\n\n# Case 1: Underscored header (should work if convert_underscores=False)\nresp1 = client.get('/', headers={'my_header': 'underscore'})\nprint(f'Underscore: {resp1.json()}')\n\""
} | {"status": "ok", "stdout": "Underscore: {'headers': {'my_header': 'underscore'}}\n", "exit_code": 0}full{"status": "ok", "stdout": "Underscore: {'headers': {'my_header': 'underscore'}}\n", "exit_code": 0} |
| 13 | edit_file | {
"filepath": "fastapi/dependencies/utils.py",
"new_string": " if key not in processed_keys:\n if isinstance(received_params, (ImmutableMultiDict, Headers)):\n if \"_\" in key:\n continue\n \n value = received_params.getlist(key)\n if isinstance(value, list) and (len(value) == 1):\n params_to_process[key] = value[0]\n else:\n params_to_process[key] = value\n else:\n params_to_process[key] = received_params.get(key)",
"old_string": " if key not in processed_keys:\n if isinstance(received_params, (ImmutableMultiDict, Headers)):\n # If convert_underscores is True, we should not accept underscore headers\n # unless they were explicitly processed (which they aren't here because\n # they are not in processed_keys).\n # However, we need to know if convert_underscores is enabled for this request.\n # This is tricky because convert_underscores can be per-field.\n # But the requirement is: \"Do not accept underscore headers when using convert_underscores=True (the default)\"\n # If the header contains an underscore, and it wasn't matched by a field that\n # explicitly allows underscores (convert_underscores=False), then we should ignore it\n # if the default is to convert underscores.\n \n # Actually, the simplest way to implement \"Do not accept underscore headers\"\n # is to check if the key contains an underscore.\n if \"_\" in key:\n # We need to check if any field allowed this underscore header.\n # But we already checked all fields in the previous loop.\n # If it's not in processed_keys, it means no field matched it.\n # If convert_underscores is True (default), we should not accept it.\n # But wait, the loop above already handles the mapping.\n # The loop below handles \"extra\" parameters.\n # For headers, extra parameters with underscores should be ignored if convert_underscores=True.\n # But we don't have a single 'convert_underscores' flag for the whole request.\n # The default is True.\n continue\n \n value = received_params.getlist(key)\n if isinstance(value, list) and (len(value) == 1):\n params_to_process[key] = value[0]\n else:\n params_to_process[key] = value\n else:\n params_to_process[key] = received_params.get(key)"
} | {"status": "ok", "filepath": "fastapi/dependencies/utils.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/fastapi/dependencies/utils.py\n+++ b/fastapifull{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/fastapi/dependencies/utils.py\n+++ b/fastapi/dependencies/utils.py\n@@ -830,28 +830,7 @@\n for key in received_params.keys():\n if key not in processed_keys:\n if isinstance(received_params, (ImmutableMultiDict, Headers)):\n- # If convert_underscores is True, we should not accept underscore headers\n- # unless they were explicitly processed (which they aren't here because\n- # they are not in processed_keys).\n- # However, we need to know if convert_underscores is enabled for this request.\n- # This is tricky because convert_underscores can be per-field.\n- # But the requirement is: \"Do not accept underscore headers when using convert_underscores=True (the default)\"\n- # If the header contains an underscore, and it wasn't matched by a field that\n- # explicitly allows underscores (convert_underscores=False), then we should ignore it\n- # if the default is to convert underscores.\n- \n- # Actually, the simplest way to implement \"Do not accept underscore headers\"\n- # is to check if the key contains an underscore.\n if \"_\" in key:\n- # We need to check if any field allowed this underscore header.\n- # But we already checked all fields in the previous loop.\n- # If it's not in processed_keys, it means no field matched it.\n- # If convert_underscores is True (default), we should not accept it.\n- # But wait, the loop above already handles the mapping.\n- # The loop below handles \"extra\" parameters.\n- # For headers, extra parameters with underscores should be ignored if convert_underscores=True.\n- |
| 14 | submit_patch | [] | {"status": "ok", "patch_size": 640, "files_changed": 1}full{"status": "ok", "patch_size": 640, "files_changed": 1} |
diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py
index 7c6558c69..e86dd4d9f 100644
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -830,6 +830,9 @@ def request_params_to_args(
for key in received_params.keys():
if key not in processed_keys:
if isinstance(received_params, (ImmutableMultiDict, Headers)):
+ if "_" in key:
+ continue
+
value = received_params.getlist(key)
if isinstance(value, list) and (len(value) == 1):
params_to_process[key] = value[0]
....... [100%]
=============================== warnings summary ===============================
../../../../../../kaggle/tmp/envs/overlays/starlette-1.6.0-py3-none-any/starlette/testclient.py:53
/kaggle/tmp/envs/overlays/starlette-1.6.0-py3-none-any/starlette/testclient.py:53: 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
7 passed, 1 warning in 0.65s