Skip to content

Conversation

@touale
Copy link
Owner

@touale touale commented Jan 6, 2026

Summary by CodeRabbit

  • New Features

    • Route registration now enforces method/path uniqueness and blocks duplicate or overlapping registrations.
  • Bug Fixes

    • Prevents ambiguous or conflicting route registrations and surfaces clear errors when duplicates are detected, which may affect previously allowed duplicate registrations.
  • Tests

    • Added tests covering route registration behavior, duplicate detection, HTTP method handling, and argument forwarding.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 6, 2026

Caution

Review failed

The pull request is closed.

Walkthrough

A new add_api_route method was added to APIIngress to centralize route registration: it normalizes path patterns (parameter placeholders), uppercases/validates methods, detects duplicate path+method conflicts against app.routes and raises RuntimeError on duplicates; register_route now delegates to it.

Changes

Cohort / File(s) Summary
Route registration logic
src/framex/driver/ingress.py
Added `APIIngress.add_api_route(path, endpoint, *, methods: list[str]
Tests for new behavior
tests/driver/test_ingress.py
New test module for add_api_route: verifies normal registration, duplicate detection (same path+method), allowing same path with different methods, overlapping-method conflict, case-sensitivity validation, ignoring non-API routes, and forwarding of kwargs.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Caller / register_route
    participant Ingress as APIIngress.add_api_route
    participant App as FastAPI app
    participant Routes as app.routes (APIRoute-like)

    Caller->>Ingress: add_api_route(path, endpoint, methods?, **kwargs)
    Ingress->>Ingress: normalize path (replace params with `{}`)
    Ingress->>Ingress: normalize & validate methods (uppercase, check case)
    Ingress->>Routes: iterate existing routes
    alt normalized path matches existing route
        Routes-->>Ingress: existing route (with methods)
        Ingress->>Ingress: compute method intersection
        alt intersection non-empty
            Ingress-->>Caller: raise RuntimeError("Duplicate API route")
        else
            Ingress->>App: app.add_api_route(path, endpoint, methods=methods, **kwargs)
            App-->>Ingress: route registered
            Ingress-->>Caller: return
        end
    else no matching path
        Ingress->>App: app.add_api_route(path, endpoint, methods=methods, **kwargs)
        App-->>Ingress: route registered
        Ingress-->>Caller: return
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐇 I hopped through routes both near and far,
I tidied params and raised the bar.
If methods clash, I sound the bell—no spoof—
Now endpoints march in single-file proof! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately describes the main change: adding duplicate route detection in APIIngress, which is the core feature introduced in this pull request.

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 73ee04d and 4875ab2.

📒 Files selected for processing (2)
  • src/framex/driver/ingress.py
  • tests/driver/test_ingress.py

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.

@codecov
Copy link

codecov bot commented Jan 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@touale
Copy link
Owner Author

touale commented Jan 6, 2026

@coderabbitai generate test

@coderabbitai
Copy link

coderabbitai bot commented Jan 6, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Get PR details including changed files
gh pr view 46 --json title,body,files --jq '.files[].path'

# Get the diff for the PR
gh pr diff 46

Length of output: 1491


🏁 Script executed:

#!/bin/bash

# Get the full content of the ingress.py file to understand the structure
cat src/framex/driver/ingress.py

# Check if there's an existing test file
fd -t f "test.*ingress" -e py

# Check the project structure for test conventions
fd -t f "test_" -e py | head -5

Length of output: 6740


@touale, I'll generate comprehensive tests for the duplicate route detection feature. Here's a complete test file:

import pytest
from unittest.mock import Mock, patch
from fastapi.routing import APIRoute

from framex.driver.ingress import APIIngress


class TestAPIIngressDuplicateRouteDetection:
    """Test suite for duplicate route detection in APIIngress.add_api_route()"""

    @pytest.fixture
    def ingress(self):
        """Create an APIIngress instance for testing"""
        with patch("framex.driver.ingress.app") as mock_app:
            mock_app.routes = []
            mock_app.state = Mock()
            ingress = APIIngress.__new__(APIIngress)
            return ingress

    @pytest.fixture
    def mock_app(self):
        """Mock FastAPI app with routes"""
        with patch("framex.driver.ingress.app") as mock_app:
            mock_app.routes = []
            mock_app.add_api_route = Mock()
            yield mock_app

    def test_add_first_route_success(self, ingress, mock_app):
        """Test adding the first route succeeds"""
        endpoint = Mock()
        
        ingress.add_api_route(
            path="/users",
            endpoint=endpoint,
            methods=["GET"]
        )
        
        mock_app.add_api_route.assert_called_once()
        args, kwargs = mock_app.add_api_route.call_args
        assert args[0] == "/users"
        assert args[1] == endpoint
        assert kwargs["methods"] == ["GET"]

    def test_duplicate_route_same_path_same_method_raises_error(self, ingress, mock_app):
        """Test that adding duplicate route with same path and method raises RuntimeError"""
        existing_route = Mock(spec=APIRoute)
        existing_route.path = "/users/{id}"
        existing_route.methods = {"GET"}
        mock_app.routes = [existing_route]
        
        endpoint = Mock()
        
        with pytest.raises(RuntimeError, match=r"Duplicate API route: \['GET'\] /users/\{\}"):
            ingress.add_api_route(
                path="/users/{id}",
                endpoint=endpoint,
                methods=["GET"]
            )

    def test_duplicate_route_different_param_names_raises_error(self, ingress, mock_app):
        """Test that routes with different parameter names but same pattern are detected as duplicates"""
        existing_route = Mock(spec=APIRoute)
        existing_route.path = "/users/{id}"
        existing_route.methods = {"GET"}
        mock_app.routes = [existing_route]
        
        endpoint = Mock()
        
        # Try to add route with different parameter name but same pattern
        with pytest.raises(RuntimeError, match=r"Duplicate API route: \['GET'\] /users/\{\}"):
            ingress.add_api_route(
                path="/users/{user_id}",
                endpoint=endpoint,
                methods=["GET"]
            )

    def test_same_path_different_methods_allowed(self, ingress, mock_app):
        """Test that same path with different methods is allowed"""
        existing_route = Mock(spec=APIRoute)
        existing_route.path = "/users/{id}"
        existing_route.methods = {"GET"}
        mock_app.routes = [existing_route]
        
        endpoint = Mock()
        
        # Should succeed - different method
        ingress.add_api_route(
            path="/users/{id}",
            endpoint=endpoint,
            methods=["POST"]
        )
        
        mock_app.add_api_route.assert_called_once()

    def test_overlapping_methods_raises_error(self, ingress, mock_app):
        """Test that routes with overlapping methods raise error"""
        existing_route = Mock(spec=APIRoute)
        existing_route.path = "/users/{id}"
        existing_route.methods = {"GET", "POST"}
        mock_app.routes = [existing_route]
        
        endpoint = Mock()
        
        # Try to add route that overlaps with one method
        with pytest.raises(RuntimeError, match=r"Duplicate API route:"):
            ingress.add_api_route(
                path="/users/{id}",
                endpoint=endpoint,
                methods=["POST", "PUT"]  # POST overlaps
            )

    def test_multiple_parameters_normalized_correctly(self, ingress, mock_app):
        """Test that paths with multiple parameters are normalized correctly"""
        existing_route = Mock(spec=APIRoute)
        existing_route.path = "/users/{user_id}/posts/{post_id}"
        existing_route.methods = {"GET"}
        mock_app.routes = [existing_route]
        
        endpoint = Mock()
        
        # Try to add route with different parameter names
        with pytest.raises(RuntimeError, match=r"Duplicate API route: \['GET'\] /users/\{\}/posts/\{\}"):
            ingress.add_api_route(
                path="/users/{id}/posts/{pid}",
                endpoint=endpoint,
                methods=["GET"]
            )

    def test_different_paths_no_conflict(self, ingress, mock_app):
        """Test that different paths don't conflict"""
        existing_route = Mock(spec=APIRoute)
        existing_route.path = "/users/{id}"
        existing_route.methods = {"GET"}
        mock_app.routes = [existing_route]
        
        endpoint = Mock()
        
        # Should succeed - different path
        ingress.add_api_route(
            path="/posts/{id}",
            endpoint=endpoint,
            methods=["GET"]
        )
        
        mock_app.add_api_route.assert_called_once()

    def test_case_insensitive_method_matching(self, ingress, mock_app):
        """Test that HTTP methods are matched case-insensitively"""
        existing_route = Mock(spec=APIRoute)
        existing_route.path = "/users"
        existing_route.methods = {"GET"}
        mock_app.routes = [existing_route]
        
        endpoint = Mock()
        
        # Try to add route with lowercase method
        with pytest.raises(RuntimeError, match=r"Duplicate API route: \['GET'\] /users"):
            ingress.add_api_route(
                path="/users",
                endpoint=endpoint,
                methods=["get"]  # lowercase
            )

    def test_multiple_methods_sorted_in_error_message(self, ingress, mock_app):
        """Test that error message contains sorted methods"""
        existing_route = Mock(spec=APIRoute)
        existing_route.path = "/users"
        existing_route.methods = {"POST", "PUT"}
        mock_app.routes = [existing_route]
        
        endpoint = Mock()
        
        # Try to add duplicate with multiple methods
        with pytest.raises(RuntimeError, match=r"Duplicate API route: \['POST', 'PUT'\] /users"):
            ingress.add_api_route(
                path="/users",
                endpoint=endpoint,
                methods=["PUT", "POST"]  # Unsorted input
            )

    def test_none_methods_parameter(self, ingress, mock_app):
        """Test handling of None methods parameter"""
        endpoint = Mock()
        
        # Should succeed with empty method set
        ingress.add_api_route(
            path="/users",
            endpoint=endpoint,
            methods=None
        )
        
        mock_app.add_api_route.assert_called_once()
        args, kwargs = mock_app.add_api_route.call_args
        assert kwargs["methods"] == []

    def test_non_api_route_ignored(self, ingress, mock_app):
        """Test that non-APIRoute routes are ignored in duplicate detection"""
        from starlette.routing import Route
        
        non_api_route = Mock(spec=Route)
        non_api_route.path = "/users/{id}"
        mock_app.routes = [non_api_route]
        
        endpoint = Mock()
        
        # Should succeed - non-APIRoute is ignored
        ingress.add_api_route(
            path="/users/{id}",
            endpoint=endpoint,
            methods=["GET"]
        )
        
        mock_app.add_api_route.assert_called_once()

    def test_kwargs_passed_through(self, ingress, mock_app):
        """Test that additional kwargs are passed through to app.add_api_route"""
        endpoint = Mock()
        
        ingress.add_api_route(
            path="/users",
            endpoint=endpoint,
            methods=["GET"],
            tags=["users"],
            dependencies=[],
            response_class=Mock()
        )
        
        args, kwargs = mock_app.add_api_route.call_args
        assert "tags" in kwargs
        assert "dependencies" in kwargs
        assert "response_class" in kwargs

    def test_complex_path_with_multiple_segments(self, ingress, mock_app):
        """Test complex paths with multiple segments and parameters"""
        existing_route = Mock(spec=APIRoute)
        existing_route.path = "/api/v1/organizations/{org_id}/teams/{team_id}/members/{member_id}"
        existing_route.methods = {"DELETE"}
        mock_app.routes = [existing_route]
        
        endpoint = Mock()
        
        expected_pattern = r"Duplicate API route: \['DELETE'\] /api/v1/organizations/\{\}/teams/\{\}/members/\{\}"
        with pytest.raises(RuntimeError, match=expected_pattern):
            ingress.add_api_route(
                path="/api/v1/organizations/{organization_id}/teams/{t_id}/members/{m_id}",
                endpoint=endpoint,
                methods=["DELETE"]
            )

This test file covers:

  1. Basic functionality: Adding routes successfully
  2. Duplicate detection: Same path and method
  3. Path normalization: Different parameter names (e.g., {id} vs {user_id})
  4. Method differentiation: Same path with different methods allowed
  5. Overlapping methods: Detecting partial method overlaps
  6. Multiple parameters: Complex paths with multiple parameters
  7. Edge cases:
    • Case-insensitive method matching
    • None methods parameter
    • Non-APIRoute routes being ignored
    • Kwargs pass-through
    • Error message format with sorted methods

To run these tests:

pytest tests/driver/test_ingress.py -v

The tests use mocking to isolate the add_api_route method and verify both successful scenarios and error conditions.

@touale
Copy link
Owner Author

touale commented Jan 6, 2026

@coderabbitai help

@coderabbitai
Copy link

coderabbitai bot commented Jan 6, 2026

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

CodeRabbit commands

These commands are invoked using PR/Issue comments.

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai evaluate custom pre-merge check --instructions <custom-checks-instructions> --name <custom-checks-title> [--mode <error|warning>] to validate the custom pre-merge checks instructions. Defaults to error when --mode is omitted.
    • @coderabbitai ignore pre-merge checks to override pre-merge checks and get an approval on PR.
    • @coderabbitai run pre-merge checks to run pre-merge checks on the pull request.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve to resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit configuration file (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, support, documentation and community

  • Visit our status page to check the current availability of CodeRabbit.
  • Create a ticket on our support page for assistance with any issues or questions.
  • Visit our documentation site for detailed information on how to use CodeRabbit.
  • Join our Discord community to connect with other users and get help from the community.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI Agents
In @src/framex/driver/ingress.py:
- Around line 159-168: The duplicate-detection misses routes when methods is
None/empty because method_set becomes empty; update add_api_route to handle that
edge case by defaulting method_set to {"GET"} when methods is None or an empty
iterable (i.e., replace method_set: set[str] = {m.upper() for m in (methods or
[])} with logic that yields {"GET"} if the resulting set is empty), then keep
the existing APIRoute/path comparison and intersection check so duplicate routes
without explicit methods are detected; alternatively, if you prefer validation,
raise a clear error in add_api_route when methods is None/empty (mirror
register_route behavior) — pick one consistent approach and apply it to the
duplicate check and any error message text.
🧹 Nitpick comments (2)
src/framex/driver/ingress.py (2)

82-85: Consider consolidating duplicate detection logic.

This exact-path check is now partially redundant with the normalized path + method checking in add_api_route (lines 162-168). The two checks have different behaviors:

  • This check: exact path match → logs warning and returns False
  • New check: normalized path match with method overlap → raises RuntimeError

Consider either removing this check entirely (relying on the more comprehensive check in add_api_route) or documenting why both are needed.

🔎 Option to remove redundant check
-        try:
-            routes: list[str] = [route.path for route in app.routes if isinstance(route, Route | APIRoute)]
-            if path in routes:
-                logger.warning(f"API({path}) with tags {tags} is already registered, skipping duplicate registration.")
-                return False
-            if (not path) or (not methods):
+        try:
+            if (not path) or (not methods):

168-168: Improve error message formatting.

The error message uses sorted(method_set) which returns a list. Consider formatting it more clearly for better readability.

🔎 Improved error message formatting
-            raise RuntimeError(f"Duplicate API route: {sorted(method_set)} {norm_path}")
+            raise RuntimeError(f"Duplicate API route detected: {', '.join(sorted(method_set))} {norm_path}")

Additionally, the static analysis tool suggests avoiding long messages outside exception classes (TRY003). While this is a style preference, you could consider defining a custom exception class if this becomes more complex.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2921208 and 548e042.

📒 Files selected for processing (1)
  • src/framex/driver/ingress.py
🧰 Additional context used
🪛 Ruff (0.14.10)
src/framex/driver/ingress.py

168-168: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (3)
src/framex/driver/ingress.py (3)

1-1: LGTM!

The re module import is necessary for path normalization in the duplicate detection logic.


127-134: LGTM!

Delegating to self.add_api_route properly centralizes route registration and enables the duplicate detection logic.


170-170: Route registration looks correct.

Converting method_set back to a list is appropriate for FastAPI's add_api_route. However, this depends on properly handling the empty methods case flagged in the previous comment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
tests/driver/test_ingress.py (2)

30-32: Consider refactoring to avoid bypassing __init__.

Using __new__ to bypass __init__ tightly couples the tests to implementation details and makes them brittle. If add_api_route ever needs to access instance attributes, these tests will break.

Consider alternatives:

  • Provide a factory method or test constructor that accepts minimal dependencies
  • Use dependency injection to pass the app as a parameter
  • Refactor add_api_route to be a standalone function if it doesn't need instance state
Alternative fixture approach
@pytest.fixture
def ingress(mock_app):
    # Create a proper instance with minimal dependencies
    # or refactor APIIngress to accept app as a parameter
    with patch("framex.driver.ingress.app", mock_app):
        instance = APIIngress.__new__(APIIngress)
        # Explicitly set any required attributes if needed
        return instance

Note: This still uses __new__ but makes the dependency on mock_app explicit. A better long-term solution would be to refactor the class to support testing without __new__.


76-80: Consider clarifying the test name.

The test name test_case_insensitive_methods could be more descriptive. It's specifically testing that duplicate detection is case-insensitive (i.e., "get" matches "GET"). A name like test_duplicate_detection_is_case_insensitive or test_lowercase_method_matches_uppercase_route would be clearer.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 548e042 and 73ee04d.

📒 Files selected for processing (1)
  • tests/driver/test_ingress.py
🧰 Additional context used
🧬 Code graph analysis (1)
tests/driver/test_ingress.py (1)
src/framex/driver/ingress.py (2)
  • APIIngress (31-170)
  • add_api_route (151-170)
🔇 Additional comments (4)
tests/driver/test_ingress.py (4)

1-16: LGTM!

The imports are appropriate for the testing needs, and the make_route helper provides a clean way to create test doubles for APIRoute objects.


38-73: LGTM!

These tests provide good coverage:

  • Basic route addition is validated
  • Path normalization with different parameter names is properly tested via parametrization
  • Non-overlapping methods are correctly allowed
  • Overlapping methods correctly raise RuntimeError

The parametrized test on lines 46-58 is particularly valuable for verifying path normalization behavior.


100-111: LGTM!

The test correctly verifies that additional kwargs are passed through to the underlying app.add_api_route call, ensuring the wrapper doesn't lose configuration options.


4-4: No issues found. FastAPI 0.115.13 exists and the APIRoute import from fastapi.routing is compatible with this version. The code is correct.

@touale touale merged commit 8e74a4d into master Jan 6, 2026
6 checks passed
@touale touale deleted the feat/duplicate_route_detect branch January 6, 2026 08:45
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