Skip to content

Add circuit breakers - #167

Open
Zingzy wants to merge 4 commits into
mainfrom
fix/security-vuln
Open

Add circuit breakers#167
Zingzy wants to merge 4 commits into
mainfrom
fix/security-vuln

Conversation

@Zingzy

@Zingzy Zingzy commented May 2, 2026

Copy link
Copy Markdown
Member

Summary by Sourcery

Introduce request-level timeouts and simplify health checks to act as a pure liveness probe while expanding redirect URI validation for device auth integrations.

New Features:

  • Add request timeout middleware to enforce per-request deadlines and return 504 on timeouts.
  • Support wildcard-based redirect URI validation for device auth clients, enabling integrations such as Raycast.

Enhancements:

  • Configure MongoDB client and metrics aggregation with explicit timeouts to cap long-running operations.
  • Simplify the /health endpoint and response schema to act purely as a liveness probe with a minimal payload.
  • Extend app configuration for the Raycast device app with redirect URIs, links, and permissions metadata.

Tests:

  • Update health, smoke, and schema tests to match the new liveness-only health contract.
  • Adjust device auth integration tests to use the new redirect URI validation semantics.
  • Add unit tests covering wildcard redirect URI matching for device auth.

Summary by CodeRabbit

Release Notes

  • New Features

    • Request timeout enforcement with configurable limits; requests exceeding the timeout receive 504 error responses.
    • Wildcard redirect URI pattern support for device authentication flows.
    • Raycast app integration is now live with proper redirect URI configuration.
  • Improvements

    • Simplified health check endpoint to provide basic liveness status only.
    • Enhanced redirect URI validation with more flexible pattern matching.

Zingzy added 4 commits April 26, 2026 03:03
- Updated Raycast extension description to clarify link management functionality.
- Added redirect URI allowlist validation to ensure secure handling of redirect URIs in device authentication.
- Improved the `validate_redirect_uri` method to support exact and prefix matching for redirect URIs, enhancing security for OAuth clients.
- Changed the status of the Raycast app in the configuration file to reflect its current availability, enhancing clarity for users and developers.
- Updated the redirect URI format in the configuration for the Raycast app to support query parameters.
- Refactored the redirect URI validation logic to utilize a service method for improved security and maintainability.
- Added comprehensive unit tests for the new validation logic, including support for wildcard matching and various edge cases.
Copilot AI review requested due to automatic review settings May 2, 2026 22:39
@Zingzy Zingzy self-assigned this May 2, 2026
@sourcery-ai

sourcery-ai Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Refactors the /health endpoint into a simple liveness probe, introduces request-level timeouts and MongoDB socket/circuit timeouts, and adds wildcard-aware redirect URI validation (used for Raycast device auth), with accompanying config, app wiring, and tests.

Sequence diagram for request timeout middleware behavior

sequenceDiagram
    actor Client
    participant FastAPIApp
    participant RequestTimeoutMiddleware
    participant RequestHandler

    Client->>FastAPIApp: HTTP request
    FastAPIApp->>RequestTimeoutMiddleware: dispatch(request, call_next)

    alt Exempt path (/metric)
        RequestTimeoutMiddleware->>RequestHandler: call_next(request)
        RequestHandler-->>RequestTimeoutMiddleware: Response
        RequestTimeoutMiddleware-->>FastAPIApp: Response
        FastAPIApp-->>Client: 200 OK or other
    else Non-exempt path
        RequestTimeoutMiddleware->>RequestTimeoutMiddleware: asyncio.wait_for(call_next(request), timeout_seconds)
        alt Handler completes before timeout
            RequestTimeoutMiddleware->>RequestHandler: call_next(request)
            RequestHandler-->>RequestTimeoutMiddleware: Response
            RequestTimeoutMiddleware-->>FastAPIApp: Response
            FastAPIApp-->>Client: 2xx/4xx/5xx
        else Timeout exceeded
            RequestTimeoutMiddleware-->>FastAPIApp: JSONResponse 504 request_timeout
            FastAPIApp-->>Client: 504 Gateway Timeout
        end
    end
Loading

Sequence diagram for device auth redirect URI validation with wildcards

sequenceDiagram
    actor Browser
    participant DeviceAuthRoute
    participant DeviceAuthSvc
    participant AppEntry
    participant RedirectResponse

    Browser->>DeviceAuthRoute: GET /auth/device/login?redirect_uri=...
    DeviceAuthRoute->>DeviceAuthSvc: create_device_auth_code(user_id, email, app_id)
    DeviceAuthSvc-->>DeviceAuthRoute: code

    DeviceAuthRoute->>DeviceAuthRoute: _build_callback_redirect(code, state, redirect_uri, app, svc)
    DeviceAuthRoute->>DeviceAuthSvc: validate_redirect_uri(redirect_uri, app)

    alt redirect_uri empty
        DeviceAuthSvc-->>DeviceAuthRoute: True
        DeviceAuthRoute->>RedirectResponse: /auth/device/callback?code, state
        RedirectResponse-->>Browser: 302 to default callback
    else redirect_uri exact or wildcard match in app.redirect_uris
        DeviceAuthSvc-->>DeviceAuthRoute: True
        DeviceAuthRoute->>RedirectResponse: redirect_uri with code, state appended
        RedirectResponse-->>Browser: 302 to redirect_uri
    else redirect_uri not allowed
        DeviceAuthSvc-->>DeviceAuthRoute: False
        DeviceAuthRoute->>RedirectResponse: /auth/device/callback?code, state
        RedirectResponse-->>Browser: 302 to default callback
    end
Loading

Updated class diagram for timeout middleware, settings, health response, and device auth service

classDiagram
    class RequestTimeoutMiddleware {
        - float _timeout
        + RequestTimeoutMiddleware(app, timeout_seconds: float)
        + dispatch(request: Request, call_next: RequestResponseEndpoint) Response
    }

    class DatabaseSettings {
        + str mongodb_uri
        + str db_name
        + int max_pool_size
        + int min_pool_size
        + int server_selection_timeout_ms
        + int connect_timeout_ms
        + int socket_timeout_ms
    }

    class AppSettings {
        + int max_active_api_keys
        + int max_date_range_days
        + float http_client_timeout
        + float request_timeout_seconds
        + float blocked_url_regex_timeout
        + float shortlink_regex_timeout
        + float reserved_path_regex_timeout
        + float reserved_subdomain_regex_timeout
        + float reserved_host_regex_timeout
        + _must_be_positive_float(v: float, info) float
    }

    class DeviceAuthSvc {
        + resolve_app(app_id: str) AppEntry
        + validate_redirect_uri(redirect_uri: str, app: AppEntry) bool
        + create_device_auth_code(user_id: ObjectId, email: str, app_id: str) str
    }

    class AppEntry {
        + str id
        + list~str~ redirect_uris
        + bool is_live_device_app()
    }

    class HealthResponse {
        + str status
    }

    class FastAPIApp {
        + middleware RequestTimeoutMiddleware
        + DatabaseSettings db_settings
        + AppSettings settings
    }

    FastAPIApp "1" o-- "1" DatabaseSettings
    FastAPIApp "1" o-- "1" AppSettings
    FastAPIApp "1" o-- "1" RequestTimeoutMiddleware
    DeviceAuthSvc "1" --> "1" AppEntry
    RequestTimeoutMiddleware ..> HealthResponse : returns on timeout
    DeviceAuthSvc ..> HealthResponse : unaffected by change
