From 314e2a1c21fbc9ee38bc96c494cec620c9a7f1b1 Mon Sep 17 00:00:00 2001 From: Elfbot Date: Mon, 23 Mar 2026 13:55:24 +1300 Subject: [PATCH] feat: add opt-out flags for token negative cache and rate limiting Add two new configuration flags (defaulting to enabled for backward compatibility): - ENABLE_TOKEN_NEGATIVE_CACHE: controls the in-memory _missing_tokens TTLCache (24h TTL) in TokenStore - ENABLE_TOKEN_RATE_LIMIT: controls the middleware that short-circuits requests for missing tokens with 401/429 These flags allow operators to disable these features in multi-replica deployments where the per-process caches cause issues: 1. _missing_tokens is local to each replica, so a token cached as missing on one replica will be rejected for up to 24h even after it becomes valid in Redis (e.g. newly registered user) 2. The rate-limiting middleware uses request.client.host for IP tracking, which behind a reverse proxy (e.g. Cloudflare) resolves to the proxy IP, causing all users to share the same rate limit 3. The 401/429 responses are returned before CORSMiddleware runs, so browsers report them as CORS errors rather than the actual error Co-Authored-By: Claude Opus 4.6 (1M context) --- app/core/app.py | 2 ++ app/core/config.py | 3 +++ app/services/token_store.py | 22 ++++++++++++---------- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/core/app.py b/app/core/app.py index c65f885..a76787b 100644 --- a/app/core/app.py +++ b/app/core/app.py @@ -68,6 +68,8 @@ async def lifespan(app: FastAPI): @app.middleware("http") async def block_missing_token_middleware(request: Request, call_next): + if not settings.ENABLE_TOKEN_RATE_LIMIT: + return await call_next(request) # Extract first path segment which is commonly the token in addon routes path = request.url.path.lstrip("/") seg = path.split("/", 1)[0] if path else "" diff --git a/app/core/config.py b/app/core/config.py index 5ee7d29..1c250d6 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -38,6 +38,9 @@ class Settings(BaseSettings): RECOMMENDATION_SOURCE_ITEMS_LIMIT: int = 10 LIBRARY_ITEMS_LIMIT: int = 20 + ENABLE_TOKEN_NEGATIVE_CACHE: bool = True + ENABLE_TOKEN_RATE_LIMIT: bool = True + CATALOG_CACHE_TTL: int = 43200 # 12 hours CATALOG_STALE_TTL: int = 604800 # 7 days (soft expiration fallback) diff --git a/app/services/token_store.py b/app/services/token_store.py index ced5a16..2f486c3 100644 --- a/app/services/token_store.py +++ b/app/services/token_store.py @@ -230,12 +230,13 @@ async def _migrate_poster_rating_format_raw(self, token: str, redis_key: str, da @alru_cache(maxsize=2000, ttl=43200) async def get_user_data(self, token: str) -> dict[str, Any] | None: # Short-circuit for tokens known to be missing - try: - if token in self._missing_tokens: - logger.debug(f"[REDIS] Negative cache hit for missing token {token}") - return None - except Exception as e: - logger.debug(f"Failed to check negative cache for {token}: {e}") + if settings.ENABLE_TOKEN_NEGATIVE_CACHE: + try: + if token in self._missing_tokens: + logger.debug(f"[REDIS] Negative cache hit for missing token {token}") + return None + except Exception as e: + logger.debug(f"Failed to check negative cache for {token}: {e}") logger.debug(f"[REDIS] Cache miss. Fetching data from redis for {token}") key = self._format_key(token) @@ -243,10 +244,11 @@ async def get_user_data(self, token: str) -> dict[str, Any] | None: if not data_raw: # remember negative result briefly - try: - self._missing_tokens[token] = True - except Exception as e: - logger.debug(f"Failed to set negative cache for missing token {token}: {e}") + if settings.ENABLE_TOKEN_NEGATIVE_CACHE: + try: + self._missing_tokens[token] = True + except Exception as e: + logger.debug(f"Failed to set negative cache for missing token {token}: {e}") return None try: