feat: enhance API rate limiter with advanced security features - #180
Conversation
Extracted from monster PR #171 as part of Phase 3A strategic decomposition. Enhanced token bucket rate limiter with: - Advanced abuse detection (user agent analysis, pattern recognition) - IP whitelist/blacklist support - Concurrent request limiting - Starlette middleware integration - Test environment bypass - Comprehensive anomaly scoring 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
Reviewer's GuideThis PR refactors and extends the TokenBucketRateLimiter in api_rate_limiter.py by adding thread-safe IP whitelist/blacklist management, modularizing abuse-detection logic, tightening logging and error handling, and cleaning up formatting and docstrings for improved readability and maintainability. File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Warning Rate limit exceeded@uelkerd has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 18 minutes and 29 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (1)
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds IP whitelist/blacklist management, per-IP cleanup, and anomaly-detection thresholds to the token-bucket rate limiter. Introduces IP-to-client mapping, IP validation, new mutator APIs, refined abuse scoring/logging, and small typing/import reorganizations in Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant RL as TokenBucketRateLimiter
participant Cfg as RateLimitConfig
participant Store as Client State
Client->>RL: allow_request(ip, user_agent, path...)
RL->>Cfg: check whitelisted_ips / blacklisted_ips
alt IP blacklisted
RL-->>Client: deny (blocked)
RL->>Store: _cleanup_client_data(ip)
else IP whitelisted
RL-->>Client: allow (skip some checks)
else Normal path
RL->>Store: load bucket, concurrency, history
RL->>RL: _analyze_user_agent() & _analyze_request_patterns()
RL->>Cfg: compare scores to thresholds
alt anomaly detected
RL-->>Client: deny (anomaly)
RL->>Store: possibly block / record
else pass checks
RL->>Store: debit tokens, inc concurrency
RL-->>Client: allow
end
end
Client-->>RL: release_request(ip)
RL->>Store: dec concurrency / update history
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello @uelkerd, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a robust API rate limiter designed to enhance the security and stability of API endpoints. It incorporates advanced features for abuse detection, dynamic IP management, and concurrent request handling, all built upon a token bucket algorithm. The changes are delivered as a self-contained module, ready for integration into Starlette/FastAPI applications, and represent a focused extraction from a larger development effort. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
|
Here's the code health analysis summary for commits Analysis Summary
|
There was a problem hiding this comment.
Hey there - I've reviewed your changes - here's some feedback:
- Consider replacing threading.Lock with an async‐friendly lock (e.g., anyio.Lock or asyncio.Lock) or refactoring to avoid blocking the event loop in your Starlette/FastAPI middleware.
- Per‐client buckets, histories, and stats grow unbounded—add eviction or TTL logic to clean up stale client entries and prevent memory leaks under heavy load.
- User‐agent and request‐pattern analysis runs on every request and could become a performance bottleneck; consider precompiling patterns or otherwise optimizing those match loops.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Consider replacing threading.Lock with an async‐friendly lock (e.g., anyio.Lock or asyncio.Lock) or refactoring to avoid blocking the event loop in your Starlette/FastAPI middleware.
- Per‐client buckets, histories, and stats grow unbounded—add eviction or TTL logic to clean up stale client entries and prevent memory leaks under heavy load.
- User‐agent and request‐pattern analysis runs on every request and could become a performance bottleneck; consider precompiling patterns or otherwise optimizing those match loops.
## Individual Comments
### Comment 1
<location> `src/api_rate_limiter.py:506-513` </location>
<code_context>
+ self.concurrent_requests[client_key] - 1,
)
+ def add_to_blacklist(self, ip: str) -> None:
+ """Add IP to blacklist (thread-safe)."""
+ with self.lock:
+ if self.config.blacklisted_ips is not None:
+ self.config.blacklisted_ips.add(ip)
+ # Clean up any existing client data for this IP
+ self._cleanup_client_data(ip)
+ logger.info(f"Added {ip} to blacklist")
+
+ def remove_from_blacklist(self, ip: str) -> None:
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Adding to blacklist does not validate IP format.
Please validate the IP format using ipaddress.ip_address before adding it to the blacklist to prevent malformed entries.
```suggestion
import ipaddress
def add_to_blacklist(self, ip: str) -> None:
"""Add IP to blacklist (thread-safe)."""
try:
ipaddress.ip_address(ip)
except ValueError:
logger.error(f"Invalid IP address format: {ip}. Not adding to blacklist.")
return
with self.lock:
if self.config.blacklisted_ips is not None:
self.config.blacklisted_ips.add(ip)
# Clean up any existing client data for this IP
self._cleanup_client_data(ip)
logger.info(f"Added {ip} to blacklist")
```
</issue_to_address>
### Comment 2
<location> `src/api_rate_limiter.py:522-527` </location>
<code_context>
+ self.config.blacklisted_ips.discard(ip)
+ logger.info(f"Removed {ip} from blacklist")
+
+ def add_to_whitelist(self, ip: str) -> None:
+ """Add IP to whitelist (thread-safe)."""
+ with self.lock:
+ if self.config.whitelisted_ips is not None:
+ self.config.whitelisted_ips.add(ip)
+ logger.info(f"Added {ip} to whitelist")
+
+ def remove_from_whitelist(self, ip: str) -> None:
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Adding to whitelist does not validate IP format.
IP format should be validated before adding to the whitelist, as is done for the blacklist, to avoid invalid entries.
```suggestion
def add_to_whitelist(self, ip: str) -> None:
"""Add IP to whitelist (thread-safe)."""
with self.lock:
if self.config.whitelisted_ips is not None:
if self._is_valid_ip(ip):
self.config.whitelisted_ips.add(ip)
logger.info(f"Added {ip} to whitelist")
else:
logger.warning(f"Attempted to add invalid IP {ip} to whitelist")
```
</issue_to_address>
### Comment 3
<location> `src/api_rate_limiter.py:349` </location>
<code_context>
minute_count = sum(1 for t in recent_history if current_time - t <= 60.0)
</code_context>
<issue_to_address>
**suggestion (code-quality):** Simplify constant sum() call ([`simplify-constant-sum`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/simplify-constant-sum))
```suggestion
minute_count = sum(bool(current_time - t <= 60.0)
```
<br/><details><summary>Explanation</summary>As `sum` add the values it treats `True` as `1`, and `False` as `0`. We make use
of this fact to simplify the generator expression inside the `sum` call.
</details>
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Pull Request Overview
This PR enhances the API rate limiter with advanced security features, implementing a comprehensive token bucket rate limiter with multi-layered abuse detection, IP management capabilities, and intelligent pattern analysis for bot detection.
- Adds sophisticated abuse detection with user agent analysis and request pattern scoring
- Implements thread-safe IP whitelist/blacklist management with runtime modification methods
- Enhances code quality with better type hints, formatting improvements, and documentation updates
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
There was a problem hiding this comment.
Code Review
This pull request introduces significant enhancements to the API rate limiter, including dynamic IP whitelisting/blacklisting and a mechanism to clean up client data upon blacklisting. The code is well-structured, and the changes are mostly stylistic improvements and feature additions.
I've found a critical issue in the new _cleanup_client_data function, which is core to the blacklisting feature. The logic for identifying client data to be removed is incorrect, causing the function to fail silently. I've left a detailed comment on how to address this. I've also pointed out a minor inconsistency in logging style for you to consider.
It would be beneficial to add unit tests for the new IP list management features (add_to_blacklist, remove_from_blacklist, etc.) and especially for the _cleanup_client_data logic. This would have likely caught the bug and will prevent future regressions.
| def add_to_blacklist(self, ip: str) -> None: | ||
| """Add IP to blacklist (thread-safe).""" | ||
| with self.lock: | ||
| if self.config.blacklisted_ips is not None: | ||
| self.config.blacklisted_ips.add(ip) | ||
| # Clean up any existing client data for this IP | ||
| self._cleanup_client_data(ip) | ||
| logger.info(f"Added {ip} to blacklist") | ||
|
|
||
| def remove_from_blacklist(self, ip: str) -> None: | ||
| """Remove IP from blacklist (thread-safe).""" | ||
| with self.lock: | ||
| if self.config.blacklisted_ips is not None: | ||
| self.config.blacklisted_ips.discard(ip) | ||
| logger.info(f"Removed {ip} from blacklist") | ||
|
|
||
| def add_to_whitelist(self, ip: str) -> None: | ||
| """Add IP to whitelist (thread-safe).""" | ||
| with self.lock: | ||
| if self.config.whitelisted_ips is not None: | ||
| self.config.whitelisted_ips.add(ip) | ||
| logger.info(f"Added {ip} to whitelist") | ||
|
|
||
| def remove_from_whitelist(self, ip: str) -> None: | ||
| """Remove IP from whitelist (thread-safe).""" | ||
| with self.lock: | ||
| if self.config.whitelisted_ips is not None: | ||
| self.config.whitelisted_ips.discard(ip) | ||
| logger.info(f"Removed {ip} from whitelist") | ||
|
|
There was a problem hiding this comment.
The logging calls in the newly added methods (add_to_blacklist, remove_from_blacklist, add_to_whitelist, remove_from_whitelist) use f-strings for logging. This is inconsistent with the rest of the file, which uses the msg % args style for deferred formatting (e.g., logger.warning("Blocked request from blacklisted IP: %s", client_ip)).
Using deferred formatting is a best practice for logging as it avoids the cost of string formatting if the message is not going to be logged. For consistency and performance, please update these calls.
For example:
# In add_to_blacklist
logger.info("Added %s to blacklist", ip)
# In remove_from_blacklist
logger.info("Removed %s from blacklist", ip)This should be applied to the add_to_whitelist and remove_from_whitelist methods as well.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/api_rate_limiter.py (3)
119-151: Fix leaked concurrent slots in middleware and return standard Retry-After header.
dispatchnever callsrelease_request, so successful requests permanently consume a concurrency slot. Also, 429 responses should include aRetry-Afterheader.Apply:
allowed, reason, meta = self._limiter.allow_request(client_ip, user_agent) if not allowed: from fastapi.responses import JSONResponse - - return JSONResponse( + retry_after = str(meta.get("retry_after", 60)) + return JSONResponse( status_code=429, content={ "error": "Rate limit exceeded", "message": reason, "retry_after": meta.get("retry_after", 60), }, - ) + headers={"Retry-After": retry_after}, + ) - # Add rate limit headers - response = await call_next(request) - response.headers["X-RateLimit-Limit"] = str(self._cfg.requests_per_minute) - response.headers["X-RateLimit-Remaining"] = str(meta.get("tokens_remaining", 0)) - response.headers["X-RateLimit-Reset"] = str(meta.get("reset_time", 0)) - return response + # Add rate limit headers and always release the concurrency slot + try: + response = await call_next(request) + response.headers["X-RateLimit-Limit"] = str(self._cfg.requests_per_minute) + response.headers["X-RateLimit-Remaining"] = str(meta.get("tokens_remaining", 0)) + response.headers["X-RateLimit-Reset"] = str(meta.get("reset_time", 0)) + return response + finally: + self._limiter.release_request(client_ip, user_agent)
166-174: Add IP→client_keys index and tighten request_history typing.Required to make blacklist cleanup correct and improve types.
- self.request_history: Dict[str, Deque] = defaultdict(lambda: deque(maxlen=100)) + self.request_history: Dict[str, Deque[float]] = defaultdict(lambda: deque(maxlen=100)) + self.ip_to_keys: Dict[str, Set[str]] = defaultdict(set) self.lock = threading.RLock()
45-47: Default UA score threshold is too aggressive; raise to 5 to reduce false positives.At 3, plain curl/python-requests get blocked by default.
- suspicious_user_agent_score_threshold: int = 3 # Score threshold for suspicious UAs + suspicious_user_agent_score_threshold: int = 5 # Score threshold for suspicious UAs
🧹 Nitpick comments (3)
src/api_rate_limiter.py (3)
219-220: Use warning instead of exception logging for invalid IPs.Avoids noisy tracebacks for routine invalid/unknown client hosts.
- except ValueError: - logger.exception("Invalid IP address: %s", client_ip) + except ValueError: + logger.warning("Invalid IP address: %s", client_ip)
506-535: Parameterize log messages instead of f-strings.Consistent style and avoids formatting when disabled.
- logger.info(f"Added {ip} to blacklist") + logger.info("Added %s to blacklist", ip) @@ - logger.info(f"Removed {ip} from blacklist") + logger.info("Removed %s from blacklist", ip) @@ - logger.info(f"Added {ip} to whitelist") + logger.info("Added %s to whitelist", ip) @@ - logger.info(f"Removed {ip} from whitelist") + logger.info("Removed %s from whitelist", ip)
568-599: Operational note: in-memory limiter is per-process only.If you run multiple workers/instances, consider Redis/memcached for shared buckets and blocks; add health metrics for blocked/429 counts and bucket utilization.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/api_rate_limiter.py(16 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/api_rate_limiter.py (1)
deployment/secure_api_server.py (2)
add_to_blacklist(929-942)add_to_whitelist(946-959)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Sourcery review
- Add IP format validation to add_to_blacklist() and add_to_whitelist() methods - Create _is_valid_ip() helper method using ipaddress.ip_address() - Simplify constant sum() call in _calculate_sustained_volume_score() - Prevent malformed IP entries in whitelist/blacklist - Improve code quality and performance
CRITICAL FIX: - Fix _cleanup_client_data logic - client keys are SHA256 hashes, not IP addresses - Add ip_to_keys mapping to track IP addresses to their client keys - Update _get_client_key to maintain IP-to-keys mapping - _cleanup_client_data now properly removes all client data for blacklisted IPs IMPROVEMENTS: - Fix logging consistency - use deferred formatting instead of f-strings - Update all IP list management methods to use consistent logging style - Improve performance by avoiding string formatting when logging is disabled This fixes the critical bug where blacklisting IPs had no effect on existing client data.
- Replace logger.exception with logger.error in _is_ip_allowed method - logger.exception should only be used when logging actual exception context - This fixes the Copilot AI suggestion about proper logging usage
…lation FIXES: - Remove duplicate patterns from low_risk_patterns (bot, crawler, spider) - These patterns were already in high_risk_patterns, causing double-counting - Add accurate retry_after calculation for rate limit exceeded responses - Calculate seconds until next token is available based on deficit and RPM This fixes the CodeRabbit AI suggestions for proper scoring and better API responses.
- Change logger.error to logger.warning for invalid IP addresses - Avoids noisy tracebacks for routine invalid/unknown client hosts - Add operational note about in-memory limiter limitations - Document multi-worker deployment considerations All nitpick comments from code review have been addressed.
Resolved issues in src/api_rate_limiter.py with DeepSource Autofix
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/api_rate_limiter.py (2)
127-131: Do not bypass rate limiting based on UA in production; gate behind config.Unconditional UA-based bypass is a security risk; attackers can set UA to "pytest"/"testclient" to evade limits. Gate this behind a config flag.
- # Bypass rate limiting for test environment UAs - ua_lower = user_agent.lower() - if "test" in ua_lower or "pytest" in ua_lower or "testclient" in ua_lower: - return await call_next(request) + # Optional: bypass for test UAs if explicitly enabled + ua_lower = user_agent.lower() + if ( + self._cfg.enable_test_ua_bypass + and ("test" in ua_lower or "pytest" in ua_lower or "testclient" in ua_lower) + ): + return await call_next(request)Also add a config switch (see RateLimitConfig change suggestion):
@@ enable_request_pattern_analysis: bool = True + # Testing + enable_test_ua_bypass: bool = False
146-151: Fix concurrency leak: release_request is never called.Concurrent slots are incremented in allow_request but never released in middleware, causing permanent denial. Release in a finally block.
- # Add rate limit headers - response = await call_next(request) - response.headers["X-RateLimit-Limit"] = str(self._cfg.requests_per_minute) - response.headers["X-RateLimit-Remaining"] = str(meta.get("tokens_remaining", 0)) - response.headers["X-RateLimit-Reset"] = str(meta.get("reset_time", 0)) - return response + try: + response = await call_next(request) + # Add rate limit headers + response.headers["X-RateLimit-Limit"] = str(self._cfg.requests_per_minute) + response.headers["X-RateLimit-Remaining"] = str(meta.get("tokens_remaining", 0)) + response.headers["X-RateLimit-Reset"] = str(meta.get("reset_time", 0)) + return response + finally: + # Ensure concurrent slot is released even on exceptions + self._limiter.release_request(client_ip, user_agent)
🧹 Nitpick comments (6)
src/api_rate_limiter.py (6)
490-498: Return reset_time on success to populate headers.Include seconds until next token so middleware can set X-RateLimit-Reset.
- return ( + rpm = max(self.config.requests_per_minute, 1) + reset_time = max( + 0, + int((max(1.0 - self.buckets[client_key], 0.0) * 60.0) / rpm), + ) + return ( True, "Request allowed", { "client_key": client_key, "tokens_remaining": self.buckets[client_key], "concurrent_requests": self.concurrent_requests[client_key], + "reset_time": reset_time, }, )
223-224: Prefer logger.warning over logger.exception for invalid IP.Avoid stack traces for routine validation failures.
- logger.exception("Invalid IP address: %s", client_ip) + logger.warning("Invalid IP address: %s", client_ip)
74-83: Expand default exclusions to common noise paths.Exclude favicon and robots.txt by default to reduce unnecessary throttling.
default_exclusions: Set[str] = { "/health", "/metrics", "/docs", "/redoc", "/openapi.json", + "/favicon.ico", + "/robots.txt", }
570-586: Improve total_clients metric to reflect active keys.Include request_history keys for a more accurate count.
- "total_clients": len( - set(self.buckets.keys()) | set(self.concurrent_requests.keys()), - ), + "total_clients": len( + set(self.buckets.keys()) + | set(self.concurrent_requests.keys()) + | set(self.request_history.keys()) + ),
43-48: Add config flag for UA test bypass (companion to earlier comment).Define the field in RateLimitConfig so gating compiles.
@@ enable_user_agent_analysis: bool = True enable_request_pattern_analysis: bool = True suspicious_user_agent_score_threshold: int = 3 # Score threshold for suspicious UAs request_pattern_score_threshold: int = 5 # Score threshold for suspicious patterns anomaly_detection_window: float = 300.0 # 5 minutes for pattern analysis + # Testing + enable_test_ua_bypass: bool = FalseAlso applies to: 127-131
500-509: Optional: Prevent unnecessary ip_to_keys entries in release_requestrelease_request invokes _get_client_key, which always adds the IP–UA fingerprint to ip_to_keys. If user_agent differs (or defaults change), ip_to_keys can accumulate unused keys. Consider looking up the existing client_key (e.g. from concurrent_requests or ip_to_keys) instead of unconditionally calling _get_client_key.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/api_rate_limiter.py(17 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/api_rate_limiter.py (1)
deployment/secure_api_server.py (2)
add_to_blacklist(929-942)add_to_whitelist(946-959)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Sourcery review
- GitHub Check: Analyze (python)
🔇 Additional comments (6)
src/api_rate_limiter.py (6)
137-144: Return standard Retry-After header on 429.Clients and proxies rely on Retry-After; include it in headers.
- return JSONResponse( + return JSONResponse( status_code=429, content={ "error": "Rate limit exceeded", "message": reason, "retry_after": meta.get("retry_after", 60), }, + headers={"Retry-After": str(meta.get("retry_after", 60))}, )
186-193: LGTM: Correctly record IP→client_key mapping.This fixes prior cleanup issues by tracking keys per IP for targeted purge.
510-544: LGTM: Whitelist/blacklist mutators validate IPs and use parameterized logs.Thread-safe; cleanup on blacklist is correctly invoked.
554-569: LGTM: Cleanup now uses IP→keys index.Fixes prior ineffective cleanup that attempted prefix matching on hashed keys.
274-283: Avoid double-counting UA tokens across risk tiers."bot/crawler/spider" appear in both high and low lists, inflating scores.
- low_risk_patterns = [ - "bot", - "crawler", - "spider", + low_risk_patterns = [ "indexer", "feed", "rss", "aggregator", "monitor", "checker", ]
477-486: Provide accurate retry_after and reset_time in 429 metadata.Compute seconds until the next token; power both JSON body and headers.
- if self.buckets[client_key] < 0.999999: - return ( + if self.buckets[client_key] < 0.999999: + tokens = self.buckets[client_key] + rpm = max(self.config.requests_per_minute, 1) + deficit = max(1.0 - tokens, 0.0) + retry_after = max(1, int((deficit * 60.0) / rpm)) + return ( False, "Rate limit exceeded", { "client_key": client_key, - "tokens": self.buckets[client_key], - "rate_limit": self.config.requests_per_minute, + "tokens": tokens, + "rate_limit": self.config.requests_per_minute, + "retry_after": retry_after, + "reset_time": retry_after, }, )
CRITICAL FIX: - Add request_history_size config parameter (default: 250) - Ensure deque capacity >= sustained_rate_threshold + buffer - Fix hardcoded maxlen=100 that prevented detection of >100/min rates - Now properly supports 200/min sustained rate detection threshold The previous hardcoded maxlen=100 made the 200/min threshold useless since the deque would never contain enough history to detect sustained rates above 100 requests per minute.
…AMO--DL into feat/enhance-api-rate-limiting
Summary
Strategic extraction from monster PR #171 as part of Phase 3A focused decomposition.
Adds a comprehensive token bucket rate limiter with advanced security features:
Key Enhancements
Test Plan
🏰 Fortress-Protected Development: Single-purpose micro-PR (1 file, focused enhancement)
🤖 Generated with Claude Code
Summary by Sourcery
Add thread-safe IP whitelist/blacklist management and improve type safety, logging, and code readability in the rate limiter and middleware
New Features:
Enhancements:
Summary by CodeRabbit