From 2eacdf7f45c55f0083d03b8638b3c0b8dae1f013 Mon Sep 17 00:00:00 2001 From: raj_Prakhar Date: Sun, 7 Jun 2026 19:17:18 +0000 Subject: [PATCH 1/2] fix: add error classification middleware for better observability (#618) --- backend/app/main.py | 2 + backend/app/middleware.py | 86 +++++++++++++++++++++------------------ 2 files changed, 48 insertions(+), 40 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index e1903cfd..97829121 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -31,6 +31,7 @@ from .routers import metrics as metrics_router from .services import database from .services.scheduler import start_scheduler, stop_scheduler +from .middleware import error_classification_middleware from .observability import ( initialise_app_info, prometheus_metrics_middleware, @@ -105,6 +106,7 @@ async def lifespan(app: FastAPI): # METRICS_ENABLED is false, so installing it unconditionally costs nothing # operationally and lets operators flip the flag without a restart. app.middleware("http")(prometheus_metrics_middleware) +app.middleware("http")(error_classification_middleware) @app.middleware("http") diff --git a/backend/app/middleware.py b/backend/app/middleware.py index 9bcfbd09..d7dc344b 100644 --- a/backend/app/middleware.py +++ b/backend/app/middleware.py @@ -1,7 +1,9 @@ +cat > backend/app/middleware.py << 'EOF' import logging import time import uuid from collections import defaultdict, deque +from enum import Enum from threading import Lock from fastapi import Request @@ -27,49 +29,26 @@ def get_client_key(request: Request) -> str: async def request_id_and_logging_middleware(request: Request, call_next): request_id = str(uuid.uuid4()) request.state.request_id = request_id - started_at = time.perf_counter() - logger.info( - "request_started request_id=%s method=%s path=%s", - request_id, - request.method, - request.url.path, - ) - + logger.info("request_started request_id=%s method=%s path=%s", request_id, request.method, request.url.path) response = await call_next(request) - elapsed_ms = (time.perf_counter() - started_at) * 1000 response.headers["X-Request-ID"] = request_id - logger.info( - "request_finished request_id=%s method=%s path=%s status=%s elapsed_ms=%.2f", - request_id, - request.method, - request.url.path, - response.status_code, - elapsed_ms, - ) + logger.info("request_finished request_id=%s method=%s path=%s status=%s elapsed_ms=%.2f", request_id, request.method, request.url.path, response.status_code, elapsed_ms) return response async def request_size_limit_middleware(request: Request, call_next): if request.method not in {"POST", "PUT", "PATCH"}: return await call_next(request) - content_length = request.headers.get("content-length") if content_length: try: declared_size = int(content_length) if declared_size > settings.max_request_bytes: - return JSONResponse( - status_code=413, - content={ - "error": "payload_too_large", - "detail": f"Request body exceeds {settings.max_request_bytes} bytes limit.", - }, - ) + return JSONResponse(status_code=413, content={"error": "payload_too_large", "detail": f"Request body exceeds {settings.max_request_bytes} bytes limit."}) except ValueError: pass - return await call_next(request) @@ -77,24 +56,51 @@ async def rate_limit_middleware(request: Request, call_next): client_key = get_client_key(request) now = time.time() cutoff = now - settings.rate_limit_window_seconds - with _rate_limit_lock: bucket = _rate_limit_buckets[client_key] while bucket and bucket[0] < cutoff: bucket.popleft() - if len(bucket) >= settings.rate_limit_requests: - return JSONResponse( - status_code=429, - content={ - "error": "rate_limited", - "detail": ( - f"Too many requests. Limit is {settings.rate_limit_requests} requests " - f"per {settings.rate_limit_window_seconds} seconds." - ), - }, - ) - + return JSONResponse(status_code=429, content={"error": "rate_limited", "detail": f"Too many requests. Limit is {settings.rate_limit_requests} requests per {settings.rate_limit_window_seconds} seconds."}) bucket.append(now) - return await call_next(request) + + +class ErrorCategory(str, Enum): + CLIENT = "client_error" + SERVER = "server_error" + PROVIDER = "provider_error" + + +ERROR_CATEGORY_MAP = { + 400: ErrorCategory.CLIENT, + 401: ErrorCategory.CLIENT, + 403: ErrorCategory.CLIENT, + 404: ErrorCategory.CLIENT, + 413: ErrorCategory.CLIENT, + 422: ErrorCategory.CLIENT, + 429: ErrorCategory.CLIENT, + 500: ErrorCategory.SERVER, + 502: ErrorCategory.PROVIDER, + 503: ErrorCategory.PROVIDER, + 504: ErrorCategory.PROVIDER, +} + + +async def error_classification_middleware(request: Request, call_next): + response = await call_next(request) + status = response.status_code + if status >= 400: + category = ERROR_CATEGORY_MAP.get(status) + if category is None: + category = ErrorCategory.SERVER if status >= 500 else ErrorCategory.CLIENT + response.headers["X-Error-Category"] = category.value + logger.warning( + "error_response request_id=%s status=%s category=%s path=%s", + getattr(request.state, "request_id", "unknown"), + status, + category.value, + request.url.path, + ) + return response +EOF \ No newline at end of file From 8710c3d79d37048b3999ad66d683914d2c7a6993 Mon Sep 17 00:00:00 2001 From: raj_Prakhar Date: Mon, 8 Jun 2026 05:04:58 +0000 Subject: [PATCH 2/2] fix: correct middleware.py file content --- backend/app/middleware.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/app/middleware.py b/backend/app/middleware.py index d7dc344b..772aa945 100644 --- a/backend/app/middleware.py +++ b/backend/app/middleware.py @@ -1,4 +1,3 @@ -cat > backend/app/middleware.py << 'EOF' import logging import time import uuid @@ -102,5 +101,4 @@ async def error_classification_middleware(request: Request, call_next): category.value, request.url.path, ) - return response -EOF \ No newline at end of file + return response \ No newline at end of file