Skip to content

feat: enhance API rate limiter with advanced security features - #180

Merged
d-ulker merged 9 commits into
mainfrom
feat/enhance-api-rate-limiting
Sep 25, 2025
Merged

feat: enhance API rate limiter with advanced security features#180
d-ulker merged 9 commits into
mainfrom
feat/enhance-api-rate-limiting

Conversation

@d-ulker

@d-ulker d-ulker commented Sep 25, 2025

Copy link
Copy Markdown
Owner

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:

  • 🛡️ Enhanced Abuse Detection: Multi-layered scoring system analyzing user agents and request patterns
  • 🚫 IP Management: Whitelist/blacklist support with thread-safe operations
  • ⚡ Token Bucket Algorithm: Smooth rate limiting with configurable burst protection
  • 🔒 Concurrent Limiting: Prevents resource exhaustion attacks
  • 🧪 Test Environment Bypass: Smart detection of test user agents
  • 📊 Advanced Analytics: Request fingerprinting and anomaly scoring
  • ⚙️ Middleware Integration: Drop-in Starlette/FastAPI middleware

Key Enhancements

  • Pattern Analysis: Detects bot-like behavior through request timing analysis
  • User Agent Scoring: Identifies suspicious automation tools and scrapers
  • Multi-window Burst Detection: Analyzes request patterns across 1s, 5s, 10s windows
  • Sustained Rate Monitoring: Long-term abuse prevention (200 req/min threshold)
  • Path Exclusion: Built-in exclusions for health checks, metrics, documentation

Test Plan

  • Unit tests for token bucket algorithm
  • Security feature validation (abuse detection, IP filtering)
  • Integration tests with FastAPI middleware
  • Performance testing under load
  • Edge case testing (concurrent requests, blocked clients)

🏰 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:

  • Add methods to dynamically add/remove IPs to/from whitelist and blacklist with automatic client data cleanup

Enhancements:

  • Strengthen RateLimitConfig types with Optional Set annotations and enforce them via runtime assertions
  • Improve logging for invalid IP addresses and abuse detection using exception logging and consistent warning formats
  • Refactor allow_request responses for consistent multiline tuple formatting and standardize docstrings

Summary by CodeRabbit

  • New Features
    • Added IP allow/deny list management with runtime updates (add/remove at runtime).
    • Configurable thresholds and a time window for anomaly detection of suspicious user agents and request patterns.
  • Improvements
    • Strengthened abuse detection and clearer logging; expanded per-IP tracking and stats reporting.
  • Bug Fixes
    • More robust invalid-IP handling and automatic cleanup of all tracked data when an IP is blocked.

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>
Copilot AI review requested due to automatic review settings September 25, 2025 13:57
@sourcery-ai

sourcery-ai Bot commented Sep 25, 2025

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This 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

Change Details Files
Thread-safe IP whitelist/blacklist management
  • Introduced add/remove methods for whitelist and blacklist
  • Updated RateLimitConfig to use Optional[Set[str]] with type assertions
  • Implemented _cleanup_client_data to purge related client state
  • Enhanced _is_ip_allowed to respect config flags and sets
src/api_rate_limiter.py
Modularized abuse detection and scoring
  • Delegated request-pattern and sustained-volume scoring into helper functions
  • Refined _analyze_user_agent and _detect_abuse loops for clarity
  • Preserved thresholds and multi-window burst logic
src/api_rate_limiter.py
Improved logging and error handling
  • Switched invalid IP logs from logger.error to logger.exception
  • Standardized multi-line logger calls with trailing commas
  • Added log entries in IP management methods
src/api_rate_limiter.py
Code formatting and docstring cleanup
  • Reordered and consolidated imports
  • Unified docstring styles and line breaks
  • Applied consistent parentheses, trailing commas, and line wrapping
src/api_rate_limiter.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Sep 25, 2025

Copy link
Copy Markdown

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

📥 Commits

Reviewing files that changed from the base of the PR and between ef06fc8 and e71440f.

📒 Files selected for processing (1)
  • src/api_rate_limiter.py (17 hunks)

Note

Other AI code review bot(s) detected

CodeRabbit 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.

Walkthrough

Adds 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 src/api_rate_limiter.py.

Changes

