Skip to main content
Glama
error_handler.py11 kB
"""Centralized error handler for Trakt API responses.""" import contextlib import logging import uuid from collections.abc import Callable from typing import Any import httpx from utils.api.errors import ( InternalError, InvalidParamsError, InvalidRequestError, MCPError, ) from .error_types import ( AuthenticationRequiredError, AuthorizationPendingError, TraktRateLimitError, TraktResourceNotFoundError, TraktServerError, TraktValidationError, ) logger = logging.getLogger("trakt_mcp.error_handler") class TraktAPIErrorHandler: """Centralized handler for Trakt API errors. Converts HTTP errors from the Trakt API into appropriate MCP-compliant errors with rich context and proper logging. """ @classmethod def handle_http_error( cls, error: httpx.HTTPStatusError, *, endpoint: str | None = None, resource_type: str | None = None, resource_id: str | None = None, correlation_id: str | None = None, ) -> MCPError: """Convert HTTP error to appropriate MCP error. Args: error: The HTTP error from the API call endpoint: API endpoint that failed resource_type: Type of resource being accessed resource_id: ID of the resource being accessed correlation_id: Request correlation ID for tracing Returns: Appropriate MCPError subclass for the HTTP status code """ status_code = error.response.status_code # Generate correlation ID if not provided if correlation_id is None: correlation_id = str(uuid.uuid4()) # Extract response details for context try: response_text = error.response.text except Exception: response_text = "Unable to read response text" # Log the error with context cls.log_http_error( status_code=status_code, endpoint=endpoint, resource_type=resource_type, resource_id=resource_id, correlation_id=correlation_id, response_text=response_text, ) # Get appropriate handler for status code handler = cls.get_status_code_handler(status_code) # Call handler with context return handler( error=error, endpoint=endpoint, resource_type=resource_type, resource_id=resource_id, correlation_id=correlation_id, response_text=response_text, ) @classmethod def get_status_code_handler(cls, status_code: int) -> Callable[..., MCPError]: """Get the appropriate handler method for a status code.""" handlers = { 400: cls.handle_bad_request, 401: cls.handle_unauthorized, 403: cls.handle_forbidden, 404: cls.handle_not_found, 409: cls.handle_conflict, 422: cls.handle_validation_error, 429: cls.handle_rate_limit, 500: cls.handle_server_error, 502: cls.handle_bad_gateway, 503: cls.handle_service_unavailable, } return handlers.get(status_code, cls.handle_unknown_error) @classmethod def handle_bad_request( cls, **context: Any ) -> InvalidParamsError | AuthorizationPendingError | TraktValidationError: """Handle 400 Bad Request errors.""" response_text = context.get("response_text", "") # Check for authorization pending (OAuth device flow) if "authorization_pending" in response_text.lower(): return AuthorizationPendingError( device_code=context.get("resource_id"), ) # Check for specific validation errors if "invalid" in response_text.lower() or "validation" in response_text.lower(): return TraktValidationError( "Bad request. Please check your request parameters.", validation_details={"api_response": response_text}, ) return InvalidParamsError( "Bad request. Please check your request parameters.", data={ "http_status": 400, "details": response_text, "endpoint": context.get("endpoint"), "correlation_id": context.get("correlation_id"), }, ) @classmethod def handle_unauthorized(cls, **context: Any) -> AuthenticationRequiredError: """Handle 401 Unauthorized errors.""" resource_type = context.get("resource_type") or "resource" action = f"access {resource_type}" return AuthenticationRequiredError( action=action, message="Authentication required. Please check your Trakt API credentials.", ) @classmethod def handle_forbidden(cls, **context: Any) -> InvalidRequestError: """Handle 403 Forbidden errors.""" return InvalidRequestError( "Forbidden. Invalid API key or unapproved application.", data={ "http_status": 403, "endpoint": context.get("endpoint"), "correlation_id": context.get("correlation_id"), "details": context.get("response_text"), }, ) @classmethod def handle_not_found(cls, **context: Any) -> TraktResourceNotFoundError: """Handle 404 Not Found errors.""" resource_type = context.get("resource_type") or "resource" resource_id = context.get("resource_id") or "unknown" return TraktResourceNotFoundError( resource_type=resource_type, resource_id=resource_id, endpoint=context.get("endpoint"), correlation_id=context.get("correlation_id"), ) @classmethod def handle_conflict(cls, **context: Any) -> InvalidRequestError: """Handle 409 Conflict errors.""" return InvalidRequestError( "Conflict. The resource already exists or cannot be modified.", data={ "http_status": 409, "endpoint": context.get("endpoint"), "resource_type": context.get("resource_type"), "resource_id": context.get("resource_id"), "correlation_id": context.get("correlation_id"), "details": context.get("response_text"), }, ) @classmethod def handle_validation_error(cls, **context: Any) -> TraktValidationError: """Handle 422 Unprocessable Entity errors.""" response_text = context.get("response_text", "") validation_details: dict[str, str] = {"api_response": response_text} endpoint = context.get("endpoint") if endpoint is not None: validation_details["endpoint"] = endpoint correlation_id = context.get("correlation_id") if correlation_id is not None: validation_details["correlation_id"] = correlation_id return TraktValidationError( "Validation error. Please check your input data.", validation_details=validation_details, ) @classmethod def handle_rate_limit(cls, **context: Any) -> TraktRateLimitError: """Handle 429 Rate Limit Exceeded errors.""" # Try to extract retry-after header if available error = context.get("error") retry_after = None if error and hasattr(error, "response"): retry_after_header = error.response.headers.get("retry-after") if retry_after_header: with contextlib.suppress(ValueError): retry_after = int(retry_after_header) return TraktRateLimitError( retry_after=retry_after, endpoint=context.get("endpoint"), correlation_id=context.get("correlation_id"), ) @classmethod def handle_server_error(cls, **context: Any) -> TraktServerError: """Handle 500 Internal Server Error.""" return TraktServerError( http_status=500, message="Trakt API server error. Please try again later.", endpoint=context.get("endpoint"), correlation_id=context.get("correlation_id"), ) @classmethod def handle_bad_gateway(cls, **context: Any) -> TraktServerError: """Handle 502 Bad Gateway errors.""" return TraktServerError( http_status=502, message="Bad gateway. The Trakt API server is experiencing issues.", endpoint=context.get("endpoint"), correlation_id=context.get("correlation_id"), ) @classmethod def handle_service_unavailable(cls, **context: Any) -> TraktServerError: """Handle 503 Service Unavailable errors.""" return TraktServerError( http_status=503, message="Service unavailable. Please try again in 30 seconds.", endpoint=context.get("endpoint"), correlation_id=context.get("correlation_id"), ) @classmethod def handle_unknown_error(cls, **context: Any) -> InternalError: """Handle unknown/unmapped status codes.""" error = context.get("error") status_code = error.response.status_code if error else "unknown" return InternalError( f"HTTP {status_code} error occurred", data={ "http_status": status_code, "endpoint": context.get("endpoint"), "resource_type": context.get("resource_type"), "resource_id": context.get("resource_id"), "correlation_id": context.get("correlation_id"), "response": context.get("response_text"), }, ) @classmethod def log_http_error( cls, *, status_code: int, endpoint: str | None, resource_type: str | None, resource_id: str | None, correlation_id: str, response_text: str, ) -> None: """Log HTTP error with structured context.""" log_context = { "http_status": status_code, "correlation_id": correlation_id, "response_length": len(response_text), } if endpoint: log_context["endpoint"] = endpoint if resource_type: log_context["resource_type"] = resource_type if resource_id: log_context["resource_id"] = resource_id # Truncate response text for logging truncated_response = ( response_text[:200] + "..." if len(response_text) > 200 else response_text ) log_context["response_preview"] = truncated_response logger.error( f"Trakt API HTTP {status_code} error", extra={"context": log_context}, ) def create_correlation_id() -> str: """Generate a new correlation ID for request tracing.""" return str(uuid.uuid4())

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wwiens/trakt_mcpserver'

If you have feedback or need assistance with the MCP directory API, please join our Discord server