File size: 9,092 Bytes
4a2ab42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
"""
Enhanced Error Handling with User-Friendly Messages

This module provides structured error tracking and user-friendly error messages
for API responses.
"""

from enum import Enum
from typing import Any

from fastapi import HTTPException
from pydantic import BaseModel

from core.logging import log_error, logger


class ErrorCategory(str, Enum):
    """Error categories for better error handling"""

    CLIENT_ERROR = "client_error"  # User input issues (400-level)
    SERVER_ERROR = "server_error"  # Internal server issues (500-level)
    VALIDATION_ERROR = "validation_error"  # Data validation failures
    AUTHENTICATION_ERROR = "authentication_error"  # Auth failures
    AUTHORIZATION_ERROR = "authorization_error"  # Permission issues
    NOT_FOUND_ERROR = "not_found_error"  # Resource not found
    CONFLICT_ERROR = "conflict_error"  # Resource conflicts
    RATE_LIMIT_ERROR = "rate_limit_error"  # Too many requests


class AppError(BaseModel):
    """Structured error model"""

    category: ErrorCategory
    code: str
    message: str  # User-friendly message
    technical_message: str | None = None  # Technical details (dev only)
    context: dict[str, Any] | None = None
    suggestion: str | None = None  # What user should do


# User-friendly error message mappings
ERROR_MESSAGES = {
    # Database errors
    "db_connection_failed": AppError(
        category=ErrorCategory.SERVER_ERROR,
        code="db_connection_failed",
        message="Unable to connect to the database. Please try again later.",
        suggestion="If the problem persists, please contact support.",
    ),
    "db_query_timeout": AppError(
        category=ErrorCategory.SERVER_ERROR,
        code="db_query_timeout",
        message="The request took too long to process. Please try again.",
        suggestion="Try narrowing your search criteria.",
    ),
    # Validation errors
    "invalid_case_id": AppError(
        category=ErrorCategory.VALIDATION_ERROR,
        code="invalid_case_id",
        message="The case ID provided is not valid.",
        suggestion="Please check the case ID and try again.",
    ),
    "invalid_date_range": AppError(
        category=ErrorCategory.VALIDATION_ERROR,
        code="invalid_date_range",
        message="The date range is invalid.",
        suggestion="Please ensure the start date is before the end date.",
    ),
    "file_too_large": AppError(
        category=ErrorCategory.VALIDATION_ERROR,
        code="file_too_large",
        message="The file you're trying to upload is too large.",
        suggestion="Please upload a file smaller than 50MB.",
    ),
    # Authentication errors
    "invalid_credentials": AppError(
        category=ErrorCategory.AUTHENTICATION_ERROR,
        code="invalid_credentials",
        message="The username or password you entered is incorrect.",
        suggestion="Please check your credentials and try again.",
    ),
    "session_expired": AppError(
        category=ErrorCategory.AUTHENTICATION_ERROR,
        code="session_expired",
        message="Your session has expired.",
        suggestion="Please log in again to continue.",
    ),
    # Authorization errors
    "insufficient_permissions": AppError(
        category=ErrorCategory.AUTHORIZATION_ERROR,
        code="insufficient_permissions",
        message="You don't have permission to perform this action.",
        suggestion="Contact your administrator if you need access.",
    ),
    # Not found errors
    "case_not_found": AppError(
        category=ErrorCategory.NOT_FOUND_ERROR,
        code="case_not_found",
        message="The case you're looking for could not be found.",
        suggestion="Please verify the case ID and try again.",
    ),
    "evidence_not_found": AppError(
        category=ErrorCategory.NOT_FOUND_ERROR,
        code="evidence_not_found",
        message="The evidence file could not be found.",
        suggestion="The file may have been deleted or moved.",
    ),
    # Conflict errors
    "case_already_exists": AppError(
        category=ErrorCategory.CONFLICT_ERROR,
        code="case_already_exists",
        message="A case with this ID already exists.",
        suggestion="Please use a different case ID or update the existing case.",
    ),
    # Rate limit errors
    "rate_limit_exceeded": AppError(
        category=ErrorCategory.RATE_LIMIT_ERROR,
        code="rate_limit_exceeded",
        message="You've made too many requests. Please slow down.",
        suggestion="Wait a few minutes before trying again.",
    ),
    # Fraud detection errors
    "fraud_analysis_failed": AppError(
        category=ErrorCategory.SERVER_ERROR,
        code="fraud_analysis_failed",
        message="We couldn't complete the fraud analysis.",
        suggestion="Please try again or upload different evidence.",
    ),
    # File processing errors
    "file_processing_failed": AppError(
        category=ErrorCategory.SERVER_ERROR,
        code="file_processing_failed",
        message="We couldn't process your file.",
        suggestion="Please ensure the file is not corrupted and try again.",
    ),
    "unsupported_file_type": AppError(
        category=ErrorCategory.VALIDATION_ERROR,
        code="unsupported_file_type",
        message="This file type is not supported.",
        suggestion="Please upload a PDF, image, or video file.",
    ),
}


