diff --git a/slowapi/extension.py b/slowapi/extension.py index 050f882..c04ff4a 100644 --- a/slowapi/extension.py +++ b/slowapi/extension.py @@ -87,6 +87,9 @@ def _rate_limit_exceeded_handler(request: Request, exc: RateLimitExceeded) -> Re return response +_UNSET = object() # sentinel: distinguishes "not passed" from explicit None + + class Limiter: """ Initializes the slowapi rate limiter. @@ -123,7 +126,10 @@ class Limiter: * **key_prefix**: prefix prepended to rate limiter keys. * **enabled**: set to False to deactivate the limiter (default: True) * **config_filename**: name of the config file for Starlette from which to load settings - for the rate limiter. Defaults to ".env". + for the rate limiter. When not specified, auto-detects ".env" in the current directory + and loads it if found. Pass ``None`` explicitly to disable all file loading (recommended + for Docker and Kubernetes deployments where environment variables are injected by the + orchestration layer rather than from a file). * **key_style**: set to "url" to use the url, "endpoint" to use the view_func """ @@ -144,7 +150,7 @@ def __init__( retry_after: Optional[str] = None, key_prefix: str = "", enabled: bool = True, - config_filename: Optional[str] = None, + config_filename: Optional[str] = _UNSET, # type: ignore[assignment] key_style: Literal["endpoint", "url"] = "url", ) -> None: """ @@ -155,13 +161,14 @@ def __init__( # app.state.limiter = self self.logger = logging.getLogger("slowapi") - - dotenv_file_exists = os.path.isfile(".env") - self.app_config = Config( - ".env" - if dotenv_file_exists and config_filename is None - else config_filename - ) + if config_filename is _UNSET: + # Backward-compatible auto-detect: load .env only if it exists + _config_file: Optional[str] = ".env" if os.path.isfile(".env") else None + else: + # User explicitly passed config_filename. + # None means "don't load any file" (e.g. Docker/K8s envs). + _config_file = config_filename # type: ignore[assignment] + self.app_config = Config(_config_file) self.enabled = enabled self._default_limits = [] diff --git a/tests/test_fastapi_extension.py b/tests/test_fastapi_extension.py index 42e6322..387c94f 100644 --- a/tests/test_fastapi_extension.py +++ b/tests/test_fastapi_extension.py @@ -4,7 +4,8 @@ from starlette.responses import PlainTextResponse, Response from starlette.testclient import TestClient -from slowapi.util import get_ipaddr +from slowapi.extension import Limiter +from slowapi.util import get_ipaddr, get_remote_address from tests import TestSlowapi @@ -369,3 +370,58 @@ async def t1_func(my_param: str, request: Request): ) == 2 ) + + +# -------------------------------------------------------------------------- +# Tests for config_filename sentinel behaviour (issue #256) +# -------------------------------------------------------------------------- + +def test_config_filename_none_disables_dotenv_loading(tmp_path, monkeypatch): + """ + Passing config_filename=None explicitly must NOT load a .env file, + even if one exists in the current directory. + This is the Docker/K8s opt-out: the user manages their own env vars. + """ + env_file = tmp_path / ".env" + env_file.write_text("RATELIMIT_TEST_SENTINEL=loaded_from_file\n") + monkeypatch.chdir(tmp_path) # make os.path.isfile(".env") see the file + + limiter = Limiter(key_func=get_remote_address, config_filename=None) + + assert ( + limiter.app_config("RATELIMIT_TEST_SENTINEL", default="not_loaded") + == "not_loaded" + ), "config_filename=None should disable .env loading" + + +def test_config_filename_unset_loads_dotenv_when_present(tmp_path, monkeypatch): + """ + Omitting config_filename entirely should preserve the existing behaviour: + auto-detect and load .env when it exists in the current directory. + """ + env_file = tmp_path / ".env" + env_file.write_text("RATELIMIT_TEST_SENTINEL=loaded_from_file\n") + monkeypatch.chdir(tmp_path) + + limiter = Limiter(key_func=get_remote_address) + + assert ( + limiter.app_config("RATELIMIT_TEST_SENTINEL", default="not_loaded") + == "loaded_from_file" + ), "omitting config_filename should auto-load .env when present" + + +def test_config_filename_unset_no_dotenv_does_not_error(tmp_path, monkeypatch): + """ + Omitting config_filename when no .env file exists must not raise. + This covers the original issue: containers without a .env file mounted. + """ + monkeypatch.chdir(tmp_path) # tmp_path is empty, no .env present + + # Must not raise any exception + limiter = Limiter(key_func=get_remote_address) + + assert ( + limiter.app_config("RATELIMIT_TEST_SENTINEL", default="fallback") + == "fallback" + )