fix: improve error handling and stability - no logout on transient errors, add ErrorBoundary, global error logging

This commit is contained in:
Agent Zero
2026-07-26 12:18:52 +02:00
parent e12b85c2ce
commit 808da564f3
7 changed files with 344 additions and 8 deletions
+21 -3
View File
@@ -8,7 +8,7 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from starlette.middleware.base import BaseHTTPMiddleware
import importlib
@@ -29,6 +29,7 @@ from app.routes import (
ai_copilot,
audit,
auth,
errors,
contact_folders,
contacts,
dashboard,
@@ -308,6 +309,22 @@ def create_app() -> FastAPI:
app.add_middleware(CSRFMiddleware)
app.add_middleware(RequestLoggingMiddleware)
# ── Global exception handler — catch ALL unhandled exceptions ──
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
logger.error(f"Unhandled exception: {exc}", exc_info=True)
record_error(
event="unhandled_exception",
method=request.method,
path=request.url.path,
status_code=500,
error=str(exc),
)
return JSONResponse(
status_code=500,
content={"detail": "Internal server error", "code": "internal_error"},
)
app.include_router(health.router)
app.include_router(metrics.router)
app.include_router(auth.router)
@@ -338,6 +355,7 @@ def create_app() -> FastAPI:
app.include_router(custom_fields.router)
app.include_router(saved_filters.router)
app.include_router(webhooks.router)
app.include_router(errors.router)
# ── Register plugin routes for all built-in plugins ──
# Routes are registered here (before app start); activation status
@@ -375,8 +393,8 @@ def create_app() -> FastAPI:
router_module = importlib.import_module(route_def.module)
router = getattr(router_module, route_def.router_attr)
app.include_router(router)
except Exception:
pass
except Exception as exc:
logger.error(f"Failed to register route {route_def.module}.{route_def.router_attr}: {exc}")
break
except Exception as exc:
logger.error(f"Failed to register plugin routes for {mod_name}: {exc}")
+77
View File
@@ -0,0 +1,77 @@
"""Error logging endpoint — accepts frontend errors and logs them.
No auth required so errors can be logged even during logout.
Rate-limited to 10 requests per minute per IP (simple in-memory implementation).
"""
from __future__ import annotations
import time
import logging
from collections import defaultdict, deque
from typing import Any
from fastapi import APIRouter, Request, Response, status
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/errors", tags=["errors"])
# -- Simple in-memory rate limiter (10 req/min per IP) --
RATE_LIMIT = 10 # max requests
RATE_WINDOW = 60 # seconds
_ip_requests: dict[str, deque[float]] = defaultdict(deque)
def _is_rate_limited(client_ip: str) -> bool:
"""Return True if the IP has exceeded the rate limit."""
now = time.monotonic()
dq = _ip_requests[client_ip]
# Remove timestamps outside the window
while dq and now - dq[0] > RATE_WINDOW:
dq.popleft()
if len(dq) >= RATE_LIMIT:
return True
dq.append(now)
return False
# -- Request schema --
class ErrorReport(BaseModel):
timestamp: str | None = None
message: str = Field(..., max_length=2000)
stack: str | None = Field(None, max_length=10000)
context: dict[str, Any] | None = None
url: str | None = Field(None, max_length=500)
userAgent: str | None = Field(None, max_length=500)
@router.post("", status_code=status.HTTP_204_NO_CONTENT)
async def report_error(error: ErrorReport, request: Request) -> Response:
"""Log a frontend error. No auth required. Rate-limited per IP."""
client_ip = request.client.host if request.client else "unknown"
if _is_rate_limited(client_ip):
return Response(status_code=status.HTTP_429_TOO_MANY_REQUESTS)
# Log with structured info
logger.error(
"Frontend error reported: %s",
error.message,
extra={
"error_timestamp": error.timestamp,
"error_message": error.message,
"error_stack": error.stack,
"error_context": error.context,
"error_url": error.url,
"error_user_agent": error.userAgent,
"client_ip": client_ip,
},
)
return Response(status_code=status.HTTP_204_NO_CONTENT)