Loading

File-Level Changes

Change Details Files
Simplify /health into a pure liveness probe with no DB/Redis dependencies and update response schema and tests accordingly.
  • Remove MongoDB/Redis health checks from the health_check route and return a fixed 200/ok response body.
  • Update HealthResponse schema to only expose a status field and drop checks.
  • Adjust integration, smoke, and unit tests to expect a simple {"status": "ok"} response and no DB/Redis interaction in health.
  • Simplify test app construction for health tests to not inject mocked DB/Redis state.
routes/health_routes.py
schemas/dto/responses/common.py
tests/integration/test_health.py
tests/smoke/test_startup.py
tests/unit/schemas/dto/test_common.py
Introduce request timeout middleware to cap request processing time and configure DB client timeouts to avoid hanging connections.
  • Add RequestTimeoutMiddleware that wraps each request in asyncio.wait_for, exempting the /metric route and returning a 504 JSON error on timeout with structured logging.
  • Wire RequestTimeoutMiddleware into FastAPI app creation with a configurable timeout sourced from AppSettings.request_timeout_seconds.
  • Extend DatabaseSettings with MongoDB server selection, connect, and socket timeout settings and pass them to AsyncMongoClient in the lifespan startup.
  • Validate request_timeout_seconds as a positive float along with existing float settings.
middleware/timeout.py
app.py
config.py
Tighten device auth redirect URI validation to support explicit wildcard entries and use it consistently in redirect building, including Raycast app config.
  • Enhance DeviceAuthService.validate_redirect_uri to support exact matches and prefix matches for allowlist entries ending in '' while still allowing empty redirect URIs.
  • Update _build_callback_redirect to delegate redirect URI validation to DeviceAuthService.validate_redirect_uri instead of doing a simple membership check.
  • Change Raycast app configuration to mark it live, add a wildcard redirect URI for its OAuth-style callback, and document new permissions/links.
  • Adjust device auth service test fixture to mirror the wildcard matching logic used in validate_redirect_uri.
  • Add focused unit tests covering validate_redirect_uri behavior for exact, wildcard, mixed, and bare '' cases.
services/auth/device.py
routes/auth/device.py
config/apps.yaml
tests/integration/test_device_auth.py
tests/unit/services/test_redirect_uri.py
Protect the metrics aggregation endpoint against long-running MongoDB operations with a per-query maxTimeMS limit.
  • Set maxTimeMS=90000 on the MongoDB aggregate call used by the /metric endpoint to cap server-side execution time, aligning with the request timeout strategy.
routes/legacy/url_shortener.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 May 2, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces request timeout middleware to enforce maximum request handling duration, simplifies the health endpoint from a dependency-aware check to a liveness probe, enhances redirect URI validation with wildcard pattern support, and updates database connection timeout configuration and Raycast app metadata.

Changes

Request Timeout Infrastructure