Cohort / File(s) Summary
Config & typing
src/api_rate_limiter.py
RateLimitConfig signature updated with suspicious_user_agent_score_threshold, request_pattern_score_threshold, anomaly_detection_window; whitelisted_ips/blacklisted_ips changed to Optional[Set[str]]. Reorganized typing imports.
IP list management APIs
src/api_rate_limiter.py
TokenBucketRateLimiter adds public mutators: add_to_blacklist, remove_from_blacklist, add_to_whitelist, remove_from_whitelist. These use IP validation and thread-safe updates; blacklisting triggers client data cleanup.
Per-IP state & cleanup
src/api_rate_limiter.py
New ip_to_keys: Dict[str, Set[str]] mapping, _is_valid_ip helper, and _cleanup_client_data to purge buckets, last_refill, blocked_clients, concurrent_requests, and request_history for an IP. Client-key tracking updated in _get_client_key.
Allow/request flow & abuse detection
src/api_rate_limiter.py
_is_ip_allowed honors optional whitelists/blacklists with safer None checks; allow_request/helpers return consistent tuples; _analyze_user_agent, _analyze_request_patterns, and _detect_abuse updated to return/consume scores and use the new anomaly thresholds; logging/messages refined.
Stats & minor reorganizations
src/api_rate_limiter.py
get_stats adjusted to reflect new internal tracking (e.g., total_clients from mappings). Minor import/formatting and docstring tweaks.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

I twitch my ears at lists of IPs,
A whitelist carrot, blacklist nibble—
Buckets sip, then softly drip,
I watch patterns, count each quibble.
Threshold stars and tidy cleanup—
I hop through logs, a happy scribble. 🐇

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “feat: enhance API rate limiter with advanced security features” clearly and concisely captures the core enhancement to the API rate limiter by emphasizing the addition of advanced security measures. It directly reflects the main objective of the changeset without extraneous detail and uses straightforward phrasing that teammates can quickly understand.
Docstring Coverage ✅ Passed Docstring coverage is 90.48% which is sufficient. The required threshold is 80.00%.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @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

  • Enhanced Abuse Detection: A multi-layered scoring system has been implemented to analyze user agents and request patterns, identifying suspicious behavior.
  • IP Management: The rate limiter now supports dynamic IP whitelisting and blacklisting with thread-safe operations, allowing for real-time control over access.
  • Token Bucket Algorithm: The core rate limiting mechanism uses a token bucket algorithm, providing smooth rate limiting with configurable burst protection.
  • Concurrent Limiting: The system prevents resource exhaustion attacks by limiting the number of concurrent requests per client.
  • Test Environment Bypass: Smart detection of test user agents allows for bypassing rate limits in development and testing environments.
  • Middleware Integration: The rate limiter is provided as a drop-in Starlette/FastAPI middleware for easy integration into existing applications.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@deepsource-io

deepsource-io Bot commented Sep 25, 2025

Copy link
Copy Markdown
Contributor

Here's the code health analysis summary for commits fc6e52a..e71440f. View details on DeepSource ↗.

Analysis Summary

AnalyzerStatusSummaryLink
DeepSource Test coverage LogoTest coverage⚠️ Artifact not reportedTimed out: Artifact was never reportedView Check ↗
DeepSource Python LogoPython✅ Success
🎯 4 occurences resolved
View Check ↗
DeepSource Terraform LogoTerraform✅ SuccessView Check ↗
DeepSource Secrets LogoSecrets✅ SuccessView Check ↗
DeepSource Shell LogoShell✅ SuccessView Check ↗
DeepSource Docker LogoDocker✅ SuccessView Check ↗

💡 If you’re a repository administrator, you can configure the quality gates from the settings.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/api_rate_limiter.py Outdated
Comment thread src/api_rate_limiter.py Outdated
Comment thread src/api_rate_limiter.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/api_rate_limiter.py Outdated
Comment thread src/api_rate_limiter.py Outdated
Comment thread src/api_rate_limiter.py Outdated
Comment thread src/api_rate_limiter.py Outdated
Comment thread src/api_rate_limiter.py Outdated
Comment thread src/api_rate_limiter.py Outdated

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/api_rate_limiter.py Outdated
Comment thread src/api_rate_limiter.py
Comment on lines +506 to +535
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")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

dispatch never calls release_request, so successful requests permanently consume a concurrency slot. Also, 429 responses should include a Retry-After header.

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

📥 Commits

Reviewing files that changed from the base of the PR and between fc6e52a and 8e1f7e5.

📒 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

Comment thread src/api_rate_limiter.py Outdated
Comment thread src/api_rate_limiter.py
Comment thread src/api_rate_limiter.py
Comment thread src/api_rate_limiter.py
d-ulker and others added 6 commits September 25, 2025 18:27
- 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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = False

Also applies to: 127-131


500-509: Optional: Prevent unnecessary ip_to_keys entries in release_request

release_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

📥 Commits

Reviewing files that changed from the base of the PR and between 8e1f7e5 and ef06fc8.

📒 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,
                     },
                 )

Comment thread src/api_rate_limiter.py
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.
@d-ulker
d-ulker merged commit 4d0e5e6 into main Sep 25, 2025
11 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants