Team Ai
Apppublic

kernelmind/Resume-Screener-API

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
exceptions.py167 linesDownload Raw Back to core
1"""2Custom exceptions for the Resume Screener API.3 4Why custom exceptions instead of generic HTTPException everywhere?51. Centralized error handling - one place to change error formats62. Type safety - catch specific errors, not generic Exception73. Better logging - we can log different errors differently84. Client-friendly messages - hide internal details from API responses9"""10 11from typing import Any12 13from fastapi import HTTPException, Request14from fastapi.responses import JSONResponse15 16from src.core.logging import logger17 18 19class ResumeScreenerError(Exception):20    """Base exception for all Resume Screener errors."""21    22    def __init__(self, message: str, details: dict[str, Any] | None = None):23        self.message = message24        self.details = details or {}25        super().__init__(self.message)26 27 28class InvalidFileError(ResumeScreenerError):29    """30    Raised when uploaded file fails validation.31    32    This catches both wrong extensions AND invalid magic bytes.33    Common attack: rename malware.exe to resume.pdf34    """35    pass36 37 38class PDFParsingError(ResumeScreenerError):39    """40    Raised when pdfplumber fails to extract text.41    42    Usually means corrupted PDF or scanned image without OCR.43    """44    pass45 46 47class GeminiAPIError(ResumeScreenerError):48    """49    Raised when Gemini API calls fail after all retries.50    51    This should only bubble up after tenacity exhausts retries,52    so if we see this, something is seriously wrong (quota, key, outage).53    """54    pass55 56 57class EmbeddingError(ResumeScreenerError):58    """Raised when embedding generation fails."""59    pass60 61 62# === FastAPI Exception Handlers ===63# These convert our custom exceptions into proper HTTP responses64 65async def resume_screener_error_handler(66    request: Request, 67    exc: ResumeScreenerError68) -> JSONResponse:69    """Handle all ResumeScreener exceptions with consistent format."""70    71    # Log the error with context - escape braces to prevent loguru KeyError72    safe_message = str(exc.message).replace('{', '{{').replace('}', '}}')73    logger.error(74        f"ResumeScreenerError: {safe_message}",75        extra={"details": exc.details, "path": request.url.path}76    )77    78    return JSONResponse(79        status_code=400,80        content={81            "success": False,82            "error": {83                "type": exc.__class__.__name__,84                "message": exc.message,85                "details": exc.details,86            }87        }88    )89 90 91async def invalid_file_error_handler(92    request: Request, 93    exc: InvalidFileError94) -> JSONResponse:95    """Handle file validation errors."""96    97    safe_message = str(exc.message).replace('{', '{{').replace('}', '}}')98    logger.warning(99        f"Invalid file upload attempt: {safe_message}",100        extra={"path": request.url.path}101    )102    103    return JSONResponse(104        status_code=415,  # Unsupported Media Type - more accurate than 400105        content={106            "success": False,107            "error": {108                "type": "InvalidFileError",109                "message": exc.message,110            }111        }112    )113 114 115async def gemini_api_error_handler(116    request: Request, 117    exc: GeminiAPIError118) -> JSONResponse:119    """Handle Gemini API failures."""120    121    # Escape braces to prevent loguru KeyError from Gemini's JSON error messages122    safe_message = str(exc.message).replace('{', '{{').replace('}', '}}')123    logger.error(124        f"Gemini API error after retries: {safe_message}",125        extra={"details": exc.details}126    )127    128    return JSONResponse(129        status_code=503,  # Service Unavailable - indicates upstream failure130        content={131            "success": False,132            "error": {133                "type": "GeminiAPIError",134                "message": "AI processing service temporarily unavailable",135                # Don't expose internal error details to clients136            }137        }138    )139 140 141async def unhandled_exception_handler(142    request: Request, 143    exc: Exception144) -> JSONResponse:145    """146    Catch-all for unhandled exceptions.147    148    This prevents stack traces from leaking to clients.149    We log the full error but return a generic message.150    """151    152    logger.exception(153        f"Unhandled exception on {request.url.path}",154        exc_info=exc155    )156    157    return JSONResponse(158        status_code=500,159        content={160            "success": False,161            "error": {162                "type": "InternalError",163                "message": "An unexpected error occurred. Please try again.",164            }165        }166    )167