resolved RESOLVED PASS · 22 tool calls · 82 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": 550,
"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 field_info=field_info,\n )\n if is_path_param:\n assert is_scalar_field(field=field), (\n \"Path params must be of one of the supported types\"\n", "start_line": 520, "end_line": 550, "total_lines": 1057, "is_truncated": false} |
| 4 | run_command | {
"command": "grep -rn \"Header\" fastapi/params.py"
} | {"status": "ok", "stdout": "303:class Header(Param): # type: ignore[misc]\n", "exit_code": 0}full{"status": "ok", "stdout": "303:class Header(Param): # type: ignore[misc]\n", "exit_code": 0} |
| 5 | read_file | {
"end_line": 360,
"filepath": "fastapi/params.py",
"start_line": 300
} | {"status": "ok", "filepath": "fastapi/params.py", "content": " )\n\n\nclass Header(Param): # type: ignore[misc]\n in_ = ParamTypes.header\n\n def __init__(\n sefull{"status": "ok", "filepath": "fastapi/params.py", "content": " )\n\n\nclass Header(Param): # type: ignore[misc]\n in_ = ParamTypes.header\n\n def __init__(\n self,\n default: Any = Undefined,\n *,\n default_factory: Callable[[], Any] | None = _Unset,\n annotation: Any | None = None,\n alias: str | None = None,\n alias_priority: int | None = _Unset,\n validation_alias: str | AliasPath | AliasChoices | None = None,\n serialization_alias: str | None = None,\n convert_underscores: bool = True,\n title: str | None = None,\n description: str | None = None,\n gt: float | None = None,\n ge: float | None = None,\n lt: float | None = None,\n le: float | None = None,\n min_length: int | None = None,\n max_length: int | None = None,\n pattern: str | None = None,\n regex: Annotated[\n str | None,\n deprecated(\n \"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead.\"\n ),\n ] = None,\n discriminator: str | None = None,\n strict: bool | None = _Unset,\n multiple_of: float | None = _Unset,\n allow_inf_nan: bool | None = _Unset,\n max_digits: int | None = _Unset,\n decimal_places: int | None = _Unset,\n examples: list[Any] | None = None,\n example: Annotated[\n Any | None,\n deprecated(\n \"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, \"\n \"although still supported. Use examples instead.\"\n ),\n ] = _Unset,\n openapi_examples: dict[str, Example] | None = None,\n deprecated: deprecated | str | bool | None = None,\n include_in_schema: bool = True,\n json_schema_extra: dict[str, Any] | None = None,\n **extra: Any,\n ):\n self.convert_underscores = convert_underscores\n |
| 6 | read_file | {
"end_line": 840,
"filepath": "fastapi/dependencies/utils.py",
"start_line": 810
} | {"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "\n processed_keys = set()\n\n for field in fields_to_extract:\n alias = None\n if isinfull{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "\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 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": 810, "end_line": 840, "total_lines": 1057, "is_truncated": false} |
| 7 | read_file | {
"end_line": 540,
"filepath": "fastapi/dependencies/utils.py",
"start_line": 530
} | {"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": " and getattr(field_info, \"in_\", None) is None\n ):\n field_info.in_ = params.ParamTyfull{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": " 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", "start_line": 530, "end_line": 540, "total_lines": 1057, "is_truncated": false} |
| 8 | run_command | {
"command": "grep -rn \"Header\" fastapi/dependencies/utils.py"
} | {"status": "ok", "stdout": "69: Headers,\n760: and isinstance(values, (ImmutableMultiDict, Headers))\n786: received_params: Mapping[str, Any] | QueryParams | Hefull{"status": "ok", "stdout": "69: Headers,\n760: and isinstance(values, (ImmutableMultiDict, Headers))\n786: received_params: Mapping[str, Any] | QueryParams | Headers,\n804: # would be with Header(convert_underscores=False) at the Pydantic model level\n815: if isinstance(received_params, Headers):\n817: # doesn't have a FieldInfo of type Header with the default convert_underscores=True\n832: if isinstance(received_params, (ImmutableMultiDict, Headers)):\n", "exit_code": 0} |
| 9 | run_command | {
"command": "grep -rn \"Headers\" fastapi/dependencies/utils.py"
} | {"status": "ok", "stdout": "69: Headers,\n760: and isinstance(values, (ImmutableMultiDict, Headers))\n786: received_params: Mapping[str, Any] | QueryParams | Hefull{"status": "ok", "stdout": "69: Headers,\n760: and isinstance(values, (ImmutableMultiDict, Headers))\n786: received_params: Mapping[str, Any] | QueryParams | Headers,\n815: if isinstance(received_params, Headers):\n832: if isinstance(received_params, (ImmutableMultiDict, Headers)):\n", "exit_code": 0} |
| 10 | read_file | {
"end_line": 100,
"filepath": "fastapi/dependencies/utils.py",
"start_line": 1
} | {"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "import dataclasses\nimport inspect\nimport sys\nfrom collections.abc import (\n AsyncGfull{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "import dataclasses\nimport inspect\nimport sys\nfrom collections.abc import (\n AsyncGenerator,\n AsyncIterable,\n AsyncIterator,\n Callable,\n Generator,\n Iterable,\n Iterator,\n Mapping,\n Sequence,\n)\nfrom contextlib import AsyncExitStack, contextmanager\nfrom copy import copy, deepcopy\nfrom dataclasses import dataclass\nfrom typing import (\n Annotated,\n Any,\n ForwardRef,\n Literal,\n Union,\n cast,\n get_args,\n get_origin,\n)\n\nfrom fastapi import params\nfrom fastapi._compat import (\n ModelField,\n RequiredParam,\n Undefined,\n copy_field_info,\n create_body_model,\n evaluate_forwardref,\n field_annotation_is_scalar,\n field_annotation_is_scalar_sequence,\n field_annotation_is_sequence,\n get_cached_model_fields,\n get_missing_field_error,\n is_bytes_or_nonable_bytes_annotation,\n is_bytes_sequence_annotation,\n is_scalar_field,\n is_uploadfile_or_nonable_uploadfile_annotation,\n is_uploadfile_sequence_annotation,\n lenient_issubclass,\n sequence_types,\n serialize_sequence_value,\n value_is_sequence,\n)\nfrom fastapi.background import BackgroundTasks\nfrom fastapi.concurrency import (\n asynccontextmanager,\n contextmanager_in_threadpool,\n)\nfrom fastapi.dependencies.models import Dependant\nfrom fastapi.exceptions import DependencyScopeError\nfrom fastapi.logger import logger\nfrom fastapi.security.oauth2 import SecurityScopes\nfrom fastapi.types import DependencyCacheKey\nfrom fastapi.utils import create_model_field, get_path_param_names\nfrom pydantic import BaseModel, Json\nfrom pydantic.fields import FieldInfo\nfrom starlette.background import BackgroundTasks as StarletteBackgroundTasks\nfrom starlette.concurrency import run_in_threadpool\nfrom starlette.datastructures import (\n FormData,\n Headers,\n ImmutableMultiDict,\n QueryParams,\n Uplo |
| 11 | write_file | {
"content": "\nfrom fastapi import FastAPI, Header\nfrom fastapi.testclient import TestClient\nfrom pydantic import BaseModel\n\napp = FastAPI()\n\n@app.get(\"/\")\nasync def root(x_token: str = Header()):\n return {\"x_token\": x_token}\n\nclient = TestClient(app)\n\ndef test_underscore_header_rejected():\n # By default, convert_underscores=True. \n # It should accept 'x-token' and map it to 'x_token'.\n # It should NOT accept 'x_token' as a header.\n response = client.get(\"/\", headers={\"x-token\": \"foo\"})\n assert response.status_code == 200\n assert response.json() == {\"x_token\": \"foo\"}\n\n # This is the case we want to ensure fails or is not accepted as the parameter\n # If we send 'x_token', it should not be mapped to 'x_token' if convert_underscores=True\n # Actually, if convert_underscores=True, FastAPI looks for 'x-token'.\n # If it doesn't find 'x-token', it might still find 'x_token' if it's not strictly forbidden.\n # The issue says \"Do not accept underscore headers when using convert_underscores=True\"\n \n # Let's see what happens now.\n response = client.get(\"/\", headers={\"x_token\": \"bar\"})\n # If it's currently accepted, this will be 200. We want it to be 422 (Unprocessable Entity)\n # because 'x_token' is an underscore header.\n print(f\"Response for x_token: {response.status_code}, {response.json()}\")\n\ndef test_underscore_header_accepted_when_disabled():\n @app.get(\"/disabled\")\n async def disabled(x_token: str = Header(convert_underscores=False)):\n return {\"x_token\": x_token}\n \n response = client.get(\"/disabled\", headers={\"x_token\": \"bar\"})\n assert response.status_code == 200\n assert response.json() == {\"x_token\": \"bar\"}\n\nif __name__ == \"__main__\":\n test_underscore_header_rejected()\n test_underscore_header_accepted_when_disabled()\n print(\"Tests passed!\")\n",
"filepath": "/tmp/test_underscore_headers.py"
} | {"status": "ok", "filepath": "/tmp/test_underscore_headers.py", "size": 1838}full{"status": "ok", "filepath": "/tmp/test_underscore_headers.py", "size": 1838} |
| 12-13 ×2 | run_command | {
"command": "python3 /tmp/test_underscore_headers.py"
} | {"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 2", "details": {"stdout": "", "stderr": "python3: can't open ffull{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 2", "details": {"stdout": "", "stderr": "python3: can't open file '/tmp/swe_work/eval4_submission_v1b/fastapi_15589/a/tmp/test_underscore_headers.py': [Errno 2] No such file or directory\n", "exit_code": 2}} |
| 14 | run_command | {
"command": "ls -R /tmp"
} | {"status": "ok", "stdout": "/tmp/swe_work/eval4_submission_v1b/fastapi_15589/a/tmp:\n", "exit_code": 0}full{"status": "ok", "stdout": "/tmp/swe_work/eval4_submission_v1b/fastapi_15589/a/tmp:\n", "exit_code": 0} |
| 15 | 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_token: str = Header()):\n return {'x_token': x_token}\n\nclient = TestClient(app)\nresponse = client.get('/', headers={'x_token': 'bar'})\nprint(f'Status: {response.status_code}, JSON: {response.json()}')\n\""
} | {"status": "ok", "stdout": "Status: 422, JSON: {'detail': [{'type': 'missing', 'loc': ['header', 'x-token'], 'msg': 'Field required', 'input': None}]}\n", "exitfull{"status": "ok", "stdout": "Status: 422, JSON: {'detail': [{'type': 'missing', 'loc': ['header', 'x-token'], 'msg': 'Field required', 'input': None}]}\n", "exit_code": 0} |
| 16 | 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_token: str = Header()):\n return {'x_token': x_token}\n\nclient = TestClient(app)\nresponse = client.get('/', headers={'x-token': 'bar'})\nprint(f'Status: {response.status_code}, JSON: {response.json()}')\n\""
} | {"status": "ok", "stdout": "Status: 200, JSON: {'x_token': 'bar'}\n", "exit_code": 0}full{"status": "ok", "stdout": "Status: 200, JSON: {'x_token': 'bar'}\n", "exit_code": 0} |
| 17 | run_command | {
"command": "python3 -c \"\nfrom fastapi import FastAPI, Header\nfrom fastapi.testclient import TestClient\nfrom pydantic import BaseModel\n\napp = FastAPI()\n\nclass HeaderModel(BaseModel):\n x_token: str = Header()\n\n@app.get('/')\nasync def root(headers: HeaderModel = Header()):\n return {'x_token': headers.x_token}\n\nclient = TestClient(app)\nresponse = client.get('/', headers={'x_token': 'bar'})\nprint(f'Status: {response.status_code}, JSON: {response.json()}')\n\""
} | {"status": "ok", "stdout": "Status: 200, JSON: {'x_token': 'bar'}\n", "exit_code": 0}full{"status": "ok", "stdout": "Status: 200, JSON: {'x_token': 'bar'}\n", "exit_code": 0} |
| 18 | read_file | {
"end_line": 840,
"filepath": "fastapi/dependencies/utils.py",
"start_line": 810
} | {"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "\n processed_keys = set()\n\n for field in fields_to_extract:\n alias = None\n if isinfull{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "\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 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": 810, "end_line": 840, "total_lines": 1057, "is_truncated": false} |
| 19 | edit_file | {
"filepath": "fastapi/dependencies/utils.py",
"new_string": " 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 # This is a bit tricky because we don't have a single convert_underscores \n # for the whole request, but we can check if any of the fields had it.\n # However, the requirement is \"Do not accept underscore headers when using convert_underscores=True\".\n # In the case of a Pydantic model, we already have default_convert_underscores.\n if default_convert_underscores and \"_\" in key:\n continue\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": " for key in received_params.keys():\n 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,13 @@\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+ # This is a bit tricky because we don't have a single convert_underscores \n+ # for the whole request, but we can check if any of the fields had it.\n+ # However, the requirement is \"Do not accept underscore headers when using convert_underscores=True\".\n+ # In the case of a Pydantic model, we already have default_convert_underscores.\n+ if default_convert_underscores and \"_\" in key:\n+ continue\n value = received_params.getlist(key)\n if isinstance(value, list) and (len(value) == 1):\n params_to_process[key] = value[0]\n", "is_truncated": false} |
| 20 | run_command | {
"command": "python3 -c \"\nfrom fastapi import FastAPI, Header\nfrom fastapi.testclient import TestClient\nfrom pydantic import BaseModel\n\napp = FastAPI()\n\nclass HeaderModel(BaseModel):\n x_token: str = Header()\n\n@app.get('/')\nasync def root(headers: HeaderModel = Header()):\n return {'x_token': headers.x_token}\n\nclient = TestClient(app)\nresponse = client.get('/', headers={'x_token': 'bar'})\nprint(f'Status: {response.status_code}, JSON: {response.json()}')\n\""
} | {"status": "ok", "stdout": "Status: 422, JSON: {'detail': [{'type': 'missing', 'loc': ['header', 'x_token'], 'msg': 'Field required', 'input': {'host': 'testserfull{"status": "ok", "stdout": "Status: 422, JSON: {'detail': [{'type': 'missing', 'loc': ['header', 'x_token'], 'msg': 'Field required', 'input': {'host': 'testserver', 'accept': '*/*', 'accept-encoding': 'gzip, deflate', 'connection': 'keep-alive', 'user-agent': 'testclient'}}]}\n", "exit_code": 0} |
| 21 | run_command | {
"command": "python3 -c \"\nfrom fastapi import FastAPI, Header\nfrom fastapi.testclient import TestClient\nfrom pydantic import BaseModel\n\napp = FastAPI()\n\nclass HeaderModel(BaseModel):\n x_token: str = Header(convert_underscores=False)\n\n@app.get('/')\nasync def root(headers: HeaderModel = Header()):\n return {'x_token': headers.x_token}\n\nclient = TestClient(app)\nresponse = client.get('/', headers={'x_token': 'bar'})\nprint(f'Status: {response.status_code}, JSON: {response.json()}')\n\""
} | {"status": "ok", "stdout": "Status: 200, JSON: {'x_token': 'bar'}\n", "exit_code": 0}full{"status": "ok", "stdout": "Status: 200, JSON: {'x_token': 'bar'}\n", "exit_code": 0} |
| 22 | 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_token: str = Header()):\n return {'x_token': x_token}\n\nclient = TestClient(app)\nresponse = client.get('/', headers={'x_token': 'bar'})\nprint(f'Status: {response.status_code}, JSON: {response.json()}')\n\""
} | {"status": "ok", "stdout": "Status: 422, JSON: {'detail': [{'type': 'missing', 'loc': ['header', 'x-token'], 'msg': 'Field required', 'input': None}]}\n", "exitfull{"status": "ok", "stdout": "Status: 422, JSON: {'detail': [{'type': 'missing', 'loc': ['header', 'x-token'], 'msg': 'Field required', 'input': None}]}\n", "exit_code": 0} |
| 23 | submit_patch | [] | {"status": "ok", "patch_size": 3219, "files_changed": 2}full{"status": "ok", "patch_size": 3219, "files_changed": 2} |
diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py
index 7c6558c69..3b442c927 100644
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -830,6 +830,13 @@ def request_params_to_args(
for key in received_params.keys():
if key not in processed_keys:
if isinstance(received_params, (ImmutableMultiDict, Headers)):
+ # If convert_underscores is True, we should not accept underscore headers
+ # This is a bit tricky because we don't have a single convert_underscores
+ # for the whole request, but we can check if any of the fields had it.
+ # However, the requirement is "Do not accept underscore headers when using convert_underscores=True".
+ # In the case of a Pydantic model, we already have default_convert_underscores.
+ if default_convert_underscores and "_" in key:
+ continue
value = received_params.getlist(key)
if isinstance(value, list) and (len(value) == 1):
params_to_process[key] = value[0]
diff --git a/tmp/test_underscore_headers.py b/tmp/test_underscore_headers.py
new file mode 100644
index 000000000..1ca0b4cb7
--- /dev/null
+++ b/tmp/test_underscore_headers.py
@@ -0,0 +1,46 @@
+
+from fastapi import FastAPI, Header
+from fastapi.testclient import TestClient
+from pydantic import BaseModel
+
+app = FastAPI()
+
+@app.get("/")
+async def root(x_token: str = Header()):
+ return {"x_token": x_token}
+
+client = TestClient(app)
+
+def test_underscore_header_rejected():
+ # By default, convert_underscores=True.
+ # It should accept 'x-token' and map it to 'x_token'.
+ # It should NOT accept 'x_token' as a header.
+ response = client.get("/", headers={"x-token": "foo"})
+ assert response.status_code == 200
+ assert response.json() == {"x_token": "foo"}
+
+ # This is the case we want to ensure fails or is not accepted as the parameter
+ # If we send 'x_token', it should not be mapped to 'x_token' if convert_underscores=True
+ # Actually, if convert_underscores=True, FastAPI looks for 'x-token'.
+ # If it doesn't find 'x-token', it might still find 'x_token' if it's not strictly forbidden.
+ # The issue says "Do not accept underscore headers when using convert_underscores=True"
+
+ # Let's see what happens now.
+ response = client.get("/", headers={"x_token": "bar"})
+ # If it's currently accepted, this will be 200. We want it to be 422 (Unprocessable Entity)
+ # because 'x_token' is an underscore header.
+ print(f"Response for x_token: {response.status_code}, {response.json()}")
+
+def test_underscore_header_accepted_when_disabled():
+ @app.get("/disabled")
+ async def disabled(x_token: str = Header(convert_underscores=False)):
+ return {"x_token": x_token}
+
+ response = client.get("/disabled", headers={"x_token": "bar"})
+ assert response.status_code == 200
+ assert response.json() == {"x_token": "bar"}
+
+if __name__ == "__main__":
+ test_underscore_header_rejected()
+ test_underscore_header_accepted_when_disabled()
+ print("Tests passed!")
....... [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.70s