Skip to content

Conversation

@touale
Copy link
Owner

@touale touale commented Dec 23, 2025

Summary by CodeRabbit

  • New Features

    • API documentation endpoints now require HTTP Basic authentication for security
    • Auto-generated secure passwords for documentation access when not explicitly configured
    • Improved error responses with timestamps
  • Bug Fixes

    • Enhanced exception handling with proper header propagation
    • Better error logging and response standardization
  • Chores

    • Updated Ray dependency to latest version

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

@coderabbitai
Copy link

coderabbitai bot commented Dec 23, 2025

Walkthrough

The PR introduces HTTP Basic authentication for API documentation endpoints. It adds configuration fields for docs credentials with UUID-based password generation, URL constants for documentation routes, authenticated endpoints serving Swagger UI and Redoc, improved error handling with timestamping, and comprehensive tests for application behavior.

Changes

Cohort / File(s) Summary
Dependency upgrade
pyproject.toml
Ray dependency bumped from 2.47.1 to 2.53.0
Configuration and constants
src/framex/config.py, src/framex/consts.py
Adds docs_user and docs_password fields to ServerConfig with post-validation UUID-based password generation; defines OPENAPI_URL, DOCS_URL, REDOC_URL constants
Authentication and docs routing
src/framex/driver/application.py
Implements HTTP Basic authentication for docs endpoints, disables default FastAPI docs/redoc, registers authenticated routes for Swagger UI and Redoc serving OpenAPI schema, improves error handling with timezone-aware timestamping and header propagation, updates response middleware to skip docs paths from logging
Comprehensive test suite
tests/driver/test_application.py
New test module covering FastAPI default configuration, auth requirements for docs/OpenAPI endpoints, exception handling (HTTPException, general errors, unicode), response wrapping middleware behavior with timestamp fields, and lifespan behavior with/without Ray

Sequence Diagram

sequenceDiagram
    participant Client
    participant FastAPI as FastAPI Application
    participant AuthValidator as HTTPBasic Validator
    participant Settings as ServerConfig
    participant Logger as framex.log

    Client->>FastAPI: GET /docs
    FastAPI->>AuthValidator: Check credentials
    AuthValidator->>Settings: Retrieve docs_user<br/>& docs_password
    
    rect rgb(200, 220, 255)
    Note over AuthValidator: Credentials verification<br/>using secrets.compare_digest
    alt Valid credentials
        AuthValidator->>FastAPI: Auth success
        FastAPI->>Client: 200 Swagger UI (HTML)
    else Invalid/Missing credentials
        AuthValidator->>Logger: Log failed attempt
        Logger->>FastAPI: ✗
        FastAPI->>Client: 401 Unauthorized
    end
    end

    Client->>FastAPI: GET /api/v1/openapi.json
    FastAPI->>AuthValidator: Check credentials
    alt Valid credentials
        AuthValidator->>FastAPI: Auth success
        FastAPI->>Client: 200 OpenAPI schema (JSON)
    else Invalid/Missing credentials
        FastAPI->>Client: 401 Unauthorized
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Hop, hop! The docs now stand so tall,
Guarded by Basic auth through the hall,
UUIDs generated with cryptic grace,
Error handlers catch every trace,
Tests bound for glory, full of might,
CodeRabbit's secured your API right! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.94% 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 'Feat/support docs auth' clearly summarizes the main change: adding documentation authentication support to the application via Basic Auth.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/support-docs-auth

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 Dec 23, 2025

Codecov Report

❌ Patch coverage is 96.55172% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/framex/driver/application.py 95.45% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai
Copy link

coderabbitai bot commented Dec 23, 2025

Note

Unit test generation is an Early Access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

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

🧹 Nitpick comments (2)
src/framex/consts.py (1)

4-6: Consider using API_STR to construct OPENAPI_URL for consistency.

The OPENAPI_URL hardcodes /api/v1 instead of referencing the existing API_STR constant. This could lead to inconsistency if API_STR is updated in the future.

🔎 Proposed fix
 API_STR = "/api/v1"
-OPENAPI_URL = "/api/v1/openapi.json"
+OPENAPI_URL = f"{API_STR}/openapi.json"
 DOCS_URL = "/docs"
 REDOC_URL = "/redoc"
src/framex/driver/application.py (1)

133-139: Consider adding REDOC_URL to excluded log paths for consistency.

DOCS_URL and OPENAPI_URL are excluded from response logging, but REDOC_URL is not. All documentation endpoints should likely receive the same treatment.

🔎 Proposed fix
         if (
             not request.url.path.startswith(API_STR)
-            or request.url.path in [DOCS_URL, OPENAPI_URL, *settings.server.excluded_log_paths]
+            or request.url.path in [DOCS_URL, OPENAPI_URL, REDOC_URL, *settings.server.excluded_log_paths]
             or b"text/event-stream; charset=utf-8" in response.raw_headers[0]
             or response.headers.get("X-Raw-Output", "False") == "True"
         ):
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d68cc6b and b4e2bd0.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • pyproject.toml
  • src/framex/config.py
  • src/framex/consts.py
  • src/framex/driver/application.py
🧰 Additional context used
🪛 Ruff (0.14.10)
src/framex/driver/application.py

76-76: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

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

91-105: LGTM!

The protected documentation endpoints are correctly implemented. Disabling FastAPI's default docs routes and replacing them with authenticated versions is a sound approach. Using include_in_schema=False appropriately keeps these meta-endpoints hidden from the generated API spec.


107-118: LGTM!

Propagating the exception's headers to the JSON response is essential for HTTP Basic auth to work correctly—the WWW-Authenticate header triggers the browser's login prompt on 401 responses.

pyproject.toml (1)

17-17: No documented breaking changes for ray[serve] 2.53.0. Ray 2.53.0 release notes do not call out Serve breaking changes; documented updates focus on feature additions (in-place updates, async inference, autoscaling). The codebase uses standard Ray Serve APIs (DeploymentHandle, ray.init(), app.state.deployments_dict) which are unchanged. Pinned version is appropriate for reproducibility.

Comment on lines +55 to +63
@model_validator(mode="after")
def validate_model(self) -> Self:
if self.docs_password == "":
key = str(uuid4())
self.docs_password = key
from framex.log import logger

logger.warning(f"No docs_password set, generate a random key: {key}")
return self
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Security risk: Logging generated password exposes credentials.