Layer / File(s) Summary
Configuration & Type Updates
config.py, app.py
DatabaseSettings adds server_selection_timeout_ms, connect_timeout_ms, and socket_timeout_ms fields. AppSettings adds request_timeout_seconds (default 8.0) to validator. app.py updates lifespan type from AsyncIterator to AsyncGenerator.
Middleware Implementation
middleware/timeout.py
New RequestTimeoutMiddleware enforces request deadlines using asyncio.wait_for. Exempts /metric path. Catches asyncio.TimeoutError, logs metadata, and returns 504 with {"error": "Request timed out", "code": "request_timeout"}.
Integration & Tuning
app.py, routes/legacy/url_shortener.py
Middleware registered as innermost component with configured timeout. Metric aggregation pipeline adds maxTimeMS=90000 for explicit query deadline.
Tests
tests/integration/*, tests/smoke/*
Integration and smoke tests updated to account for new middleware presence and timeout configuration.

Health Endpoint as Liveness Probe

Layer / File(s) Summary
Route & Response DTO
routes/health_routes.py, schemas/dto/responses/common.py
/health endpoint simplified from dependency-aware check to stateless liveness probe. Removes Request parameter and dependency checks. HealthResponse now contains only status: str. HealthChecks class removed.
Tests
tests/integration/test_health.py, tests/smoke/test_startup.py, tests/unit/schemas/dto/test_common.py
Test fixtures simplified to use bare FastAPI() without injected DB/Redis mocks. Assertions reduced from multi-state checks (healthy/degraded/unhealthy) to single fixed response {"status": "ok"}.

Redirect URI Validation with Wildcard Support

Layer / File(s) Summary
Service Enhancement
services/auth/device.py
validate_redirect_uri updated to support wildcard allowlist patterns. Entries ending with * are treated as prefix matches. Empty redirect_uri remains allowed. Exact matches still supported for non-wildcard entries.
Route Integration
routes/auth/device.py
Device auth handlers updated to pass DeviceAuthSvc to _build_callback_redirect for stricter URI validation instead of direct membership check.
Tests
tests/integration/test_device_auth.py, tests/unit/services/test_redirect_uri.py
Integration fixture mock updated for wildcard matching. New unit test module covers empty URI, exact matches, prefix/wildcard patterns, host/path mismatch rejection, and bare * global match.

Sequence Diagram

sequenceDiagram
    participant Client
    participant RequestTimeoutMiddleware
    participant Handler as App Handler
    participant Response

    Client->>RequestTimeoutMiddleware: HTTP Request
    Note over RequestTimeoutMiddleware: Check if path in EXEMPT_PATHS
    alt Path exempted (e.g., /metric)
        RequestTimeoutMiddleware->>Handler: call_next(request)
    else Path not exempted
        RequestTimeoutMiddleware->>RequestTimeoutMiddleware: Wrap with asyncio.wait_for(timeout=8.0s)
        RequestTimeoutMiddleware->>Handler: call_next(request) [with deadline]
        alt Request completes within timeout
            Handler->>Response: Normal response
            Response->>Client: 200/201/etc.
        else Request exceeds timeout
            RequestTimeoutMiddleware->>RequestTimeoutMiddleware: asyncio.TimeoutError caught
            RequestTimeoutMiddleware->>RequestTimeoutMiddleware: Log with request metadata
            RequestTimeoutMiddleware->>Response: 504 JSON error
            Response->>Client: {"error": "Request timed out", "code": "request_timeout"}
        end
    end
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested Labels

✨ Refactor, ⚙️ Config, 🔐 Auth


🐰 A hop, a skip, liveness so quick!

Timeouts and redirects work their magic trick,

Health probes are lean, no checks to adhere—

Infrastructure hops clear, with each middleware tier! 🎯

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title 'Add circuit breakers' does not align with the actual changes, which focus on request timeout handling, health endpoint simplification, redirect URI validation, and configuration updates rather than circuit breaker patterns. Revise the title to accurately reflect the primary changes, such as 'Add request timeout middleware and simplify health endpoint' or similar.
Docstring Coverage ⚠️ Warning Docstring coverage is 38.24% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/security-vuln

Tip

💬 Introducing Slack Agent: Turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get your free trial and get 200 agent minutes per Slack user (a $50 value).


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
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

@Zingzy Zingzy added the Security Issues related to Security label May 2, 2026
@Zingzy Zingzy moved this to 🏗️ In Progress in spoo.me Development Roadmap May 2, 2026

@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 - I've found 4 issues, and left some high level feedback:

  • The request timeout middleware currently exempts only exact path matches (e.g. "/metric"); if you ever serve the same endpoint under a prefix (versioned or mounted app) you may want to switch this to a prefix or pattern match instead of a hard-coded exact path set.
  • The 504 timeout response uses a bespoke JSON shape ({"error": ..., "code": ...}); consider wiring this through your existing error-handling/response schema so timeout errors are consistent with other API error responses.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The request timeout middleware currently exempts only exact path matches (e.g. "/metric"); if you ever serve the same endpoint under a prefix (versioned or mounted app) you may want to switch this to a prefix or pattern match instead of a hard-coded exact path set.
- The 504 timeout response uses a bespoke JSON shape (`{"error": ..., "code": ...}`); consider wiring this through your existing error-handling/response schema so timeout errors are consistent with other API error responses.

## Individual Comments

### Comment 1
<location path="tests/integration/test_health.py" line_range="21-30" />
<code_context>
     return app


 class TestHealthEndpoint:
-    def test_healthy_when_both_ok(self):
-        app = _build_test_app(mongo_ok=True, redis_ok=True)
-        with TestClient(app) as client:
-            resp = client.get("/health")
-        assert resp.status_code == 200
-        body = resp.json()
-        assert body["status"] == "healthy"
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen `test_no_db_dependency` to explicitly prove that `/health` does not touch app state

Currently `test_returns_ok` and `test_no_db_dependency` both just assert a 200 and static body, and `test_no_db_dependency` only *implicitly* checks that `app.state` is not used. To encode that contract more clearly, consider making the test fail if `app.state` is touched, e.g. by assigning sentinels that raise on access:

```python
app = _build_test_app()

class Sentinel:
    def __getattr__(self, name):
        raise AssertionError("health endpoint must not touch DB/Redis")

app.state.db = Sentinel()
app.state.redis = Sentinel()

with TestClient(app) as client:
    resp = client.get("/health")

assert resp.status_code == 200
assert resp.json() == {"status": "ok"}
```

This ensures any future reintroduction of DB/Redis access in `/health` is caught for the right reason, instead of just duplicating `test_returns_ok`.

Suggested implementation:

```python
class TestHealthEndpoint:
    def test_no_db_dependency(self):
        app = _build_test_app()

        class Sentinel:
            def __getattr__(self, name):
                raise AssertionError("health endpoint must not touch DB/Redis")

        app.state.db = Sentinel()
        app.state.redis = Sentinel()

        with TestClient(app) as client:
            resp = client.get("/health")

        assert resp.status_code == 200
        assert resp.json() == {"status": "ok"}

```

1. Ensure `TestClient` is imported at the top of `tests/integration/test_health.py`, e.g.:
   `from starlette.testclient import TestClient` or `from fastapi.testclient import TestClient`, matching the rest of your test suite.
2. If your `/health` endpoint returns a different JSON body (e.g. `{"status": "healthy"}` or includes additional keys), adjust the `assert resp.json() == {"status": "ok"}` accordingly to match the actual contract you want to enforce.
</issue_to_address>

### Comment 2
<location path="tests/smoke/test_startup.py" line_range="19-23" />
<code_context>


 def test_health_endpoint_responds(smoke_client: TestClient) -> None:
-    """Health endpoint should return a response (mock DB will show unhealthy, but no crash)."""
+    """Liveness probe — always 200, no dependency checks."""
     resp = smoke_client.get("/health")
-    assert resp.status_code in (200, 503)
-    data = resp.json()
-    assert "status" in data
-    assert "checks" in data
+    assert resp.status_code == 200
+    assert resp.json() == {"status": "ok"}


</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for the new `RequestTimeoutMiddleware`, including timeout and exempt paths

The new `RequestTimeoutMiddleware` wired in `app.py` isn’t covered by tests. Please add coverage for at least:

1. A handler that sleeps longer than the configured timeout and returns a 504 with the expected JSON body.
2. An exempt path (e.g. `/metric`) that does not time out even if it sleeps longer than the global timeout.
3. (Optional) Using a very short timeout (e.g. 0.01s) so the tests stay fast.

These can go in a small dedicated integration test module (e.g. `tests/integration/test_timeout_middleware.py`) or be folded into the smoke tests, so regressions in the timeout behavior are caught.

Suggested implementation:

```python
def test_health_endpoint_responds(smoke_client: TestClient) -> None:
    """Liveness probe — always 200, no dependency checks."""
    resp = smoke_client.get("/health")
    assert resp.status_code == 200


def test_request_timeout_returns_504(timeout_client: TestClient) -> None:
    """
    Requests exceeding the configured timeout should return 504 with a JSON body.

    The /timeout-test handler is expected to sleep longer than the configured timeout.
    """
    resp = timeout_client.get("/timeout-test")
    assert resp.status_code == 504

    data = resp.json()
    # Be flexible on the exact schema but ensure we have a timeout indication.
    assert isinstance(data, dict)
    # Common patterns: {"detail": "...timeout..."} or {"error": "...timeout..."}
    text = " ".join(str(v) for v in data.values()).lower()
    assert "timeout" in text


def test_timeout_middleware_does_not_affect_exempt_path(timeout_client: TestClient) -> None:
    """
    Exempt paths (e.g. /metrics) should not be subject to the global timeout,
    even if their handler sleeps longer than the configured timeout.
    """
    resp = timeout_client.get("/metrics")
    assert resp.status_code == 200

```

To make these tests pass, the following additional work is needed elsewhere in the codebase:

1. **Provide a timeout-aware TestClient fixture**:
   - Add a `timeout_client: TestClient` fixture (e.g. in `tests/conftest.py`) that:
     - Constructs the FastAPI/Starlette app using the normal app factory.
     - Configures `RequestTimeoutMiddleware` with a very short timeout (e.g. 0.01s) for fast tests, via settings/env or direct kwargs.
   - Ensure this fixture is independent from `smoke_client` so it can override timeout-specific configuration.

2. **Add a slow test handler for the timeout scenario**:
   - In the app used by tests (or in a test-only router), add a route such as:
     - `GET /timeout-test` that performs `await asyncio.sleep(x)` where `x` is significantly larger than the configured timeout, so it reliably triggers the middleware.

3. **Mark the exempt path in the middleware**:
   - Ensure that the timeout middleware is configured so that `/metrics` (or whatever path is used for metrics) is in the middleware’s exempt-path list.
   - If your metrics endpoint uses a different path (e.g. `/metric`), update the test to use the correct exempt path.

4. **Align JSON error shape if needed**:
   - If your `RequestTimeoutMiddleware` returns a specific JSON schema (e.g. `{"detail": "Request timed out"}`), you can tighten the assertion in `test_request_timeout_returns_504` to match it exactly.
</issue_to_address>

### Comment 3
<location path="tests/integration/test_device_auth.py" line_range="109-115" />
<code_context>
     # resolve_app and validate_redirect_uri are sync — must not be coroutines
     svc.resolve_app = MagicMock(side_effect=_resolve_app)
     svc.validate_redirect_uri = MagicMock(
-        side_effect=lambda uri, app: not uri or uri in app.redirect_uris
+        side_effect=lambda uri, app: (
+            not uri
+            or any(
+                uri.startswith(a[:-1]) if a.endswith("*") else uri == a
+                for a in app.redirect_uris
+            )
+        )
     )
</code_context>
<issue_to_address>
**suggestion (testing):** Consider at least one integration test that uses the real `validate_redirect_uri` for end-to-end coverage

The mock now copies the wildcard logic from the implementation, so existing tests will keep passing even if the real `validate_redirect_uri` later changes. You already have good unit coverage, but there’s no integration‑level check that `_build_callback_redirect` works with the real `DeviceAuthService.validate_redirect_uri`.

Please add a single integration test that:
- Uses the real `svc.validate_redirect_uri` (no override),
- Sets up an app with a wildcard redirect URI, and
- Asserts that the redirect response uses that wildcard redirect with the appended `code`/`state`.

This will give you one end‑to‑end check tying together the route, service, and redirect behaviour.

Suggested implementation:

```python
    # resolve_app and validate_redirect_uri are sync — must not be coroutines
    svc.resolve_app = MagicMock(side_effect=_resolve_app)
    svc.validate_redirect_uri = MagicMock(
        side_effect=lambda uri, app: (
            not uri
            or any(
                uri.startswith(a[:-1]) if a.endswith("*") else uri == a
                for a in app.redirect_uris
            )
        )
    )
    return svc


@pytest.mark.integration
def test_device_callback_uses_wildcard_redirect_with_real_validation(
    client,
    app_factory,
):
    """
    Integration-level check that _build_callback_redirect cooperates with the real
    DeviceAuthService.validate_redirect_uri for wildcard redirect URIs.
    """

    # Arrange: create an app that allows a wildcard redirect URI
    app = app_factory(redirect_uris=["https://example.com/callback/*"])

    # This is the specific redirect URI we want to use; it should be accepted
    redirect_uri = "https://example.com/callback/flow-123"

    # Act: call the device callback endpoint, which should internally:
    # - resolve the app
    # - validate the redirect_uri using DeviceAuthService.validate_redirect_uri
    # - build a redirect with appended code/state
    response = client.get(
        "/device/callback",
        query_string={
            "client_id": app.client_id,
            "redirect_uri": redirect_uri,
        },
    )

    # Assert: we got redirected to the provided redirect_uri with code/state
    assert response.status_code == 302
    location = response.headers["Location"]

    assert location.startswith(redirect_uri)
    assert "code=" in location
    assert "state=" in location

```

To make this compile and pass, you’ll likely need to:

1. Ensure `pytest` is imported (`import pytest`) at the top of `tests/integration/test_device_auth.py` if it is not already.
2. Adjust the fixture names to match your existing test helpers:
   - If your factory for creating client applications is not called `app_factory`, rename the parameter and usage accordingly (e.g. `application_factory`, `make_app`, etc.).
   - If your device callback route is not `/device/callback`, update the `client.get(...)` path to the actual callback endpoint that triggers `_build_callback_redirect`.
   - If your application model does not expose `client_id` as `app.client_id`, replace that access with the appropriate attribute.
3. Make sure the app created by `app_factory` is persisted/visible to the running application under test (e.g. committing to the DB or registering in whatever storage `resolve_app` uses), consistent with how other integration tests create apps.
4. If you want this test to bypass the `MagicMock` override of `validate_redirect_uri`, introduce a separate fixture that *does not* patch `svc.validate_redirect_uri` and use that fixture’s client in this test, or scope the existing mocking fixture so it doesn’t apply here (e.g. via a different marker or fixture layering).
</issue_to_address>

### Comment 4
<location path="tests/unit/services/test_redirect_uri.py" line_range="29-33" />
<code_context>
+    return AppEntry(name="test", description="test app", redirect_uris=redirect_uris)
+
+
+class TestValidateRedirectUri:
+    """Tests for DeviceAuthService.validate_redirect_uri."""
+
+    def setup_method(self):
+        self.svc = DeviceAuthService.__new__(DeviceAuthService)
+
+    def test_empty_uri_allowed(self):
+        app = _app(["https://example.com/callback"])
+        assert self.svc.validate_redirect_uri("", app) is True
+
+    def test_exact_match(self):
+        app = _app(["https://example.com/callback"])
+        assert (
+            self.svc.validate_redirect_uri("https://example.com/callback", app) is True
+        )
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test for patterns where `*` appears but not as a suffix, to document literal behaviour

One missing edge case is a redirect URI that contains `*` not at the end (e.g. `"https://exa*mple.com/callback"`). Since only entries ending with `*` are treated as wildcards, such patterns are effectively literals and will rarely match real URIs. A test like:

```python
def test_internal_asterisk_is_literal(self):
    app = _app(["https://exa*mple.com/callback"])
    assert self.svc.validate_redirect_uri("https://exa*mple.com/callback", app) is True
    assert self.svc.validate_redirect_uri("https://example.com/callback", app) is False
```

would document and lock in this behavior around non-suffix `*`.

```suggestion
    def test_exact_no_match(self):
        app = _app(["https://example.com/callback"])
        assert self.svc.validate_redirect_uri("https://evil.com/callback", app) is False

    def test_internal_asterisk_is_literal(self):
        app = _app(["https://exa*mple.com/callback"])
        assert (
            self.svc.validate_redirect_uri("https://exa*mple.com/callback", app) is True
        )
        assert (
            self.svc.validate_redirect_uri("https://example.com/callback", app) is False
        )

    def test_wildcard_query_match(self):
```
</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 on lines 21 to +30
class TestHealthEndpoint:
def test_healthy_when_both_ok(self):
app = _build_test_app(mongo_ok=True, redis_ok=True)
with TestClient(app) as client:
resp = client.get("/health")
assert resp.status_code == 200
body = resp.json()
assert body["status"] == "healthy"
assert body["checks"]["mongodb"] == "ok"
assert body["checks"]["redis"] == "ok"

def test_unhealthy_when_mongo_fails(self):
app = _build_test_app(mongo_ok=False, redis_ok=True)
with TestClient(app) as client:
resp = client.get("/health")
assert resp.status_code == 503
body = resp.json()
assert body["status"] == "unhealthy"
assert body["checks"]["mongodb"] == "error"

def test_degraded_when_redis_fails(self):
app = _build_test_app(mongo_ok=True, redis_ok=False)
with TestClient(app) as client:
resp = client.get("/health")
assert resp.status_code == 200
body = resp.json()
assert body["status"] == "degraded"
assert body["checks"]["redis"] == "error"

def test_degraded_when_redis_not_configured(self):
app = _build_test_app(mongo_ok=True, redis_configured=False)
def test_returns_ok(self):
app = _build_test_app()
with TestClient(app) as client:
resp = client.get("/health")
assert resp.status_code == 200
body = resp.json()
assert body["status"] == "degraded"
assert body["checks"]["redis"] == "not_configured"
assert resp.json() == {"status": "ok"}

def test_response_shape(self):
def test_no_db_dependency(self):
"""Health must not touch app.state.db or app.state.redis."""

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.

suggestion (testing): Strengthen test_no_db_dependency to explicitly prove that /health does not touch app state

Currently test_returns_ok and test_no_db_dependency both just assert a 200 and static body, and test_no_db_dependency only implicitly checks that app.state is not used. To encode that contract more clearly, consider making the test fail if app.state is touched, e.g. by assigning sentinels that raise on access:

app = _build_test_app()

class Sentinel:
    def __getattr__(self, name):
        raise AssertionError("health endpoint must not touch DB/Redis")

app.state.db = Sentinel()
app.state.redis = Sentinel()

with TestClient(app) as client:
    resp = client.get("/health")

assert resp.status_code == 200
assert resp.json() == {"status": "ok"}

This ensures any future reintroduction of DB/Redis access in /health is caught for the right reason, instead of just duplicating test_returns_ok.

Suggested implementation:

class TestHealthEndpoint:
    def test_no_db_dependency(self):
        app = _build_test_app()

        class Sentinel:
            def __getattr__(self, name):
                raise AssertionError("health endpoint must not touch DB/Redis")

        app.state.db = Sentinel()
        app.state.redis = Sentinel()

        with TestClient(app) as client:
            resp = client.get("/health")

        assert resp.status_code == 200
        assert resp.json() == {"status": "ok"}
  1. Ensure TestClient is imported at the top of tests/integration/test_health.py, e.g.:
    from starlette.testclient import TestClient or from fastapi.testclient import TestClient, matching the rest of your test suite.
  2. If your /health endpoint returns a different JSON body (e.g. {"status": "healthy"} or includes additional keys), adjust the assert resp.json() == {"status": "ok"} accordingly to match the actual contract you want to enforce.

Comment on lines 19 to +23
def test_health_endpoint_responds(smoke_client: TestClient) -> None:
"""Health endpoint should return a response (mock DB will show unhealthy, but no crash)."""
"""Liveness probe — always 200, no dependency checks."""
resp = smoke_client.get("/health")
assert resp.status_code in (200, 503)
data = resp.json()
assert "status" in data
assert "checks" in data
assert resp.status_code == 200
assert resp.json() == {"status": "ok"}

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.

suggestion (testing): Add tests for the new RequestTimeoutMiddleware, including timeout and exempt paths

The new RequestTimeoutMiddleware wired in app.py isn’t covered by tests. Please add coverage for at least:

  1. A handler that sleeps longer than the configured timeout and returns a 504 with the expected JSON body.
  2. An exempt path (e.g. /metric) that does not time out even if it sleeps longer than the global timeout.
  3. (Optional) Using a very short timeout (e.g. 0.01s) so the tests stay fast.

These can go in a small dedicated integration test module (e.g. tests/integration/test_timeout_middleware.py) or be folded into the smoke tests, so regressions in the timeout behavior are caught.

Suggested implementation:

def test_health_endpoint_responds(smoke_client: TestClient) -> None:
    """Liveness probe — always 200, no dependency checks."""
    resp = smoke_client.get("/health")
    assert resp.status_code == 200


def test_request_timeout_returns_504(timeout_client: TestClient) -> None:
    """
    Requests exceeding the configured timeout should return 504 with a JSON body.

    The /timeout-test handler is expected to sleep longer than the configured timeout.
    """
    resp = timeout_client.get("/timeout-test")
    assert resp.status_code == 504

    data = resp.json()
    # Be flexible on the exact schema but ensure we have a timeout indication.
    assert isinstance(data, dict)
    # Common patterns: {"detail": "...timeout..."} or {"error": "...timeout..."}
    text = " ".join(str(v) for v in data.values()).lower()
    assert "timeout" in text


def test_timeout_middleware_does_not_affect_exempt_path(timeout_client: TestClient) -> None:
    """
    Exempt paths (e.g. /metrics) should not be subject to the global timeout,
    even if their handler sleeps longer than the configured timeout.
    """
    resp = timeout_client.get("/metrics")
    assert resp.status_code == 200

To make these tests pass, the following additional work is needed elsewhere in the codebase:

  1. Provide a timeout-aware TestClient fixture:

    • Add a timeout_client: TestClient fixture (e.g. in tests/conftest.py) that:
      • Constructs the FastAPI/Starlette app using the normal app factory.
      • Configures RequestTimeoutMiddleware with a very short timeout (e.g. 0.01s) for fast tests, via settings/env or direct kwargs.
    • Ensure this fixture is independent from smoke_client so it can override timeout-specific configuration.
  2. Add a slow test handler for the timeout scenario:

    • In the app used by tests (or in a test-only router), add a route such as:
      • GET /timeout-test that performs await asyncio.sleep(x) where x is significantly larger than the configured timeout, so it reliably triggers the middleware.
  3. Mark the exempt path in the middleware:

    • Ensure that the timeout middleware is configured so that /metrics (or whatever path is used for metrics) is in the middleware’s exempt-path list.
    • If your metrics endpoint uses a different path (e.g. /metric), update the test to use the correct exempt path.
  4. Align JSON error shape if needed:

    • If your RequestTimeoutMiddleware returns a specific JSON schema (e.g. {"detail": "Request timed out"}), you can tighten the assertion in test_request_timeout_returns_504 to match it exactly.

Comment on lines 109 to +115
svc.validate_redirect_uri = MagicMock(
side_effect=lambda uri, app: not uri or uri in app.redirect_uris
side_effect=lambda uri, app: (
not uri
or any(
uri.startswith(a[:-1]) if a.endswith("*") else uri == a
for a in app.redirect_uris
)

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.

suggestion (testing): Consider at least one integration test that uses the real validate_redirect_uri for end-to-end coverage

The mock now copies the wildcard logic from the implementation, so existing tests will keep passing even if the real validate_redirect_uri later changes. You already have good unit coverage, but there’s no integration‑level check that _build_callback_redirect works with the real DeviceAuthService.validate_redirect_uri.

Please add a single integration test that:

  • Uses the real svc.validate_redirect_uri (no override),
  • Sets up an app with a wildcard redirect URI, and
  • Asserts that the redirect response uses that wildcard redirect with the appended code/state.

This will give you one end‑to‑end check tying together the route, service, and redirect behaviour.

Suggested implementation:

    # resolve_app and validate_redirect_uri are sync — must not be coroutines
    svc.resolve_app = MagicMock(side_effect=_resolve_app)
    svc.validate_redirect_uri = MagicMock(
        side_effect=lambda uri, app: (
            not uri
            or any(
                uri.startswith(a[:-1]) if a.endswith("*") else uri == a
                for a in app.redirect_uris
            )
        )
    )
    return svc


@pytest.mark.integration
def test_device_callback_uses_wildcard_redirect_with_real_validation(
    client,
    app_factory,
):
    """
    Integration-level check that _build_callback_redirect cooperates with the real
    DeviceAuthService.validate_redirect_uri for wildcard redirect URIs.
    """

    # Arrange: create an app that allows a wildcard redirect URI
    app = app_factory(redirect_uris=["https://example.com/callback/*"])

    # This is the specific redirect URI we want to use; it should be accepted
    redirect_uri = "https://example.com/callback/flow-123"

    # Act: call the device callback endpoint, which should internally:
    # - resolve the app
    # - validate the redirect_uri using DeviceAuthService.validate_redirect_uri
    # - build a redirect with appended code/state
    response = client.get(
        "/device/callback",
        query_string={
            "client_id": app.client_id,
            "redirect_uri": redirect_uri,
        },
    )

    # Assert: we got redirected to the provided redirect_uri with code/state
    assert response.status_code == 302
    location = response.headers["Location"]

    assert location.startswith(redirect_uri)
    assert "code=" in location
    assert "state=" in location

To make this compile and pass, you’ll likely need to:

  1. Ensure pytest is imported (import pytest) at the top of tests/integration/test_device_auth.py if it is not already.
  2. Adjust the fixture names to match your existing test helpers:
    • If your factory for creating client applications is not called app_factory, rename the parameter and usage accordingly (e.g. application_factory, make_app, etc.).
    • If your device callback route is not /device/callback, update the client.get(...) path to the actual callback endpoint that triggers _build_callback_redirect.
    • If your application model does not expose client_id as app.client_id, replace that access with the appropriate attribute.
  3. Make sure the app created by app_factory is persisted/visible to the running application under test (e.g. committing to the DB or registering in whatever storage resolve_app uses), consistent with how other integration tests create apps.
  4. If you want this test to bypass the MagicMock override of validate_redirect_uri, introduce a separate fixture that does not patch svc.validate_redirect_uri and use that fixture’s client in this test, or scope the existing mocking fixture so it doesn’t apply here (e.g. via a different marker or fixture layering).

Comment on lines +29 to +33
def test_exact_no_match(self):
app = _app(["https://example.com/callback"])
assert self.svc.validate_redirect_uri("https://evil.com/callback", app) is False

def test_wildcard_query_match(self):

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.

suggestion (testing): Add a test for patterns where * appears but not as a suffix, to document literal behaviour

One missing edge case is a redirect URI that contains * not at the end (e.g. "https://exa*mple.com/callback"). Since only entries ending with * are treated as wildcards, such patterns are effectively literals and will rarely match real URIs. A test like:

def test_internal_asterisk_is_literal(self):
    app = _app(["https://exa*mple.com/callback"])
    assert self.svc.validate_redirect_uri("https://exa*mple.com/callback", app) is True
    assert self.svc.validate_redirect_uri("https://example.com/callback", app) is False

would document and lock in this behavior around non-suffix *.

Suggested change
def test_exact_no_match(self):
app = _app(["https://example.com/callback"])
assert self.svc.validate_redirect_uri("https://evil.com/callback", app) is False
def test_wildcard_query_match(self):
def test_exact_no_match(self):
app = _app(["https://example.com/callback"])
assert self.svc.validate_redirect_uri("https://evil.com/callback", app) is False
def test_internal_asterisk_is_literal(self):
app = _app(["https://exa*mple.com/callback"])
assert (
self.svc.validate_redirect_uri("https://exa*mple.com/callback", app) is True
)
assert (
self.svc.validate_redirect_uri("https://example.com/callback", app) is False
)
def test_wildcard_query_match(self):

Copilot AI 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.

Pull request overview

This PR improves operational resilience for the FastAPI service by adding request/DB timeouts, simplifying /health into a liveness probe, and expanding device-auth redirect URI allowlisting to support explicit wildcard entries (e.g., Raycast’s query-string variations).

Changes:

  • Add a request-deadline middleware (504 on timeout) and wire it into app startup via a new request_timeout_seconds setting.
  • Add MongoDB client timeouts and cap the /metric aggregation via maxTimeMS.
  • Update device-auth redirect URI validation to support allowlist entries ending in *, and update tests/config accordingly.
  • Simplify /health response/schema to {"status": "ok"} and update tests.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
middleware/timeout.py New request timeout middleware returning 504 on deadline exceeded.
app.py Wires in RequestTimeoutMiddleware and Mongo client timeout options; updates lifespan type.
config.py Adds DB timeout settings and request_timeout_seconds app setting with validation.
routes/legacy/url_shortener.py Adds maxTimeMS to the /metric aggregation query.
routes/health_routes.py Converts /health into a liveness-only endpoint always returning 200.
schemas/dto/responses/common.py Removes dependency checks from HealthResponse.
services/auth/device.py Adds explicit *-suffix prefix matching for redirect URI allowlists.
routes/auth/device.py Uses the service’s redirect URI validation when building callback redirects.
config/apps.yaml Marks Raycast app live and adds wildcard redirect URI allowlist entry.
tests/unit/services/test_redirect_uri.py Adds unit tests for wildcard redirect URI matching behavior.
tests/integration/test_device_auth.py Updates mocked redirect URI validation to match new wildcard behavior.
tests/integration/test_health.py Updates integration tests for new liveness-only /health behavior.
tests/smoke/test_startup.py Updates smoke test expectations for /health response shape.
tests/unit/schemas/dto/test_common.py Updates HealthResponse DTO serialization test for new shape.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread routes/health_routes.py
Comment on lines +20 to +21
"""Liveness probe. Public, unauthenticated, no dependency checks."""
return JSONResponse(status_code=200, content={"status": "ok"})
Comment on lines +29 to +34
def test_no_db_dependency(self):
"""Health must not touch app.state.db or app.state.redis."""
app = _build_test_app()
with TestClient(app) as client:
resp = client.get("/health")
body = resp.json()
assert "status" in body
assert "checks" in body
assert "mongodb" in body["checks"]
assert "redis" in body["checks"]
assert resp.status_code == 200
Comment thread middleware/timeout.py
Comment on lines +33 to +39
log.error(
"request_deadline_exceeded",
method=request.method,
path=request.url.path,
timeout_seconds=self._timeout,
client_ip=get_client_ip(request),
)
Comment thread app.py
Comment on lines 228 to +247
# ── Middleware (registered in reverse execution order) ────────────────
# 1. Session — outermost, needed by Authlib OAuth for state storage
app.add_middleware(SessionMiddleware, secret_key=settings.secret_key)
# 2. Security headers — must be outer so HSTS/CSP/nosniff apply to
# all responses including CORS preflights (204) and body-limit (413)
app.add_middleware(SecurityHeadersMiddleware, hsts_enabled=settings.is_production)
# 2a. Long-lived cache headers for /static/*
app.add_middleware(StaticCacheHeadersMiddleware)
# 3. CORS
configure_cors(app, settings)
# 4. Body size limit
app.add_middleware(
MaxContentLengthMiddleware, max_content_length=settings.max_content_length
)
# 5. Request logging — innermost, logs all requests with request_id
# 5. Request logging — logs all requests with request_id
app.add_middleware(RequestLoggingMiddleware)
# 6. Request deadline — innermost
app.add_middleware(
RequestTimeoutMiddleware, timeout_seconds=settings.request_timeout_seconds
)
Comment thread middleware/timeout.py
Comment on lines +19 to +43
class RequestTimeoutMiddleware(BaseHTTPMiddleware):
def __init__(self, app, timeout_seconds: float = 8.0) -> None:
super().__init__(app)
self._timeout = timeout_seconds

async def dispatch(
self, request: Request, call_next: RequestResponseEndpoint
) -> Response:
if request.url.path in EXEMPT_PATHS:
return await call_next(request)

try:
return await asyncio.wait_for(call_next(request), timeout=self._timeout)
except asyncio.TimeoutError:
log.error(
"request_deadline_exceeded",
method=request.method,
path=request.url.path,
timeout_seconds=self._timeout,
client_ip=get_client_ip(request),
)
return JSONResponse(
status_code=504,
content={"error": "Request timed out", "code": "request_timeout"},
)

@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: 3

🧹 Nitpick comments (1)
routes/legacy/url_shortener.py (1)

561-561: ⚡ Quick win

Keep the /metric query deadline configurable.

maxTimeMS=90000 is now coupled to DatabaseSettings.socket_timeout_ms only by convention. If a self-hoster lowers the socket timeout, this route can start failing before Mongo hits its own deadline. Please source the aggregation cap from settings, or derive it from the socket timeout with some headroom.

♻️ Possible adjustment
-        cursor = await db["urls"].aggregate(METRIC_PIPELINE_V1, maxTimeMS=90000)
+        metric_query_timeout_ms = max(1000, settings.db.socket_timeout_ms - 5000)
+        cursor = await db["urls"].aggregate(
+            METRIC_PIPELINE_V1,
+            maxTimeMS=metric_query_timeout_ms,
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@routes/legacy/url_shortener.py` at line 561, The hardcoded maxTimeMS in the
aggregation call (cursor = await db["urls"].aggregate(METRIC_PIPELINE_V1,
maxTimeMS=90000)) should be sourced from configuration or derived from
DatabaseSettings.socket_timeout_ms with headroom; update the aggregation
invocation to use a settings value (e.g., settings.metric_max_time_ms) or
compute maxTimeMS = int(settings.socket_timeout_ms * 0.9) (or similar) and pass
that variable into db["urls"].aggregate, and ensure METRIC_PIPELINE_V1 and the
route handler that performs this aggregation read the same settings object so
the deadline is configurable and consistent with socket timeout.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@config.py`:
- Around line 26-29: Add startup validation to ensure the new Mongo timeout
config ints (server_selection_timeout_ms, connect_timeout_ms, socket_timeout_ms)
are positive values just like the existing float timeouts: check each value > 0
during config initialization and raise a clear ValueError (or log+exit) if any
are zero or negative. Locate the validation block used for the float timeout
fields and replicate the same positive-value check for these three integer
fields in the same config initialization path (the config class or function that
reads env vars) so bad deployments fail fast.

In `@services/auth/device.py`:
- Around line 79-82: The wildcard branch currently treats allowed == "*" as a
valid prefix match, allowing any redirect_uri; update the logic that iterates
over app.redirect_uris so that wildcard entries require a non-empty prefix
before treating the trailing "*" as a wildcard (i.e., only accept when
allowed.endswith("*") and the prefix part allowed[:-1] is non-empty), then
perform redirect_uri.startswith(prefix) using that prefix; use the existing loop
variables (app.redirect_uris, allowed, redirect_uri) to locate and change the
check.

In `@tests/integration/test_device_auth.py`:
- Around line 110-116: The mock side_effect currently short-circuits with "not
uri" which allows empty URIs when app.redirect_uris is empty; update the
side_effect lambda so it first requires a truthy uri and then checks any(match)
against app.redirect_uris (i.e., replace "not uri or any(...)" with "bool(uri)
and any(...)" or equivalent) so an empty allowlist (app.redirect_uris==[]) will
not validate an empty URI; locate the side_effect lambda and ensure it uses
app.redirect_uris and wildcard handling exactly as in the existing generator
logic.

---

Nitpick comments:
In `@routes/legacy/url_shortener.py`:
- Line 561: The hardcoded maxTimeMS in the aggregation call (cursor = await
db["urls"].aggregate(METRIC_PIPELINE_V1, maxTimeMS=90000)) should be sourced
from configuration or derived from DatabaseSettings.socket_timeout_ms with
headroom; update the aggregation invocation to use a settings value (e.g.,
settings.metric_max_time_ms) or compute maxTimeMS =
int(settings.socket_timeout_ms * 0.9) (or similar) and pass that variable into
db["urls"].aggregate, and ensure METRIC_PIPELINE_V1 and the route handler that
performs this aggregation read the same settings object so the deadline is
configurable and consistent with socket timeout.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7103bab3-6df1-42c0-b202-76187c9ab7d3

📥 Commits

Reviewing files that changed from the base of the PR and between 0b16002 and 12f1118.

📒 Files selected for processing (14)
  • app.py
  • config.py
  • config/apps.yaml
  • middleware/timeout.py
  • routes/auth/device.py
  • routes/health_routes.py
  • routes/legacy/url_shortener.py
  • schemas/dto/responses/common.py
  • services/auth/device.py
  • tests/integration/test_device_auth.py
  • tests/integration/test_health.py
  • tests/smoke/test_startup.py
  • tests/unit/schemas/dto/test_common.py
  • tests/unit/services/test_redirect_uri.py

Comment thread config.py
Comment on lines +26 to +29
# socket_timeout sized for /metric aggregation; hot-path cap lives in RequestTimeoutMiddleware.
server_selection_timeout_ms: int = 3000
connect_timeout_ms: int = 3000
socket_timeout_ms: int = 120000

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate the new Mongo timeout settings at startup.

These env-backed fields currently accept 0 and negative values, so a bad deployment config can disable or break the new DB-side timeout behavior at runtime. Please add the same positive-value validation you already apply to the float timeouts.

🛡️ Suggested validation
 class DatabaseSettings(BaseSettings):
     model_config = SettingsConfigDict(env_file=".env", extra="ignore")
@@
     server_selection_timeout_ms: int = 3000
     connect_timeout_ms: int = 3000
     socket_timeout_ms: int = 120000
+
+    `@field_validator`(
+        "server_selection_timeout_ms", "connect_timeout_ms", "socket_timeout_ms"
+    )
+    `@classmethod`
+    def _must_be_positive_timeout_ms(cls, v: int, info) -> int:
+        if v <= 0:
+            raise ValueError(f"{info.field_name} must be > 0, got {v}")
+        return v
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# socket_timeout sized for /metric aggregation; hot-path cap lives in RequestTimeoutMiddleware.
server_selection_timeout_ms: int = 3000
connect_timeout_ms: int = 3000
socket_timeout_ms: int = 120000
# socket_timeout sized for /metric aggregation; hot-path cap lives in RequestTimeoutMiddleware.
server_selection_timeout_ms: int = 3000
connect_timeout_ms: int = 3000
socket_timeout_ms: int = 120000
`@field_validator`(
"server_selection_timeout_ms", "connect_timeout_ms", "socket_timeout_ms"
)
`@classmethod`
def _must_be_positive_timeout_ms(cls, v: int, info) -> int:
if v <= 0:
raise ValueError(f"{info.field_name} must be > 0, got {v}")
return v
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config.py` around lines 26 - 29, Add startup validation to ensure the new
Mongo timeout config ints (server_selection_timeout_ms, connect_timeout_ms,
socket_timeout_ms) are positive values just like the existing float timeouts:
check each value > 0 during config initialization and raise a clear ValueError
(or log+exit) if any are zero or negative. Locate the validation block used for
the float timeout fields and replicate the same positive-value check for these
three integer fields in the same config initialization path (the config class or
function that reads env vars) so bad deployments fail fast.

Comment thread services/auth/device.py
Comment on lines +79 to +82
for allowed in app.redirect_uris:
if allowed.endswith("*"):
if redirect_uri.startswith(allowed[:-1]):
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject bare * redirect allowlist entries.

This branch also accepts allowed == "*", and redirect_uri.startswith("") then approves every callback URI. In this flow that turns one bad app entry into an open redirect for the device auth code. Require a non-empty prefix before treating * as a wildcard.

🔒 Suggested fix
         for allowed in app.redirect_uris:
             if allowed.endswith("*"):
-                if redirect_uri.startswith(allowed[:-1]):
+                prefix = allowed[:-1]
+                if prefix and redirect_uri.startswith(prefix):
                     return True
             elif redirect_uri == allowed:
                 return True
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for allowed in app.redirect_uris:
if allowed.endswith("*"):
if redirect_uri.startswith(allowed[:-1]):
return True
for allowed in app.redirect_uris:
if allowed.endswith("*"):
prefix = allowed[:-1]
if prefix and redirect_uri.startswith(prefix):
return True
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/auth/device.py` around lines 79 - 82, The wildcard branch currently
treats allowed == "*" as a valid prefix match, allowing any redirect_uri; update
the logic that iterates over app.redirect_uris so that wildcard entries require
a non-empty prefix before treating the trailing "*" as a wildcard (i.e., only
accept when allowed.endswith("*") and the prefix part allowed[:-1] is
non-empty), then perform redirect_uri.startswith(prefix) using that prefix; use
the existing loop variables (app.redirect_uris, allowed, redirect_uri) to locate
and change the check.

Comment on lines +110 to +116
side_effect=lambda uri, app: (
not uri
or any(
uri.startswith(a[:-1]) if a.endswith("*") else uri == a
for a in app.redirect_uris
)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Redirect URI mock incorrectly allows empty URI when allowlist is empty

Line 111 short-circuits not uri to True, so apps with redirect_uris=[] are treated as valid for empty redirect URIs. That conflicts with the new unit contract (tests/unit/services/test_redirect_uri.py, Line 62-Line 65) and can hide redirect-validation regressions in integration tests.

Suggested fix
     svc.validate_redirect_uri = MagicMock(
         side_effect=lambda uri, app: (
-            not uri
-            or any(
-                uri.startswith(a[:-1]) if a.endswith("*") else uri == a
-                for a in app.redirect_uris
-            )
+            bool(app.redirect_uris)
+            and (
+                not uri
+                or any(
+                    uri.startswith(a[:-1]) if a.endswith("*") else uri == a
+                    for a in app.redirect_uris
+                )
+            )
         )
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
side_effect=lambda uri, app: (
not uri
or any(
uri.startswith(a[:-1]) if a.endswith("*") else uri == a
for a in app.redirect_uris
)
)
side_effect=lambda uri, app: (
bool(app.redirect_uris)
and (
not uri
or any(
uri.startswith(a[:-1]) if a.endswith("*") else uri == a
for a in app.redirect_uris
)
)
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/test_device_auth.py` around lines 110 - 116, The mock
side_effect currently short-circuits with "not uri" which allows empty URIs when
app.redirect_uris is empty; update the side_effect lambda so it first requires a
truthy uri and then checks any(match) against app.redirect_uris (i.e., replace
"not uri or any(...)" with "bool(uri) and any(...)" or equivalent) so an empty
allowlist (app.redirect_uris==[]) will not validate an empty URI; locate the
side_effect lambda and ensure it uses app.redirect_uris and wildcard handling
exactly as in the existing generator logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Security Issues related to Security

Projects

Status: 🏗️ In Progress

Development

Successfully merging this pull request may close these issues.

2 participants