def get_error_message(
    error_code: str,
    context: dict[str, Any] | None = None,
    technical_message: str | None = None,
) -> AppError:
    """
    Get user-friendly error message for a given error code.

    Args:
        error_code: Error code identifier
        context: Additional context about the error
        technical_message: Technical details (only shown in dev mode)

    Returns:
        AppError: Structured error with user-friendly message
    """
    error = ERROR_MESSAGES.get(error_code)

    if not error:
        # Default error for unknown codes
        error = AppError(
            category=ErrorCategory.SERVER_ERROR,
            code=error_code,
            message="An unexpected error occurred.",
            suggestion="Please try again later or contact support.",
        )

    # Add context and technical details
    if context:
        error.context = context
    if technical_message:
        error.technical_message = technical_message

    return error


def raise_app_error(
    error_code: str,
    status_code: int = 400,
    context: dict[str, Any] | None = None,
    technical_message: str | None = None,
    log_level: str = "warning",
):
    """
    Raise an HTTPException with structured error details.

    Args:
        error_code: Error code identifier
        status_code: HTTP status code
        context: Additional error context
        technical_message: Technical details for logging
        log_level: Logging level (debug, info, warning, error)
    """
    error = get_error_message(error_code, context, technical_message)

    # Log the error
    log_data = {
        "error_code": error_code,
        "category": error.category,
        "status_code": status_code,
        "context": context or {},
    }

    if technical_message:
        log_data["technical_message"] = technical_message

    if log_level == "error":
        log_error(error_code, error.message, log_data)
    elif log_level == "warning":
        logger.warning(error.message, extra=log_data)
    else:
        logger.info(error.message, extra=log_data)

    # Raise HTTP exception
    raise HTTPException(
        status_code=status_code,
        detail={
            "error": {
                "code": error.code,
                "category": error.category,
                "message": error.message,
                "suggestion": error.suggestion,
                "context": error.context,
            }
        },
    )


def handle_exception(exc: Exception, request_id: str | None = None) -> dict[str, Any]:
    """
    Convert any exception to a structured error response.

    Args:
        exc: Exception to handle
        request_id: Optional request ID for tracking

    Returns:
        dict: Structured error response
    """
    error_code = "unknown_error"

    # Map common exceptions to error codes
    if isinstance(exc, ValueError):
        error_code = "invalid_input"
    elif isinstance(exc, PermissionError):
        error_code = "insufficient_permissions"
    elif isinstance(exc, FileNotFoundError):
        error_code = "not_found"
    elif isinstance(exc, TimeoutError):
        error_code = "request_timeout"

    error = get_error_message(
        error_code,
        context={"request_id": request_id} if request_id else None,
        technical_message=str(exc),
    )

    # Log the exception
    log_error(
        error_code,
        str(exc),
        {"exception_type": type(exc).__name__, "request_id": request_id},
    )

    return {
        "error": {
            "code": error.code,
            "category": error.category,
            "message": error.message,
            "suggestion": error.suggestion,
            "request_id": request_id,
        }
    }