Logging the auto-generated docs_password at warning level will persist sensitive credentials in log files, potentially exposing them to unauthorized access. Credentials should never be logged.

🔎 Proposed fix
     @model_validator(mode="after")
     def validate_model(self) -> Self:
         if self.docs_password == "":
             key = str(uuid4())
             self.docs_password = key
             from framex.log import logger
 
-            logger.warning(f"No docs_password set, generate a random key: {key}")
+            logger.warning("No docs_password set, a random password has been generated. Set 'docs_password' in configuration for persistent access.")
         return self
📝 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
@model_validator(mode="after")
def validate_model(self) -> Self:
if self.docs_password == "":
key = str(uuid4())
self.docs_password = key
from framex.log import logger
logger.warning(f"No docs_password set, generate a random key: {key}")
return self
@model_validator(mode="after")
def validate_model(self) -> Self:
if self.docs_password == "":
key = str(uuid4())
self.docs_password = key
from framex.log import logger
logger.warning("No docs_password set, a random password has been generated. Set 'docs_password' in configuration for persistent access.")
return self
🤖 Prompt for AI Agents
In src/framex/config.py around lines 55 to 63, the validator currently logs the
auto-generated docs_password (exposing credentials). Remove the sensitive value
from logs: do not include the generated key in the log message; instead log a
generic warning like "No docs_password set, generated a random key (not logged)"
or omit the log entirely. Ensure the import of logger remains if you keep a
generic message, and avoid printing or persisting the actual key anywhere in
logs or stdout; if the key must be surfaced to the operator, provide a secure
retrieval mechanism (e.g., write to a protected config store) rather than
logging it.

Comment on lines 76 to 89
def authenticate(credentials: HTTPBasicCredentials = Depends(security)) -> str:
correct_username = secrets.compare_digest(credentials.username, settings.server.docs_user)
correct_password = secrets.compare_digest(credentials.password, settings.server.docs_password)
from framex.log import logger

logger.warning(f"authenticating {credentials.username} {credentials.password}")

