mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-06-01 22:09:04 +02:00
Report startup validation failures without tracebacks
This commit is contained in:
+63
-7
@@ -9,13 +9,14 @@ from fastapi.exception_handlers import request_validation_exception_handler
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.responses import JSONResponse
|
||||
from loguru import logger
|
||||
from starlette.types import Receive, Scope, Send
|
||||
|
||||
from config.logging_config import configure_logging
|
||||
from config.settings import get_settings
|
||||
from providers.exceptions import ProviderError
|
||||
|
||||
from .routes import router
|
||||
from .runtime import AppRuntime
|
||||
from .runtime import AppRuntime, startup_failure_message
|
||||
from .validation_log import summarize_request_validation_body
|
||||
|
||||
|
||||
@@ -30,18 +31,68 @@ async def lifespan(app: FastAPI):
|
||||
await runtime.shutdown()
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
class GracefulLifespanApp:
|
||||
"""ASGI wrapper that reports startup failures without Starlette tracebacks."""
|
||||
|
||||
def __init__(self, app: FastAPI):
|
||||
self.app = app
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(self.app, name)
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
if scope["type"] != "lifespan":
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
await self._lifespan(receive, send)
|
||||
|
||||
async def _lifespan(self, receive: Receive, send: Send) -> None:
|
||||
settings = get_settings()
|
||||
runtime = AppRuntime.for_app(self.app, settings=settings)
|
||||
startup_complete = False
|
||||
while True:
|
||||
message = await receive()
|
||||
if message["type"] == "lifespan.startup":
|
||||
try:
|
||||
await runtime.startup()
|
||||
except Exception as exc:
|
||||
await send(
|
||||
{
|
||||
"type": "lifespan.startup.failed",
|
||||
"message": startup_failure_message(settings, exc),
|
||||
}
|
||||
)
|
||||
return
|
||||
startup_complete = True
|
||||
await send({"type": "lifespan.startup.complete"})
|
||||
continue
|
||||
|
||||
if message["type"] == "lifespan.shutdown":
|
||||
if startup_complete:
|
||||
try:
|
||||
await runtime.shutdown()
|
||||
except Exception as exc:
|
||||
logger.error("Shutdown failed: exc_type={}", type(exc).__name__)
|
||||
await send({"type": "lifespan.shutdown.failed", "message": ""})
|
||||
return
|
||||
await send({"type": "lifespan.shutdown.complete"})
|
||||
return
|
||||
|
||||
|
||||
def create_app(*, lifespan_enabled: bool = True) -> FastAPI:
|
||||
"""Create and configure the FastAPI application."""
|
||||
settings = get_settings()
|
||||
configure_logging(
|
||||
settings.log_file, verbose_third_party=settings.log_raw_api_payloads
|
||||
)
|
||||
|
||||
app = FastAPI(
|
||||
title="Claude Code Proxy",
|
||||
version="2.0.0",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
app_kwargs: dict[str, Any] = {
|
||||
"title": "Claude Code Proxy",
|
||||
"version": "2.0.0",
|
||||
}
|
||||
if lifespan_enabled:
|
||||
app_kwargs["lifespan"] = lifespan
|
||||
app = FastAPI(**app_kwargs)
|
||||
|
||||
# Register routes
|
||||
app.include_router(router)
|
||||
@@ -117,3 +168,8 @@ def create_app() -> FastAPI:
|
||||
)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def create_asgi_app() -> GracefulLifespanApp:
|
||||
"""Create the server ASGI app with graceful lifespan failure reporting."""
|
||||
return GracefulLifespanApp(create_app(lifespan_enabled=False))
|
||||
|
||||
+9
-6
@@ -64,16 +64,19 @@ def warn_if_process_auth_token(settings: Settings) -> None:
|
||||
|
||||
def log_startup_failure(settings: Settings, exc: Exception) -> None:
|
||||
"""Log startup failures without traceback noise unless verbose diagnostics are enabled."""
|
||||
message = startup_failure_message(settings, exc)
|
||||
logger.error("Startup failed:\n{}", message)
|
||||
|
||||
|
||||
def startup_failure_message(settings: Settings, exc: Exception) -> str:
|
||||
"""Return a concise startup failure message for logs and ASGI lifespan failure."""
|
||||
if isinstance(exc, ServiceUnavailableError):
|
||||
message = exc.message.strip() or "Server startup failed."
|
||||
logger.error("Startup failed:\n{}", message)
|
||||
return
|
||||
return exc.message.strip() or "Server startup failed."
|
||||
|
||||
if settings.log_api_error_tracebacks:
|
||||
logger.error("Startup failed: {}: {}", type(exc).__name__, exc)
|
||||
return
|
||||
return f"{type(exc).__name__}: {exc}"
|
||||
|
||||
logger.error("Startup failed: exc_type={}", type(exc).__name__)
|
||||
return f"Server startup failed: exc_type={type(exc).__name__}"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ def serve() -> None:
|
||||
settings = get_settings()
|
||||
try:
|
||||
uvicorn.run(
|
||||
"api.app:create_app",
|
||||
"api.app:create_asgi_app",
|
||||
factory=True,
|
||||
host=settings.host,
|
||||
port=settings.port,
|
||||
|
||||
@@ -5,9 +5,9 @@ Minimal entry point that builds the ASGI app via :func:`api.app.create_app`.
|
||||
Run with: uv run uvicorn server:app --host 0.0.0.0 --port 8082 --timeout-graceful-shutdown 5
|
||||
"""
|
||||
|
||||
from api.app import create_app
|
||||
from api.app import create_app, create_asgi_app
|
||||
|
||||
app = create_app()
|
||||
app = create_asgi_app()
|
||||
|
||||
__all__ = ["app", "create_app"]
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import importlib
|
||||
from collections.abc import MutableMapping
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
from typing import Any, cast
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -397,6 +398,43 @@ async def test_runtime_startup_validation_blocks_messaging_and_cleans_up(tmp_pat
|
||||
assert "Traceback" not in logged
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_graceful_asgi_lifespan_failure_sends_no_traceback(tmp_path):
|
||||
import api.app as api_app_mod
|
||||
|
||||
settings = _app_settings(
|
||||
messaging_platform="none",
|
||||
telegram_bot_token=None,
|
||||
allowed_telegram_user_id=None,
|
||||
discord_bot_token=None,
|
||||
allowed_discord_channels=None,
|
||||
allowed_dir=str(tmp_path / "workspace"),
|
||||
claude_workspace=str(tmp_path / "data"),
|
||||
host="127.0.0.1",
|
||||
port=8082,
|
||||
log_file=str(tmp_path / "server.log"),
|
||||
)
|
||||
app = api_app_mod.GracefulLifespanApp(FastAPI())
|
||||
sent: list[MutableMapping[str, Any]] = []
|
||||
|
||||
async def receive() -> MutableMapping[str, Any]:
|
||||
return {"type": "lifespan.startup"}
|
||||
|
||||
async def send(message: MutableMapping[str, Any]) -> None:
|
||||
sent.append(message)
|
||||
|
||||
validation = AsyncMock(side_effect=ServiceUnavailableError("bad model"))
|
||||
cleanup = AsyncMock()
|
||||
with (
|
||||
patch.object(api_app_mod, "get_settings", return_value=settings),
|
||||
patch.object(ProviderRegistry, "validate_configured_models", new=validation),
|
||||
patch.object(ProviderRegistry, "cleanup", new=cleanup),
|
||||
):
|
||||
await app({"type": "lifespan"}, receive, send)
|
||||
|
||||
assert sent == [{"type": "lifespan.startup.failed", "message": "bad model"}]
|
||||
|
||||
|
||||
def test_app_lifespan_messaging_import_error_no_crash(tmp_path, caplog):
|
||||
"""Messaging import failure logs warning and continues without crash."""
|
||||
from api.app import create_app
|
||||
|
||||
Reference in New Issue
Block a user