import logging import traceback from functools import wraps from typing import Optional, Dict, Any, Callable, Union from flask import ( Flask, request, jsonify, render_template, flash, redirect, url_for, current_app, g ) from werkzeug.exceptions import HTTPException import psycopg2 from .exceptions import ( BaseApplicationException, DatabaseException, ValidationException, AuthenticationException, ResourceException, QueryError, ConnectionError, TransactionError ) logger = logging.getLogger(__name__) def init_error_handlers(app: Flask) -> None: @app.before_request def before_request(): g.request_id = request.headers.get('X-Request-ID', BaseApplicationException('', '', 0).request_id) @app.errorhandler(BaseApplicationException) def handle_application_error(error: BaseApplicationException): return _handle_error(error) @app.errorhandler(HTTPException) def handle_http_error(error: HTTPException): return _handle_error(error) @app.errorhandler(psycopg2.Error) def handle_database_error(error: psycopg2.Error): db_exception = _convert_psycopg2_error(error) return _handle_error(db_exception) @app.errorhandler(Exception) def handle_unexpected_error(error: Exception): logger.error( f"Unexpected error: {str(error)}", exc_info=True, extra={'request_id': getattr(g, 'request_id', 'unknown')} ) if current_app.debug: raise generic_error = BaseApplicationException( message="An unexpected error occurred", code="INTERNAL_ERROR", status_code=500, user_message="Ein unerwarteter Fehler ist aufgetreten" ) return _handle_error(generic_error) def _handle_error(error: Union[BaseApplicationException, HTTPException, Exception]) -> tuple: if isinstance(error, HTTPException): status_code = error.code error_dict = { 'error': { 'code': error.name.upper().replace(' ', '_'), 'message': error.description or str(error), 'request_id': getattr(g, 'request_id', 'unknown') } } user_message = error.description or str(error) elif isinstance(error, BaseApplicationException): status_code = error.status_code error_dict = error.to_dict(include_details=current_app.debug) error_dict['error']['request_id'] = getattr(g, 'request_id', error.request_id) user_message = error.user_message logger.error( f"{error.__class__.__name__}: {error.message}", extra={ 'error_code': error.code, 'details': error.details, 'request_id': error_dict['error']['request_id'] } ) else: status_code = 500 error_dict = { 'error': { 'code': 'INTERNAL_ERROR', 'message': 'An internal error occurred', 'request_id': getattr(g, 'request_id', 'unknown') } } user_message = "Ein interner Fehler ist aufgetreten" if _is_json_request(): return jsonify(error_dict), status_code else: if status_code == 404: return render_template('404.html'), 404 elif status_code >= 500: return render_template('500.html', error=user_message), status_code else: flash(user_message, 'error') return render_template('error.html', error=user_message, error_code=error_dict['error']['code'], request_id=error_dict['error']['request_id']), status_code def _convert_psycopg2_error(error: psycopg2.Error) -> DatabaseException: error_code = getattr(error, 'pgcode', None) error_message = str(error).split('\n')[0] if isinstance(error, psycopg2.OperationalError): return ConnectionError( message=f"Database connection failed: {error_message}", host=None ) elif isinstance(error, psycopg2.IntegrityError): if error_code == '23505': return ValidationException( message="Duplicate entry violation", details={'constraint': error_message}, user_message="Dieser Eintrag existiert bereits" ) elif error_code == '23503': return ValidationException( message="Foreign key violation", details={'constraint': error_message}, user_message="Referenzierte Daten existieren nicht" ) else: return ValidationException( message="Data integrity violation", details={'error_code': error_code}, user_message="Datenintegritätsfehler" ) elif isinstance(error, psycopg2.DataError): return ValidationException( message="Invalid data format", details={'error': error_message}, user_message="Ungültiges Datenformat" ) else: return QueryError( message=error_message, query="[query hidden for security]", error_code=error_code ) def _is_json_request() -> bool: return (request.is_json or request.path.startswith('/api/') or request.accept_mimetypes.best == 'application/json') def handle_errors( catch: tuple = (Exception,), message: str = "Operation failed", user_message: Optional[str] = None, redirect_to: Optional[str] = None ) -> Callable: def decorator(func: Callable) -> Callable: @wraps(func) def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except catch as e: if isinstance(e, BaseApplicationException): raise logger.error( f"Error in {func.__name__}: {str(e)}", exc_info=True, extra={'request_id': getattr(g, 'request_id', 'unknown')} ) if _is_json_request(): return jsonify({ 'error': { 'code': 'OPERATION_FAILED', 'message': user_message or message, 'request_id': getattr(g, 'request_id', 'unknown') } }), 500 else: flash(user_message or message, 'error') if redirect_to: return redirect(url_for(redirect_to)) return redirect(request.referrer or url_for('admin.dashboard')) return wrapper return decorator def validate_request( required_fields: Optional[Dict[str, type]] = None, optional_fields: Optional[Dict[str, type]] = None ) -> Callable: def decorator(func: Callable) -> Callable: @wraps(func) def wrapper(*args, **kwargs): data = request.get_json() if request.is_json else request.form if required_fields: for field, expected_type in required_fields.items(): if field not in data: raise ValidationException( message=f"Missing required field: {field}", field=field, user_message=f"Pflichtfeld fehlt: {field}" ) try: if expected_type != str: if expected_type == int: int(data[field]) elif expected_type == float: float(data[field]) elif expected_type == bool: if isinstance(data[field], str): if data[field].lower() not in ['true', 'false', '1', '0']: raise ValueError except (ValueError, TypeError): raise ValidationException( message=f"Invalid type for field {field}", field=field, value=data[field], details={'expected_type': expected_type.__name__}, user_message=f"Ungültiger Typ für Feld {field}" ) return func(*args, **kwargs) return wrapper return decorator class ErrorContext: def __init__( self, operation: str, resource_type: Optional[str] = None, resource_id: Optional[Any] = None ): self.operation = operation self.resource_type = resource_type self.resource_id = resource_id def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_val is None: return False if isinstance(exc_val, BaseApplicationException): return False logger.error( f"Error during {self.operation}", exc_info=True, extra={ 'operation': self.operation, 'resource_type': self.resource_type, 'resource_id': self.resource_id, 'request_id': getattr(g, 'request_id', 'unknown') } ) return False