From 81e4a639afe6872d3a9b41eaedb91052066e138a Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Wed, 19 Nov 2025 22:01:06 +0100 Subject: [PATCH] Forbid passing str to Security `scopes` parameter. Add descriptive error message --- fastapi/param_functions.py | 36 +++++++++++++++++++++++-- fastapi/params.py | 21 +++++++++++++-- tests/test_security_scopes_parameter.py | 26 ++++++++++++++++++ 3 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 tests/test_security_scopes_parameter.py diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index e32f75593..0e4a8b034 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -1,8 +1,19 @@ -from typing import Any, Callable, Dict, List, Optional, Sequence, Union +from typing import ( + Any, + Callable, + Dict, + FrozenSet, + List, + Optional, + Set, + Tuple, + Union, +) from annotated_doc import Doc from fastapi import params from fastapi._compat import Undefined +from fastapi.exceptions import FastAPIError from fastapi.openapi.models import Example from typing_extensions import Annotated, Literal, deprecated @@ -2312,7 +2323,14 @@ def Security( # noqa: N802 ] = None, *, scopes: Annotated[ - Optional[Sequence[str]], + Optional[ + Union[ + List[str], + Tuple[str, ...], + Set[str], + FrozenSet[str], + ] + ], Doc( """ OAuth2 scopes required for the *path operation* that uses this Security @@ -2378,4 +2396,18 @@ def Security( # noqa: N802 return [{"item_id": "Foo", "owner": current_user.username}] ``` """ + + if isinstance(scopes, str): + if scopes in ("function", "request"): + raise FastAPIError( + "Invalid value for `scopes` parameter in Security(). " + "You probably meant to use the `scope` parameter instead of `scopes`. " + "Expected a sequence of strings (e.g., ['admin', 'user']), but received a single string." + ) + raise FastAPIError( + "Invalid value for `scopes` parameter in Security(). " + "Expected a sequence of strings (e.g., ['admin', 'user']), but received a single string. " + "Wrap it in a list: scopes=['your_scope'] instead of scopes='your_scope'." + ) + return params.Security(dependency=dependency, scopes=scopes, use_cache=use_cache) diff --git a/fastapi/params.py b/fastapi/params.py index 6a58d5808..f92e2025c 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -1,7 +1,17 @@ import warnings from dataclasses import dataclass from enum import Enum -from typing import Any, Callable, Dict, List, Optional, Sequence, Union +from typing import ( + Any, + Callable, + Dict, + FrozenSet, + List, + Optional, + Set, + Tuple, + Union, +) from fastapi.openapi.models import Example from pydantic.fields import FieldInfo @@ -771,4 +781,11 @@ class Depends: @dataclass class Security(Depends): - scopes: Optional[Sequence[str]] = None + scopes: Optional[ + Union[ + List[str], + Tuple[str, ...], + Set[str], + FrozenSet[str], + ] + ] = None diff --git a/tests/test_security_scopes_parameter.py b/tests/test_security_scopes_parameter.py new file mode 100644 index 000000000..cd043bcd1 --- /dev/null +++ b/tests/test_security_scopes_parameter.py @@ -0,0 +1,26 @@ +import pytest +from fastapi import Security +from fastapi.exceptions import FastAPIError + + +def test_pass_single_str(): + with pytest.raises(FastAPIError) as exc_info: + Security(dependency=lambda: None, scopes="admin") + + assert str(exc_info.value) == ( + "Invalid value for `scopes` parameter in Security(). " + "Expected a sequence of strings (e.g., ['admin', 'user']), but received a single string. " + "Wrap it in a list: scopes=['your_scope'] instead of scopes='your_scope'." + ) + + +@pytest.mark.parametrize("value", ["function", "request"]) +def test_pass_scope_instead_of_scopes(value: str): + with pytest.raises(FastAPIError) as exc_info: + Security(dependency=lambda: None, scopes=value) + + assert str(exc_info.value) == ( + "Invalid value for `scopes` parameter in Security(). " + "You probably meant to use the `scope` parameter instead of `scopes`. " + "Expected a sequence of strings (e.g., ['admin', 'user']), but received a single string." + )