feat: enhance error handling with custom HTTP exception responses and middleware improvements

This commit is contained in:
2026-02-16 13:18:42 -06:00
parent ffc82ea82b
commit e6ef5c9094
4 changed files with 56 additions and 24 deletions

View File

@@ -18,5 +18,10 @@ class UserContextMiddleware(BaseHTTPMiddleware):
# Log error or ignore # Log error or ignore
pass pass
response = await call_next(request) try:
response = await call_next(request)
except Exception:
# Re-raise the exception to let other middleware and handlers deal with it
raise
return response return response

View File

@@ -5,7 +5,7 @@ Manejadores globales de excepciones para FastAPI
import logging import logging
from typing import Any, Dict from typing import Any, Dict
from fastapi import Request, status from fastapi import Request, status, HTTPException
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError from fastapi.exceptions import RequestValidationError
@@ -138,6 +138,23 @@ async def sqlalchemy_error_handler(
) )
async def http_exception_handler(
request: Request,
exc: HTTPException,
) -> JSONResponse:
"""
Manejador para HTTPException de FastAPI
"""
return JSONResponse(
status_code=exc.status_code,
content={
"error": "HTTP_ERROR",
"message": exc.detail,
"status_code": exc.status_code,
},
)
async def general_exception_handler( async def general_exception_handler(
request: Request, request: Request,
exc: Exception, exc: Exception,
@@ -172,6 +189,7 @@ def register_exception_handlers(app) -> None:
app: Instancia de FastAPI app: Instancia de FastAPI
""" """
app.add_exception_handler(BaseAPIException, base_exception_handler) app.add_exception_handler(BaseAPIException, base_exception_handler)
app.add_exception_handler(HTTPException, http_exception_handler)
app.add_exception_handler(RequestValidationError, validation_exception_handler) app.add_exception_handler(RequestValidationError, validation_exception_handler)
app.add_exception_handler(IntegrityError, integrity_error_handler) app.add_exception_handler(IntegrityError, integrity_error_handler)
app.add_exception_handler(SQLAlchemyError, sqlalchemy_error_handler) app.add_exception_handler(SQLAlchemyError, sqlalchemy_error_handler)

View File

@@ -1,7 +1,8 @@
import logging import logging
import time import time
from typing import Callable from typing import Callable
from fastapi import HTTPException, Request, Response from fastapi import Request, Response
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
from .config import settings from .config import settings
from .database import CoreSessionLocal from .database import CoreSessionLocal
@@ -34,9 +35,13 @@ class TenantMiddleware(BaseHTTPMiddleware):
# 4. Validación estricta de Token (solo para lo que no es público ni OPTIONS) # 4. Validación estricta de Token (solo para lo que no es público ni OPTIONS)
auth_header = request.headers.get("Authorization") auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "): if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException( return JSONResponse(
status_code=401, status_code=401,
detail="Missing or invalid authorization header" content={
"error": "HTTP_ERROR",
"message": "Missing or invalid authorization header",
"status_code": 401,
}
) )
token = auth_header.split(" ")[1] token = auth_header.split(" ")[1]
@@ -48,7 +53,14 @@ class TenantMiddleware(BaseHTTPMiddleware):
request.state.user_info = user_info request.state.user_info = user_info
except Exception as e: except Exception as e:
logger.error(f"❌ Tenant validation error: {str(e)}") logger.error(f"❌ Tenant validation error: {str(e)}")
raise HTTPException(status_code=401, detail="Invalid authentication") return JSONResponse(
status_code=401,
content={
"error": "HTTP_ERROR",
"message": "Invalid authentication",
"status_code": 401,
}
)
# 5. Continuar con la petición real # 5. Continuar con la petición real
return await call_next(request) return await call_next(request)
@@ -104,19 +116,28 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware):
license_info = license_service.validate_license(tenant_id) license_info = license_service.validate_license(tenant_id)
if not license_info["is_valid"]: if not license_info["is_valid"]:
raise HTTPException( return JSONResponse(
status_code=402, status_code=402,
detail=f"License validation failed: {license_info['reason']}", content={
"error": "HTTP_ERROR",
"message": f"License validation failed: {license_info['reason']}",
"status_code": 402,
}
) )
# Agregar info de licencia al request state # Agregar info de licencia al request state
request.state.license_info = license_info request.state.license_info = license_info
except HTTPException:
raise
except Exception as e: except Exception as e:
logger.error(f"License validation error: {str(e)}") logger.error(f"License validation error: {str(e)}")
raise HTTPException(status_code=500, detail="License validation error") return JSONResponse(
status_code=500,
content={
"error": "HTTP_ERROR",
"message": "License validation error",
"status_code": 500,
}
)
finally: finally:
db.close() db.close()

View File

@@ -70,18 +70,6 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
) )
# Add HTTP exception handler
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
logger.error(
f"HTTP {exc.status_code} for {request.method} {request.url.path}: {exc.detail}"
)
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
)
def run_migrations(): def run_migrations():
subprocess.run(["alembic", "upgrade", "head"], check=True) subprocess.run(["alembic", "upgrade", "head"], check=True)