if not (correct_username and correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect user or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical security issue: Password logged in plaintext on every authentication attempt.

Line 81 logs both username and password on every authentication attempt. This exposes credentials in logs and is a serious security vulnerability.

🔎 Proposed fix - remove password logging entirely
     def authenticate(credentials: HTTPBasicCredentials = Depends(security)) -> str:
         correct_username = secrets.compare_digest(credentials.username, settings.server.docs_user)
         correct_password = secrets.compare_digest(credentials.password, settings.server.docs_password)
-        from framex.log import logger
-
-        logger.warning(f"authenticating {credentials.username} {credentials.password}")
 
         if not (correct_username and correct_password):
+            from framex.log import logger
+            logger.warning(f"Failed authentication attempt for user: {credentials.username}")
             raise HTTPException(
                 status_code=status.HTTP_401_UNAUTHORIZED,
                 detail="Incorrect user or password",
                 headers={"WWW-Authenticate": "Basic"},
             )
         return credentials.username
📝 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
def authenticate(credentials: HTTPBasicCredentials = Depends(security)) -> str:
correct_username = secrets.compare_digest(credentials.username, settings.server.docs_user)
correct_password = secrets.compare_digest(credentials.password, settings.server.docs_password)
from framex.log import logger
logger.warning(f"authenticating {credentials.username} {credentials.password}")
if not (correct_username and correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect user or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
def authenticate(credentials: HTTPBasicCredentials = Depends(security)) -> str:
correct_username = secrets.compare_digest(credentials.username, settings.server.docs_user)
correct_password = secrets.compare_digest(credentials.password, settings.server.docs_password)
if not (correct_username and correct_password):
from framex.log import logger
logger.warning(f"Failed authentication attempt for user: {credentials.username}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect user or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
🧰 Tools
🪛 Ruff (0.14.10)

76-76: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🤖 Prompt for AI Agents
In src/framex/driver/application.py around lines 76 to 89, the logger.warning
call currently emits both username and plaintext password; remove the password
from logs — replace the log with one that only records the username (or a masked
value) and use an appropriate level (e.g., logger.info or logger.debug) so
credentials are never written in plaintext; also scan nearby code to ensure no
other logs output credentials.

@coderabbitai
Copy link

coderabbitai bot commented Dec 23, 2025

Here are the copyable unit test edits:

Copyable Edits

ruff.toml

@@ -1,93 +1,30 @@
-target-version = "py311"
-line-length = 119
-respect-gitignore = true
-exclude = [
-  ".bzr",
-  ".direnv",
-  ".eggs",
-  ".git",
-  ".git-rewrite",
-  ".hg",
-  ".ipynb_checkpoints",
-  ".mypy_cache",
-  ".nox",
-  ".pants.d",
-  ".pyenv",
-  ".pytest_cache",
-  ".pytype",
-  ".ruff_cache",
-  ".svn",
-  ".tox",
-  ".venv",
-  ".vscode",
-  "__pypackages__",
-  "_build",
-  "buck-out",
-  "build",
-  "dist",
-  "node_modules",
-  "site-packages",
-  ".venv",
-  ".history",
-]
-
 [lint]
-extend-select = [
-  "A",     # bad name
-  "ANN",   # flake8-annotations
-  "ARG",   # bad arguments
-  "ASYNC", # async
-  "B",     # flake8-bugbear
-  "C",     # Complexity
-  "C4",    #flake8-comprehensions
-  "E",     # PEP8 errors
-  "ERA",   # commented-out-code
-  "F",     # PEP8 formatting
-  "FLY",   # static-join-to-f-string
-  "FAST",  # FastAPI (FAST)
-  "FLY",   # static-join-to-f-string
-  "FURB",  # refurb
-  "I",     # Import sorting
-  "INP",   # flake8-no-pep420
-  "N",     # name-conventions
-  "PERF",  # Perflint
-  "PIE",   # Improper comments
-  "PTH",   # use-pathlib
-  "PT",    # pytest
-  "RET",   # unnecessary-return
-  "RUF",   # specific rules
-  "S",     # flake8-bandit
-  "SIM",   # simplify
-  "T10",   # debug found
-  "T201",  # print found
-  "TID",   # tidy-imports
-  "UP",    # Pyupgrade upgrades
-  "W",     # PEP8 warnings
-]
+select = [
+		# Pyflakes - catches logical errors and unused imports
+		"F",
+		# flake8-bugbear - catches common bugs and design issues
+		"B",
+		# flake8-bandit - security vulnerabilities
+		"S",
+		# flake8-blind-except - catches bare except clauses
+		"BLE",
+		# flake8-debugger - catches debugger statements
+		"T10",
+		# flake8-executable - catches executable files
+		"EXE",
+		# flake8-unused-arguments - catches unused function arguments
+		"ARG",
+		# tryceratops - catches try/except anti-patterns
+		"TRY",
+		# Ruff-specific rules for bugs and errors
+		"RUF",
+		# pycodestyle subset rules - only error-level issues
+		"E7",  # Syntax errors
+		"E9",  # Runtime errors
 
-ignore = [
-  "B008",   # Do not perform function calls in argument defaults
-  "B009",   # get-attr-with-constant
-  "B010",   # set-attr-with-constant
-  "ERA001",
-  "C901",
-  "E402",   # Module level import not at top of file
-  "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in `**kwargs`
-  "ANN204", # ANN204 Missing return type annotation for special method
-  # "ANN201", # Missing return type annotation for public function
-  "E501", # Line too long
 ]
-
+# suppress common non critical issues
+ignore = ["I001", "F401", "W291", "W293", "Q000", "ANN001", "ANN201", "UP006", "UP045", "E501", "S101", "EXE001"]
 [lint.per-file-ignores]
-"tests/**/*.py" = ["S101", "ANN201", "ANN001", "ANN002"]
-
-[lint.flake8-builtins]
-builtins-ignorelist = ["input", "id", "bytes", "type"]
-
-[lint.isort]
-force-wrap-aliases = true
-combine-as-imports = true
-split-on-trailing-comma = true
-
-[lint.pydocstyle]
-convention = "numpy"
+"**/__init__.py" = ["E402"]
+"**/conftest.py" = ["E402"]

tests/driver/init.py

This is a new file.

"""Tests for framex.driver module."""

tests/driver/test_application.py

This is a new file.

"""Comprehensive tests for framex.driver.application module."""

import json
import secrets
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from starlette import status
from starlette.exceptions import HTTPException

from framex.consts import API_STR, DOCS_URL, OPENAPI_URL, REDOC_URL
from framex.driver.application import create_fastapi_application


class TestCreateFastAPIApplication:
    """Test suite for create_fastapi_application function."""

    def test_creates_fastapi_instance(self):
        """Test that create_fastapi_application returns a FastAPI instance."""
        app = create_fastapi_application()
        
        assert isinstance(app, FastAPI)
        assert app.title == "FrameX"
        assert app.version == "0.2.4"

    def test_openapi_url_is_none_by_default(self):
        """Test that openapi_url is set to None (protected by auth)."""
        app = create_fastapi_application()
        
        assert app.openapi_url is None

    def test_docs_url_is_none_by_default(self):
        """Test that docs_url is set to None (protected by auth)."""
        app = create_fastapi_application()
        
        assert app.docs_url is None

    def test_redirect_slashes_disabled(self):
        """Test that redirect_slashes is disabled."""
        app = create_fastapi_application()
        
        assert app.redirect_slashes is False

    def test_cors_middleware_configured(self):
        """Test that CORS middleware is properly configured."""
        app = create_fastapi_application()
        
        # Check that middleware is present
        middleware_classes = [m.cls.__name__ for m in app.user_middleware]
        assert "CORSMiddleware" in middleware_classes


class TestAuthenticationEndpoints:
    """Test suite for documentation authentication endpoints."""

    @pytest.fixture
    def app(self):
        """Create a test FastAPI application."""
        return create_fastapi_application()

    @pytest.fixture
    def client(self, app):
        """Create a test client."""
        return TestClient(app)

    def test_docs_endpoint_requires_authentication(self, client):
        """Test that /docs endpoint requires authentication."""
        response = client.get(DOCS_URL)
        
        assert response.status_code == status.HTTP_401_UNAUTHORIZED

    def test_docs_endpoint_with_wrong_credentials(self, client):
        """Test that /docs endpoint rejects wrong credentials."""
        response = client.get(
            DOCS_URL,
            auth=("wrong_user", "wrong_password")
        )
        
        assert response.status_code == status.HTTP_401_UNAUTHORIZED
        assert response.json()["message"] == "Incorrect user or password"

    def test_docs_endpoint_with_correct_credentials(self, client):
        """Test that /docs endpoint accepts correct credentials."""
        from framex.config import settings
        
        response = client.get(
            DOCS_URL,
            auth=(settings.server.docs_user, settings.server.docs_password)
        )
        
        assert response.status_code == status.HTTP_200_OK
        assert "text/html" in response.headers["content-type"]

    def test_redoc_endpoint_requires_authentication(self, client):
        """Test that /redoc endpoint requires authentication."""
        response = client.get(REDOC_URL)
        
        assert response.status_code == status.HTTP_401_UNAUTHORIZED

    def test_redoc_endpoint_with_wrong_credentials(self, client):
        """Test that /redoc endpoint rejects wrong credentials."""
        response = client.get(
            REDOC_URL,
            auth=("wrong_user", "wrong_password")
        )
        
        assert response.status_code == status.HTTP_401_UNAUTHORIZED

    def test_redoc_endpoint_with_correct_credentials(self, client):
        """Test that /redoc endpoint accepts correct credentials."""
        from framex.config import settings
        
        response = client.get(
            REDOC_URL,
            auth=(settings.server.docs_user, settings.server.docs_password)
        )
        
        assert response.status_code == status.HTTP_200_OK
        assert "text/html" in response.headers["content-type"]

    def test_openapi_endpoint_requires_authentication(self, client):
        """Test that /api/v1/openapi.json endpoint requires authentication."""
        response = client.get(OPENAPI_URL)
        
        assert response.status_code == status.HTTP_401_UNAUTHORIZED

    def test_openapi_endpoint_with_wrong_credentials(self, client):
        """Test that /api/v1/openapi.json endpoint rejects wrong credentials."""
        response = client.get(
            OPENAPI_URL,
            auth=("wrong_user", "wrong_password")
        )
        
        assert response.status_code == status.HTTP_401_UNAUTHORIZED

    def test_openapi_endpoint_with_correct_credentials(self, client):
        """Test that /api/v1/openapi.json endpoint accepts correct credentials."""
        from framex.config import settings
        
        response = client.get(
            OPENAPI_URL,
            auth=(settings.server.docs_user, settings.server.docs_password)
        )
        
        assert response.status_code == status.HTTP_200_OK
        openapi_json = response.json()
        assert openapi_json["info"]["title"] == "FrameX API"
        assert openapi_json["info"]["version"] == "0.2.4"

    def test_authentication_uses_constant_time_comparison(self, app):
        """Test that authentication uses secrets.compare_digest for timing attack protection."""
        # This test verifies the implementation uses secrets.compare_digest
        # by checking the function is called (indirectly through behavior)
        with TestClient(app) as client:
            from framex.config import settings
            
            # Try with correct credentials
            response = client.get(
                DOCS_URL,
                auth=(settings.server.docs_user, settings.server.docs_password)
            )
            assert response.status_code == status.HTTP_200_OK
            
            # Try with credentials that differ only in one character
            wrong_password = settings.server.docs_password[:-1] + "x"
            response = client.get(
                DOCS_URL,
                auth=(settings.server.docs_user, wrong_password)
            )
            assert response.status_code == status.HTTP_401_UNAUTHORIZED

    def test_authentication_response_includes_www_authenticate_header(self, client):
        """Test that 401 responses include WWW-Authenticate header."""
        response = client.get(DOCS_URL)
        
        assert response.status_code == status.HTTP_401_UNAUTHORIZED
        assert "WWW-Authenticate" in response.headers
        assert "Basic" in response.headers["WWW-Authenticate"]

    def test_docs_endpoint_not_in_openapi_schema(self, client):
        """Test that documentation endpoints are not included in the OpenAPI schema."""
        from framex.config import settings
        
        response = client.get(
            OPENAPI_URL,
            auth=(settings.server.docs_user, settings.server.docs_password)
        )
        
        openapi_json = response.json()
        paths = openapi_json.get("paths", {})
        
        # Documentation endpoints should not be in the schema
        assert DOCS_URL not in paths
        assert REDOC_URL not in paths
        assert OPENAPI_URL not in paths


class TestExceptionHandlers:
    """Test suite for exception handlers."""

    @pytest.fixture
    def app(self):
        """Create a test FastAPI application."""
        return create_fastapi_application()

    @pytest.fixture
    def client(self, app):
        """Create a test client."""
        return TestClient(app)

    def test_http_exception_handler_preserves_status_code(self, app, client):
        """Test that HTTPException handler preserves the status code."""
        @app.get("/test-http-exception")
        async def test_endpoint():
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail="Resource not found"
            )
        
        response = client.get("/test-http-exception")
        
        assert response.status_code == status.HTTP_404_NOT_FOUND
        data = response.json()
        assert data["status"] == 404
        assert data["message"] == "Resource not found"
        assert data["is_middleware_error"] is True

    def test_http_exception_handler_preserves_headers(self, app, client):
        """Test that HTTPException handler preserves custom headers."""
        @app.get("/test-http-exception-headers")
        async def test_endpoint():
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Unauthorized",
                headers={"WWW-Authenticate": "Bearer"}
            )
        
        response = client.get("/test-http-exception-headers")
        
        assert response.status_code == status.HTTP_401_UNAUTHORIZED
        assert "WWW-Authenticate" in response.headers
        assert response.headers["WWW-Authenticate"] == "Bearer"

    def test_general_exception_handler_returns_500(self, app, client):
        """Test that general Exception handler returns 500 status."""
        @app.get("/test-general-exception")
        async def test_endpoint():
            raise ValueError("Something went wrong")
        
        response = client.get("/test-general-exception")
        
        assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
        data = response.json()
        assert data["status"] == 500
        assert "Something went wrong" in data["message"]
        assert "timestamp" in data

    def test_general_exception_handler_includes_timestamp(self, app, client):
        """Test that general exception handler includes timestamp."""
        @app.get("/test-exception-timestamp")
        async def test_endpoint():
            raise RuntimeError("Test error")
        
        response = client.get("/test-exception-timestamp")
        data = response.json()
        
        assert "timestamp" in data
        # Verify timestamp format (YYYY-MM-DD HH:MM:SS)
        timestamp = data["timestamp"]
        assert len(timestamp) == 19
        assert timestamp[4] == "-"
        assert timestamp[7] == "-"
        assert timestamp[10] == " "
        assert timestamp[13] == ":"
        assert timestamp[16] == ":"


class TestLogResponseMiddleware:
    """Test suite for log_response middleware."""

    @pytest.fixture
    def app(self):
        """Create a test FastAPI application."""
        return create_fastapi_application()

    @pytest.fixture
    def client(self, app):
        """Create a test client."""
        return TestClient(app)

    def test_middleware_wraps_api_responses(self, app, client):
        """Test that middleware wraps API responses with standard format."""
        @app.get(f"{API_STR}/test-wrap")
        async def test_endpoint():
            return {"result": "success"}
        
        response = client.get(f"{API_STR}/test-wrap")
        data = response.json()
        
        assert response.status_code == status.HTTP_200_OK
        assert data["status"] == 200
        assert data["message"] == "success"
        assert "timestamp" in data
        assert data["data"] == {"result": "success"}

    def test_middleware_skips_excluded_paths(self, app, client):
        """Test that middleware skips excluded log paths."""
        from framex.config import settings
        
        # Add test path to excluded paths
        original_excluded = settings.server.excluded_log_paths.copy()
        settings.server.excluded_log_paths.append(f"{API_STR}/test-excluded")
        
        try:
            @app.get(f"{API_STR}/test-excluded")
            async def test_endpoint():
                return {"raw": "response"}
            
            response = client.get(f"{API_STR}/test-excluded")
            data = response.json()
            
            # Should return raw response without wrapping
            assert data == {"raw": "response"}
        finally:
            settings.server.excluded_log_paths = original_excluded

    def test_middleware_skips_docs_endpoints(self, client):
        """Test that middleware skips documentation endpoints."""
        from framex.config import settings
        
        # Docs endpoint should not be wrapped
        response = client.get(
            DOCS_URL,
            auth=(settings.server.docs_user, settings.server.docs_password)
        )
        
        # Should return HTML, not JSON wrapped response
        assert response.status_code == status.HTTP_200_OK
        assert "text/html" in response.headers["content-type"]

    def test_middleware_skips_openapi_endpoint(self, client):
        """Test that middleware skips OpenAPI endpoint."""
        from framex.config import settings
        
        response = client.get(
            OPENAPI_URL,
            auth=(settings.server.docs_user, settings.server.docs_password)
        )
        
        # Should return raw OpenAPI JSON, not wrapped
        data = response.json()
        assert "info" in data
        assert "paths" in data

    def test_middleware_handles_middleware_errors(self, app, client):
        """Test that middleware properly handles errors marked as middleware errors."""
        @app.get(f"{API_STR}/test-middleware-error")
        async def test_endpoint():
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Bad request from middleware"
            )
        
        response = client.get(f"{API_STR}/test-middleware-error")
        data = response.json()
        
        # Middleware should transform the response
        assert response.status_code == status.HTTP_200_OK
        assert data["status"] == 400
        assert data["message"] == "Bad request from middleware"
        assert "timestamp" in data


class TestLifespanBehavior:
    """Test suite for application lifespan behavior."""

    @patch("framex.config.settings")
    def test_lifespan_without_ray(self, mock_settings):
        """Test lifespan behavior when not using Ray."""
        mock_settings.server.use_ray = False
        
        app = create_fastapi_application()
        
        # Lifespan should be set
        assert app.router.lifespan_context is not None

    @patch("framex.config.settings")
    def test_lifespan_with_ray(self, mock_settings):
        """Test lifespan behavior when using Ray."""
        mock_settings.server.use_ray = True
        
        app = create_fastapi_application()
        
        # Lifespan should be set
        assert app.router.lifespan_context is not None


class TestSecurityConfiguration:
    """Test suite for security-related configuration."""

    def test_http_basic_security_configured(self):
        """Test that HTTP Basic security is properly configured."""
        app = create_fastapi_application()
        
        # Check that the security scheme is used in authentication
        with TestClient(app) as client:
            response = client.get(DOCS_URL)
            assert response.status_code == status.HTTP_401_UNAUTHORIZED
            assert "WWW-Authenticate" in response.headers

    def test_cors_allows_all_methods(self):
        """Test that CORS is configured to allow all methods."""
        app = create_fastapi_application()
        
        # Check middleware configuration
        cors_middleware = None
        for middleware in app.user_middleware:
            if middleware.cls.__name__ == "CORSMiddleware":
                cors_middleware = middleware
                break
        
        assert cors_middleware is not None
        assert cors_middleware.options.get("allow_methods") == ["*"]

    def test_cors_allows_all_headers(self):
        """Test that CORS is configured to allow all headers."""
        app = create_fastapi_application()
        
        cors_middleware = None
        for middleware in app.user_middleware:
            if middleware.cls.__name__ == "CORSMiddleware":
                cors_middleware = middleware
                break
        
        assert cors_middleware is not None
        assert cors_middleware.options.get("allow_headers") == ["*"]

    def test_cors_allows_credentials(self):
        """Test that CORS is configured to allow credentials."""
        app = create_fastapi_application()
        
        cors_middleware = None
        for middleware in app.user_middleware:
            if middleware.cls.__name__ == "CORSMiddleware":
                cors_middleware = middleware
                break
        
        assert cors_middleware is not None
        assert cors_middleware.options.get("allow_credentials") is True


class TestEdgeCases:
    """Test suite for edge cases and boundary conditions."""

    @pytest.fixture
    def app(self):
        """Create a test FastAPI application."""
        return create_fastapi_application()

    @pytest.fixture
    def client(self, app):
        """Create a test client."""
        return TestClient(app)

    def test_empty_username_authentication(self, client):
        """Test authentication with empty username."""
        response = client.get(DOCS_URL, auth=("", "password"))
        
        assert response.status_code == status.HTTP_401_UNAUTHORIZED

    def test_empty_password_authentication(self, client):
        """Test authentication with empty password."""
        response = client.get(DOCS_URL, auth=("admin", ""))
        
        assert response.status_code == status.HTTP_401_UNAUTHORIZED

    def test_special_characters_in_credentials(self, client):
        """Test authentication with special characters in credentials."""
        from framex.config import settings
        
        # This should work if the actual password contains special chars
        response = client.get(
            DOCS_URL,
            auth=(settings.server.docs_user, settings.server.docs_password)
        )
        
        assert response.status_code == status.HTTP_200_OK

    def test_case_sensitivity_in_credentials(self, app, client):
        """Test that credentials are case-sensitive."""
        from framex.config import settings
        
        # Try with uppercase username
        response = client.get(
            DOCS_URL,
            auth=(settings.server.docs_user.upper(), settings.server.docs_password)
        )
        
        # Should fail if username is case-sensitive
        assert response.status_code == status.HTTP_401_UNAUTHORIZED

    def test_unicode_in_exception_message(self, app, client):
        """Test that exception handler handles Unicode properly."""
        @app.get("/test-unicode-exception")
        async def test_endpoint():
            raise ValueError("Error with Unicode: 你好世界 🌍")
        
        response = client.get("/test-unicode-exception")
        data = response.json()
        
        assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
        assert "你好世界" in data["message"]
        assert "🌍" in data["message"]

    def test_very_long_exception_message(self, app, client):
        """Test that exception handler handles very long messages."""
        long_message = "x" * 10000
        
        @app.get("/test-long-exception")
        async def test_endpoint():
            raise ValueError(long_message)
        
        response = client.get("/test-long-exception")
        data = response.json()
        
        assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
        assert long_message in data["message"]

    def test_null_byte_in_path(self, client):
        """Test handling of null byte in path."""
        # FastAPI/Starlette should handle this safely
        response = client.get(f"{API_STR}/test\x00path")
        
        # Should return 404 or handle gracefully
        assert response.status_code in [
            status.HTTP_404_NOT_FOUND,
            status.HTTP_400_BAD_REQUEST
        ]

tests/test_config.py

@@ -1,42 +0,0 @@
-import pytest
-from pydantic_core import ValidationError
-
-from framex.config import AuthConfig
-
-
-def test_config():
-    from framex.plugin import get_plugin_config
-    from framex.plugins.proxy.config import ProxyPluginConfig
-
-    cfg = get_plugin_config("proxy", ProxyPluginConfig)
-    assert isinstance(cfg, ProxyPluginConfig)
-    assert cfg.proxy_urls is not None
-
-
-def test_auth_config():
-    AuthConfig(
-        general_auth_keys=["abcdefg"],
-        auth_urls=[
-            "/api/v1/a/*",
-            "/api/b/call",
-            "/api/v1/c/*",
-        ],
-        special_auth_keys={"/api/v1/a/call": ["0123456789"], "/api/v1/c/*": ["0123456789a", "0123456789b"]},
-    )
-
-    AuthConfig(
-        general_auth_keys=["abcdefg"],
-        auth_urls=[
-            "/api/v1/a/*",
-        ],
-        special_auth_keys={"/api/v1/a/call": ["0123456789"]},
-    )
-
-    with pytest.raises(ValidationError) as exc_info:
-        AuthConfig(
-            auth_urls=["/api/v1/*"],
-            special_auth_keys={
-                "/admin/login": ["0123456789"],
-            },
-        )
-    assert "special_auth_keys url '/admin/login'" in str(exc_info.value)

tests/test_consts.py

This is a new file.

"""Tests for framex.consts module."""

import pytest

from framex.consts import (
    API_STR,
    APP_NAME,
    BACKEND_NAME,
    DEFAULT_ENV,
    DOCS_URL,
    OPENAPI_URL,
    PROJECT_NAME,
    PROXY_FUNC_HTTP_PATH,
    PROXY_PLUGIN_NAME,
    REDOC_URL,
    VERSION,
)


class TestConstants:
    """Test suite for module constants."""

    def test_project_name(self):
        """Test that PROJECT_NAME is correctly defined."""
        assert PROJECT_NAME == "FrameX"
        assert isinstance(PROJECT_NAME, str)

    def test_version(self):
        """Test that VERSION is correctly defined."""
        assert VERSION == "0.2.4"
        assert isinstance(VERSION, str)

    def test_api_str(self):
        """Test that API_STR is correctly defined."""
        assert API_STR == "/api/v1"
        assert API_STR.startswith("/")
        assert "api" in API_STR.lower()

    def test_openapi_url(self):
        """Test that OPENAPI_URL is correctly defined."""
        assert OPENAPI_URL == "/api/v1/openapi.json"
        assert OPENAPI_URL.startswith(API_STR)
        assert OPENAPI_URL.endswith(".json")

    def test_docs_url(self):
        """Test that DOCS_URL is correctly defined."""
        assert DOCS_URL == "/docs"
        assert DOCS_URL.startswith("/")

    def test_redoc_url(self):
        """Test that REDOC_URL is correctly defined."""
        assert REDOC_URL == "/redoc"
        assert REDOC_URL.startswith("/")

    def test_backend_name(self):
        """Test that BACKEND_NAME is correctly defined."""
        assert BACKEND_NAME == "backend"
        assert isinstance(BACKEND_NAME, str)

    def test_app_name(self):
        """Test that APP_NAME is correctly defined."""
        assert APP_NAME == "default"
        assert isinstance(APP_NAME, str)

    def test_proxy_plugin_name(self):
        """Test that PROXY_PLUGIN_NAME is correctly defined."""
        assert PROXY_PLUGIN_NAME == "proxy.ProxyPlugin"
        assert isinstance(PROXY_PLUGIN_NAME, str)

    def test_proxy_func_http_path(self):
        """Test that PROXY_FUNC_HTTP_PATH is correctly defined."""
        assert PROXY_FUNC_HTTP_PATH == f"{API_STR}/proxy/remote"
        assert PROXY_FUNC_HTTP_PATH.startswith(API_STR)

    def test_default_env(self):
        """Test that DEFAULT_ENV is correctly defined."""
        assert isinstance(DEFAULT_ENV, dict)
        assert "RAY_COLOR_PREFIX" in DEFAULT_ENV
        assert "RAY_DEDUP_LOGS" in DEFAULT_ENV
        assert "RAY_SERVE_RUN_SYNC_IN_THREADPOOL" in DEFAULT_ENV


class TestConstantsRelationships:
    """Test suite for relationships between constants."""

    def test_openapi_url_uses_api_str(self):
        """Test that OPENAPI_URL is built using API_STR."""
        assert OPENAPI_URL.startswith(API_STR)

    def test_proxy_func_http_path_uses_api_str(self):
        """Test that PROXY_FUNC_HTTP_PATH is built using API_STR."""
        assert PROXY_FUNC_HTTP_PATH.startswith(API_STR)

    def test_docs_urls_are_unique(self):
        """Test that all documentation URLs are unique."""
        urls = [DOCS_URL, REDOC_URL, OPENAPI_URL]
        assert len(urls) == len(set(urls))

    def test_docs_urls_are_valid_paths(self):
        """Test that all documentation URLs are valid paths."""
        urls = [DOCS_URL, REDOC_URL, OPENAPI_URL]
        for url in urls:
            assert url.startswith("/")
            assert not url.endswith("/")  # No trailing slash


class TestConstantsTypes:
    """Test suite for constant types."""

    def test_string_constants_are_strings(self):
        """Test that string constants are actually strings."""
        string_constants = [
            PROJECT_NAME,
            VERSION,
            API_STR,
            OPENAPI_URL,
            DOCS_URL,
            REDOC_URL,
            BACKEND_NAME,
            APP_NAME,
            PROXY_PLUGIN_NAME,
            PROXY_FUNC_HTTP_PATH,
        ]
        
        for constant in string_constants:
            assert isinstance(constant, str)
            assert len(constant) > 0

    def test_default_env_is_dict(self):
        """Test that DEFAULT_ENV is a dictionary."""
        assert isinstance(DEFAULT_ENV, dict)
        assert len(DEFAULT_ENV) > 0

    def test_default_env_values_are_strings(self):
        """Test that DEFAULT_ENV values are strings."""
        for key, value in DEFAULT_ENV.items():
            assert isinstance(key, str)
            assert isinstance(value, str)


class TestConstantsImmutability:
    """Test suite to verify constants are used correctly."""

    def test_constants_not_empty(self):
        """Test that all string constants are not empty."""
        assert PROJECT_NAME != ""
        assert VERSION != ""
        assert API_STR != ""
        assert OPENAPI_URL != ""
        assert DOCS_URL != ""
        assert REDOC_URL != ""

    def test_version_format(self):
        """Test that VERSION follows semantic versioning format."""
        parts = VERSION.split(".")
        assert len(parts) == 3
        for part in parts:
            assert part.isdigit()

    def test_api_str_format(self):
        """Test that API_STR follows expected format."""
        assert API_STR.startswith("/api/")
        assert "v" in API_STR  # versioned API

    def test_proxy_plugin_name_format(self):
        """Test that PROXY_PLUGIN_NAME follows expected format."""
        assert "." in PROXY_PLUGIN_NAME
        assert PROXY_PLUGIN_NAME.endswith("Plugin")


class TestConstantsUsage:
    """Test suite for constants usage scenarios."""

    def test_can_build_full_url_with_api_str(self):
        """Test that API_STR can be used to build full URLs."""
        endpoint = "/test/endpoint"
        full_url = f"{API_STR}{endpoint}"
        
        assert full_url.startswith("/api/")
        assert full_url.endswith(endpoint)

    def test_openapi_url_structure(self):
        """Test the structure of OPENAPI_URL."""
        assert OPENAPI_URL.count("/") >= 3  # At least /api/v1/openapi.json
        assert "openapi" in OPENAPI_URL.lower()

    def test_default_env_keys_are_uppercase(self):
        """Test that DEFAULT_ENV keys follow uppercase convention."""
        for key in DEFAULT_ENV.keys():
            assert key.isupper() or "_" in key
            assert key.startswith("RAY_")

    def test_default_env_values_are_valid(self):
        """Test that DEFAULT_ENV values are valid settings."""
        assert DEFAULT_ENV["RAY_COLOR_PREFIX"] in ["0", "1"]
        assert DEFAULT_ENV["RAY_DEDUP_LOGS"] in ["0", "1"]
        assert DEFAULT_ENV["RAY_SERVE_RUN_SYNC_IN_THREADPOOL"] in ["0", "1"]

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/framex/driver/application.py (1)

165-170: CORS middleware is missing allow_origins parameter.

The CORS configuration sets allow_credentials=True but omits allow_origins. Without allow_origins, cross-origin requests will be blocked. Note that allow_origins=["*"] cannot be used with allow_credentials=True per CORS specification. If credentials are needed, specify explicit origins.

🔎 Proposed fix (if credentials needed with specific origins)
     application.add_middleware(
         CORSMiddleware,
+        allow_origins=["http://localhost:3000"],  # Specify actual allowed origins
         allow_credentials=True,
         allow_methods=["*"],
         allow_headers=["*"],
     )
🔎 Alternative fix (if credentials not needed)
     application.add_middleware(
         CORSMiddleware,
-        allow_credentials=True,
+        allow_origins=["*"],
+        allow_credentials=False,
         allow_methods=["*"],
         allow_headers=["*"],
     )
♻️ Duplicate comments (1)
src/framex/config.py (1)

55-63: Security risk: Logging generated password exposes credentials.

This issue was previously flagged. Logging the auto-generated docs_password at warning level persists sensitive credentials in log files. Remove the password from the log message.

🔎 Proposed fix
     @model_validator(mode="after")
     def validate_model(self) -> Self:
         if self.docs_password == "":
             key = str(uuid4())
             self.docs_password = key
             from framex.log import logger

-            logger.warning(f"No docs_password set, generate a random key: {key}")
+            logger.warning("No docs_password set, a random password has been generated. Set 'docs_password' in configuration for persistent access.")
         return self
🧹 Nitpick comments (3)
src/framex/consts.py (1)

4-6: Consider using API_STR to construct OPENAPI_URL.

OPENAPI_URL hardcodes /api/v1 instead of reusing API_STR, creating a maintenance burden if the API prefix changes.

🔎 Proposed fix
-OPENAPI_URL = "/api/v1/openapi.json"
+OPENAPI_URL = f"{API_STR}/openapi.json"
tests/driver/test_application.py (1)

159-170: Lifespan tests have weak assertions.

Both test_lifespan_without_ray and test_lifespan_with_ray make identical assertions (lifespan_context is not None), which doesn't validate differentiated behavior. Consider testing actual lifespan execution with a context manager or verifying that on_start callbacks are invoked when use_ray=False.

src/framex/driver/application.py (1)

120-129: Consider sanitizing exception messages in production.

The general exception handler returns str(exc) directly in the response body. This may expose internal implementation details (stack traces, file paths, SQL queries) to clients. Consider returning a generic error message and logging the actual exception server-side.

🔎 Proposed fix
     @application.exception_handler(Exception)
     async def _general_exception_handler(request, exc):  # noqa
+        from framex.log import logger
+        logger.exception(f"Unhandled exception: {exc}")
         return JSONResponse(
             status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
             content={
                 "status": 500,
-                "message": str(exc),
+                "message": "Internal server error",
                 "timestamp": pytz.timezone("Asia/Shanghai").localize(datetime.now()).strftime("%Y-%m-%d %H:%M:%S"),
             },
         )
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d68cc6b and fb2b5d5.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • pyproject.toml
  • src/framex/config.py
  • src/framex/consts.py
  • src/framex/driver/application.py
  • tests/driver/__init__.py
  • tests/driver/test_application.py
🧰 Additional context used
🧬 Code graph analysis (2)
src/framex/config.py (1)
src/framex/cli.py (1)
  • framex (11-12)
tests/driver/test_application.py (1)
src/framex/driver/application.py (1)
  • create_fastapi_application (28-171)
🪛 Ruff (0.14.10)
src/framex/driver/application.py

76-76: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

tests/driver/test_application.py

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

(TRY003)


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

(TRY003)

🔇 Additional comments (5)
tests/driver/test_application.py (3)

15-34: LGTM! Well-structured basic configuration tests.

Good coverage of default FastAPI configuration: disabled docs/openapi URLs, redirect_slashes disabled, and CORS middleware presence.


36-73: LGTM! Authentication tests properly validate the Basic auth flow.

Good coverage of authentication scenarios including missing credentials, wrong credentials, and valid credentials for both docs and OpenAPI endpoints.


75-124: LGTM! Good exception handler test coverage.

Tests properly verify HTTPException propagation, general exception handling with 500 status, and unicode message support. The static analysis warnings (TRY003) are false positives for test fixtures.

src/framex/driver/application.py (2)

74-89: Authentication implementation looks correct.

Good use of secrets.compare_digest for timing-safe credential comparison. The previous password logging issue has been addressed - now only the username is logged on failed attempts.


91-105: LGTM! Well-implemented authenticated documentation endpoints.

Clean implementation using FastAPI's built-in get_swagger_ui_html, get_redoc_html, and get_openapi utilities. Properly excluded from schema to avoid circular references.

"pydantic-settings>=2.10.1",
"pytz>=2025.2",
"ray[serve]==2.47.1",
"ray[serve]==2.53.0",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Ray 2.53.0 breaking changes from 2.47 release notes

💡 Result:

Do you want a concise list of breaking changes introduced in Ray 2.53.0 (vs. what was documented in 2.47), or the full release-note diff/links so you can review details?


🏁 Script executed:

# Find and examine application.py to understand Ray Serve usage
find . -name "application.py" -type f

Repository: touale/FrameX-kit

Length of output: 95


🏁 Script executed:

# Also check the current pyproject.toml to see the context
cat pyproject.toml | head -30

Repository: touale/FrameX-kit

Length of output: 735


🌐 Web query:

Ray 2.53.0 vs 2.47 breaking changes list

💡 Result:

Short answer — there is no single “breaking changes” document that lists only breaking changes for 2.47→2.53. Major user-impact changes / regressions reported between Ray 2.47 and the 2.53 timeframe are:

  • Default uv run integration enabled (since ~2.47), which can change runtime_env / pip behavior and broke some existing environments. Reported as a breaking/regression issue. (github.com)
  • RLlib / Tune checkpoint-metrics regression in 2.47.0 (checkpointed metrics/step counters not restored). (github.com)
  • Ray Core / security and packaging changes across 2.51–2.52 (e.g., token auth added, Python 3.9 wheel EOL) that can affect upgrades and deployments. See release highlights. (github.com)

For the authoritative, per-release notes (2.47 → 2.48 → … → 2.53) and the full list of changes/compatibility notes, review the Ray GitHub releases page and the individual GitHub issues linked from those releases: https://github.com/ray-project/ray/releases. (github.com)

If you want, I can:

  • extract and summarize all documented breaking/behavior-changing notes from each release 2.47.0 → 2.53.0, or
  • produce a migration checklist of things to test before upgrading (runtime_env/uv, Tune/RLlib checkpointing, Serve autoscaling/deploy defaults, Python wheel compatibility). Which would you prefer?

Citations:


🏁 Script executed:

# Examine application.py to see Ray Serve usage patterns
cat -n ./src/framex/driver/application.py

Repository: touale/FrameX-kit

Length of output: 7528


🏁 Script executed:

# Also check if there are any tests or documentation about Ray usage
find . -type f \( -name "*.py" -o -name "*.md" \) | xargs rg -l "ray\[serve\]|Ray|ray\.serve" | head -20

Repository: touale/FrameX-kit

Length of output: 636


🏁 Script executed:

# Check ray_adapter.py for Ray Serve deployment patterns
cat -n ./src/framex/adapter/ray_adapter.py

Repository: touale/FrameX-kit

Length of output: 2196


🏁 Script executed:

# Search for any Ray Serve configuration or deployment setup
rg -B2 -A5 "ray\.serve|serve\.|DeploymentHandle|deployment" --type py

Repository: touale/FrameX-kit

Length of output: 29637


🌐 Web query:

Ray Serve 2.53.0 API changes breaking deployment handle calls

💡 Result:

Short answer: Ray Serve 2.53.0 tightened the DeploymentHandle/DeploymentResponse composition rules — passing a DeploymentResponse (or a response generator) implicitly into downstream handle calls from nested objects is no longer supported. Instead you must convert the response to an underlying Ray ObjectRef (or await it) before passing it downstream. See the docs and recommended fixes below.

What changed

  • The Serve docs warn that passing a DeploymentResponse into downstream handle calls in nested objects is fully deprecated / not supported; streaming DeploymentResponseGenerator values also cannot be passed. [1][2]

How to fix your code

  • If you currently do something like: downstream_handle.method.remote(upstream_handle.method.remote(...))
    then either:
    • await the DeploymentResponse inside the deployment (async) before passing the value; or
    • convert to an ObjectRef and pass that: await response._to_object_ref() (async) or response._to_object_ref_sync() (sync), then pass the ObjectRef to downstream calls. [1][2]

Relevant docs

  • Model composition / deprecation note (passing DeploymentResponse in nested objects fully deprecated). [1]
  • DeploymentHandle / DeploymentResponse and streaming response behavior (cannot pass DeploymentResponseGenerator, use await or convert to ObjectRef). [2]

If you want, paste a short snippet of the failing code and I’ll rewrite it to the supported pattern.

Sources:
[1] Ray Serve — Deploy Compositions of Models (warning about passing DeploymentResponse in nested objects).
[2] Ray Serve — DeploymentHandle / DeploymentResponse / DeploymentResponseGenerator docs.


Test Ray 2.53.0 for breaking changes in Serve deployments and runtime behavior.

This upgrade spans 6 minor versions and includes documented breaking changes: DeploymentResponse composition rules now prohibit passing responses into downstream handle calls without explicit await or ObjectRef conversion; default uv run integration enabled (affecting runtime_env/pip); Python 3.9 wheels EOL; and security/packaging changes. Verify streaming patterns in ray_adapter.py (.options(stream=True).remote()) and deployment initialization in __init__.py (serve.start() / serve.run()) are unaffected, and test the lifespan context behavior in application.py.

🤖 Prompt for AI Agents
In pyproject.toml around line 17, the Ray upgrade to "ray[serve]==2.53.0" may
introduce breaking changes; update the code and tests to validate and fix
Serve-related adjustments: in ray_adapter.py ensure all
.options(stream=True).remote() streaming calls remain compatible and adjust to
any API changes; in modules that pass DeploymentResponse objects into downstream
handle calls (search repo for .remote(handle) usages), make those calls await
the response or convert to ObjectRef before passing; in package entrypoints
(__init__.py) confirm serve.start() / serve.run() usage matches 2.53.0 semantics
and modify initialization accordingly; in application.py add tests or
adjustments to verify lifespan context behavior and ensure runtime_env/pip
interactions are correct under the new default uv loop integration. Run full
test suite and update code paths or dependency pinning based on failures.

@touale touale merged commit bb1868c into master Dec 23, 2025
7 checks passed
@touale touale deleted the feat/support-docs-auth branch December 24, 2025 01:47
This was referenced Dec 25, 2025
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