Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions slowapi/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
"""

Expand All @@ -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:
"""
Expand All @@ -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 = []
Expand Down
58 changes: 57 additions & 1 deletion tests/test_fastapi_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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"
)
Loading