awesome-errors 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- awesome_errors/__init__.py +232 -0
- awesome_errors/analysis/__init__.py +8 -0
- awesome_errors/analysis/decorators.py +251 -0
- awesome_errors/analysis/error_analyzer.py +463 -0
- awesome_errors/client/__init__.py +7 -0
- awesome_errors/client/exceptions.py +97 -0
- awesome_errors/client/response_parser.py +175 -0
- awesome_errors/converters/__init__.py +31 -0
- awesome_errors/converters/generic.py +23 -0
- awesome_errors/converters/pydantic_converter.py +75 -0
- awesome_errors/converters/python_converter.py +80 -0
- awesome_errors/converters/sql_converter.py +258 -0
- awesome_errors/converters/universal_converter.py +123 -0
- awesome_errors/core/__init__.py +0 -0
- awesome_errors/core/error_codes.py +104 -0
- awesome_errors/core/error_response.py +61 -0
- awesome_errors/core/exceptions.py +613 -0
- awesome_errors/core/renderers.py +115 -0
- awesome_errors/i18n/__init__.py +0 -0
- awesome_errors/i18n/locales/en/errors.json +32 -0
- awesome_errors/i18n/locales/test/errors.json +4 -0
- awesome_errors/i18n/locales/uk/errors.json +26 -0
- awesome_errors/i18n/translator.py +150 -0
- awesome_errors/integrations/__init__.py +11 -0
- awesome_errors/integrations/fastapi_auto_docs.py +220 -0
- awesome_errors/litestar_utils.py +55 -0
- awesome_errors/middleware/__init__.py +0 -0
- awesome_errors/middleware/fastapi.py +247 -0
- awesome_errors/middleware/litestar.py +333 -0
- awesome_errors/py.typed +1 -0
- awesome_errors/websocket/__init__.py +35 -0
- awesome_errors/websocket/error_handler.py +231 -0
- awesome_errors/websocket/exceptions.py +286 -0
- awesome_errors-0.3.0.dist-info/METADATA +239 -0
- awesome_errors-0.3.0.dist-info/RECORD +37 -0
- awesome_errors-0.3.0.dist-info/WHEEL +4 -0
- awesome_errors-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""WebSocket error handler for awesome-errors"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Dict, Any, Type, Callable, Optional
|
|
7
|
+
from fastapi import FastAPI, WebSocket
|
|
8
|
+
|
|
9
|
+
from awesome_errors.core.error_codes import ErrorCode
|
|
10
|
+
from awesome_errors.core.exceptions import AppError
|
|
11
|
+
from awesome_errors.websocket.exceptions import WebSocketError, WebSocketInternalError
|
|
12
|
+
from starlette.websockets import WebSocketState
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WebSocketErrorHandler:
|
|
18
|
+
"""
|
|
19
|
+
Centralized error handler for WebSocket connections
|
|
20
|
+
|
|
21
|
+
Provides automatic error conversion, logging, and connection management
|
|
22
|
+
similar to Anaya's approach but for WebSocket.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self.error_mappings: Dict[Type[Exception], Dict[str, Any]] = {}
|
|
27
|
+
self._setup_default_mappings()
|
|
28
|
+
|
|
29
|
+
def _setup_default_mappings(self):
|
|
30
|
+
"""Setup default error mappings"""
|
|
31
|
+
from .exceptions import (
|
|
32
|
+
WebSocketValidationError,
|
|
33
|
+
WebSocketInternalError,
|
|
34
|
+
JSONRPCErrorCode,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Map common exceptions to WebSocket errors
|
|
38
|
+
self.error_mappings.update(
|
|
39
|
+
{
|
|
40
|
+
# Pydantic validation
|
|
41
|
+
ValueError: {
|
|
42
|
+
"converter": lambda e: WebSocketValidationError(
|
|
43
|
+
message=str(e), request_id=None
|
|
44
|
+
)
|
|
45
|
+
},
|
|
46
|
+
# JSON parsing
|
|
47
|
+
json.JSONDecodeError: {
|
|
48
|
+
"converter": lambda e: WebSocketError(
|
|
49
|
+
code=ErrorCode.INVALID_FORMAT,
|
|
50
|
+
message="Invalid JSON",
|
|
51
|
+
ws_error_code=JSONRPCErrorCode.PARSE_ERROR,
|
|
52
|
+
request_id=None,
|
|
53
|
+
)
|
|
54
|
+
},
|
|
55
|
+
# Generic exceptions
|
|
56
|
+
Exception: {
|
|
57
|
+
"converter": lambda e: WebSocketInternalError(
|
|
58
|
+
message="Internal server error",
|
|
59
|
+
original_error=str(e),
|
|
60
|
+
request_id=None,
|
|
61
|
+
)
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def register_error_mapping(
|
|
67
|
+
self,
|
|
68
|
+
error_type: Type[Exception],
|
|
69
|
+
converter: Callable[[Exception], WebSocketError],
|
|
70
|
+
):
|
|
71
|
+
"""Register custom error mapping"""
|
|
72
|
+
self.error_mappings[error_type] = {"converter": converter}
|
|
73
|
+
|
|
74
|
+
async def handle_websocket_error(
|
|
75
|
+
self, websocket: WebSocket, error: Exception, request_id: Optional[str] = None
|
|
76
|
+
) -> bool:
|
|
77
|
+
"""
|
|
78
|
+
Handle WebSocket error
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
bool: True if connection should be closed
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
# Convert to WebSocketError
|
|
85
|
+
ws_error = self._convert_to_websocket_error(error, request_id)
|
|
86
|
+
|
|
87
|
+
# Log the error
|
|
88
|
+
self._log_error(ws_error)
|
|
89
|
+
|
|
90
|
+
# Send error response
|
|
91
|
+
await self._send_error_response(websocket, ws_error)
|
|
92
|
+
|
|
93
|
+
# Handle connection closing if needed
|
|
94
|
+
if ws_error.close_connection:
|
|
95
|
+
await self._close_connection(websocket, ws_error)
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
except Exception as handler_error:
|
|
101
|
+
logger.error(f"Error in error handler: {handler_error}")
|
|
102
|
+
# Try to close connection gracefully
|
|
103
|
+
try:
|
|
104
|
+
await websocket.close(code=1011, reason="Error handler failed")
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
def _convert_to_websocket_error(
|
|
110
|
+
self, error: Exception, request_id: Optional[str] = None
|
|
111
|
+
) -> WebSocketError:
|
|
112
|
+
"""Convert any exception to WebSocketError"""
|
|
113
|
+
|
|
114
|
+
# If already a WebSocketError, just update request_id
|
|
115
|
+
if isinstance(error, WebSocketError):
|
|
116
|
+
if request_id and not error.request_id:
|
|
117
|
+
error.request_id = request_id
|
|
118
|
+
return error
|
|
119
|
+
|
|
120
|
+
# If it's an AppError, convert it
|
|
121
|
+
if isinstance(error, AppError):
|
|
122
|
+
return WebSocketError.from_app_error(error, request_id)
|
|
123
|
+
|
|
124
|
+
# Check registered mappings
|
|
125
|
+
for error_type, mapping in self.error_mappings.items():
|
|
126
|
+
if isinstance(error, error_type):
|
|
127
|
+
ws_error = mapping["converter"](error)
|
|
128
|
+
if request_id and not ws_error.request_id:
|
|
129
|
+
ws_error.request_id = request_id
|
|
130
|
+
return ws_error
|
|
131
|
+
|
|
132
|
+
# Fallback to internal error
|
|
133
|
+
return WebSocketInternalError(
|
|
134
|
+
message="Unexpected error occurred",
|
|
135
|
+
original_error=str(error),
|
|
136
|
+
request_id=request_id,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def _log_error(self, error: WebSocketError):
|
|
140
|
+
"""Log error based on severity"""
|
|
141
|
+
if error.ws_error_code < -32600: # Standard JSON-RPC errors
|
|
142
|
+
logger.error(
|
|
143
|
+
f"WebSocket error: {error.message}",
|
|
144
|
+
extra={
|
|
145
|
+
"error_code": error.code.value,
|
|
146
|
+
"ws_error_code": error.ws_error_code,
|
|
147
|
+
"request_id": error.request_id,
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
else: # Custom errors
|
|
151
|
+
logger.warning(
|
|
152
|
+
f"WebSocket error: {error.message}",
|
|
153
|
+
extra={
|
|
154
|
+
"error_code": error.code.value,
|
|
155
|
+
"ws_error_code": error.ws_error_code,
|
|
156
|
+
"request_id": error.request_id,
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
async def _send_error_response(self, websocket: WebSocket, error: WebSocketError):
|
|
161
|
+
"""Send error response via WebSocket"""
|
|
162
|
+
try:
|
|
163
|
+
if getattr(websocket, "client_state", None) not in (
|
|
164
|
+
WebSocketState.CONNECTED,
|
|
165
|
+
):
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
error_response = json.dumps(error.to_jsonrpc_error())
|
|
169
|
+
await websocket.send_text(error_response)
|
|
170
|
+
except Exception as send_error:
|
|
171
|
+
logger.error(f"Failed to send error response: {send_error}")
|
|
172
|
+
|
|
173
|
+
async def _close_connection(self, websocket: WebSocket, error: WebSocketError):
|
|
174
|
+
"""Close WebSocket connection with proper code and reason"""
|
|
175
|
+
try:
|
|
176
|
+
# Wait a bit to ensure error message is delivered
|
|
177
|
+
await asyncio.sleep(0.1)
|
|
178
|
+
|
|
179
|
+
# Map error codes to WebSocket close codes
|
|
180
|
+
close_code = self._get_close_code(error)
|
|
181
|
+
close_reason = error.message[:123] # Max 123 bytes for close reason
|
|
182
|
+
|
|
183
|
+
await websocket.close(code=close_code, reason=close_reason)
|
|
184
|
+
if getattr(websocket, "client_state", None) not in (
|
|
185
|
+
WebSocketState.CONNECTED,
|
|
186
|
+
):
|
|
187
|
+
await websocket.close(code=close_code, reason=close_reason)
|
|
188
|
+
|
|
189
|
+
except Exception as close_error:
|
|
190
|
+
logger.error(f"Failed to close WebSocket: {close_error}")
|
|
191
|
+
|
|
192
|
+
def _get_close_code(self, error: WebSocketError) -> int:
|
|
193
|
+
"""Get appropriate WebSocket close code"""
|
|
194
|
+
from .exceptions import JSONRPCErrorCode
|
|
195
|
+
|
|
196
|
+
# Map error codes to WebSocket close codes
|
|
197
|
+
close_code_mapping = {
|
|
198
|
+
JSONRPCErrorCode.AUTH_REQUIRED: 1008, # Policy Violation
|
|
199
|
+
JSONRPCErrorCode.TOKEN_EXPIRED: 1008, # Policy Violation
|
|
200
|
+
JSONRPCErrorCode.RATE_LIMITED: 1008, # Policy Violation
|
|
201
|
+
JSONRPCErrorCode.INTERNAL_ERROR: 1011, # Internal Error
|
|
202
|
+
JSONRPCErrorCode.INVALID_REQUEST: 1003, # Unsupported Data
|
|
203
|
+
JSONRPCErrorCode.PARSE_ERROR: 1007, # Invalid frame payload data
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return close_code_mapping.get(error.ws_error_code, 1000) # Normal closure
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def setup_websocket_error_handling(app: FastAPI) -> WebSocketErrorHandler:
|
|
210
|
+
"""
|
|
211
|
+
Setup WebSocket error handling for FastAPI app
|
|
212
|
+
|
|
213
|
+
Usage:
|
|
214
|
+
ws_error_handler = setup_websocket_error_handling(app)
|
|
215
|
+
|
|
216
|
+
# In WebSocket endpoint:
|
|
217
|
+
try:
|
|
218
|
+
# ... handle message
|
|
219
|
+
except Exception as e:
|
|
220
|
+
should_close = await ws_error_handler.handle_websocket_error(
|
|
221
|
+
websocket, e, request_id
|
|
222
|
+
)
|
|
223
|
+
if should_close:
|
|
224
|
+
break
|
|
225
|
+
"""
|
|
226
|
+
handler = WebSocketErrorHandler()
|
|
227
|
+
|
|
228
|
+
# Store handler in app state for access
|
|
229
|
+
app.state.ws_error_handler = handler
|
|
230
|
+
|
|
231
|
+
return handler
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket exceptions extending awesome-errors for JSON-RPC 2.0 compatibility
|
|
3
|
+
|
|
4
|
+
This module provides WebSocket-specific error classes that integrate with
|
|
5
|
+
the existing awesome-errors system while adding JSON-RPC 2.0 support.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
from awesome_errors.core.error_codes import ErrorCode
|
|
11
|
+
from awesome_errors.core.exceptions import AppError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class JSONRPCErrorCode:
|
|
15
|
+
"""Standard JSON-RPC 2.0 error codes"""
|
|
16
|
+
|
|
17
|
+
# Standard codes defined by JSON-RPC 2.0 specification
|
|
18
|
+
PARSE_ERROR = -32700 # Invalid JSON was received
|
|
19
|
+
INVALID_REQUEST = -32600 # The JSON sent is not a valid Request object
|
|
20
|
+
METHOD_NOT_FOUND = -32601 # The method does not exist / is not available
|
|
21
|
+
INVALID_PARAMS = -32602 # Invalid method parameter(s)
|
|
22
|
+
INTERNAL_ERROR = -32603 # Internal JSON-RPC error
|
|
23
|
+
|
|
24
|
+
# Server error codes (reserved range: -32000 to -32099)
|
|
25
|
+
# Custom codes for WebSocket-specific errors
|
|
26
|
+
AUTH_REQUIRED = -32000 # Authentication required
|
|
27
|
+
TOKEN_EXPIRED = -32001 # Authentication token expired
|
|
28
|
+
RATE_LIMITED = -32002 # Rate limit exceeded
|
|
29
|
+
SUBSCRIPTION_DENIED = -32003 # Subscription permission denied
|
|
30
|
+
CONNECTION_LIMIT = -32004 # Too many connections
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class WebSocketError(AppError):
|
|
34
|
+
"""
|
|
35
|
+
WebSocket error extending AppError with JSON-RPC 2.0 compatibility
|
|
36
|
+
|
|
37
|
+
This class bridges awesome-errors with WebSocket/JSON-RPC requirements,
|
|
38
|
+
providing automatic error code mapping and connection management.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
code: ErrorCode,
|
|
44
|
+
message: str,
|
|
45
|
+
ws_error_code: Optional[int] = None,
|
|
46
|
+
request_id: Optional[str] = None,
|
|
47
|
+
details: Optional[Dict[str, Any]] = None,
|
|
48
|
+
close_connection: bool = False,
|
|
49
|
+
):
|
|
50
|
+
"""
|
|
51
|
+
Initialize WebSocket error
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
code: ErrorCode from awesome-errors
|
|
55
|
+
message: Human-readable error message
|
|
56
|
+
ws_error_code: JSON-RPC error code (auto-mapped if not provided)
|
|
57
|
+
request_id: JSON-RPC request ID for correlation
|
|
58
|
+
details: Additional error details
|
|
59
|
+
close_connection: Whether to close WebSocket connection after error
|
|
60
|
+
"""
|
|
61
|
+
super().__init__(code, message, details)
|
|
62
|
+
|
|
63
|
+
# WebSocket specific fields
|
|
64
|
+
self.ws_error_code = ws_error_code or self._map_to_jsonrpc_code(code)
|
|
65
|
+
self.request_id = request_id
|
|
66
|
+
self.close_connection = close_connection
|
|
67
|
+
|
|
68
|
+
def _map_to_jsonrpc_code(self, error_code: ErrorCode) -> int:
|
|
69
|
+
"""
|
|
70
|
+
Map ErrorCode to JSON-RPC error code
|
|
71
|
+
|
|
72
|
+
Provides sensible defaults for mapping awesome-errors codes
|
|
73
|
+
to JSON-RPC 2.0 standard codes.
|
|
74
|
+
"""
|
|
75
|
+
mapping = {
|
|
76
|
+
# Authentication errors
|
|
77
|
+
ErrorCode.AUTH_REQUIRED: JSONRPCErrorCode.AUTH_REQUIRED,
|
|
78
|
+
ErrorCode.AUTH_PERMISSION_DENIED: JSONRPCErrorCode.AUTH_REQUIRED,
|
|
79
|
+
ErrorCode.AUTH_TOKEN_EXPIRED: JSONRPCErrorCode.TOKEN_EXPIRED,
|
|
80
|
+
ErrorCode.AUTH_INVALID_TOKEN: JSONRPCErrorCode.AUTH_REQUIRED,
|
|
81
|
+
# Validation errors
|
|
82
|
+
ErrorCode.VALIDATION_FAILED: JSONRPCErrorCode.INVALID_PARAMS,
|
|
83
|
+
ErrorCode.INVALID_INPUT: JSONRPCErrorCode.INVALID_PARAMS,
|
|
84
|
+
ErrorCode.MISSING_REQUIRED_FIELD: JSONRPCErrorCode.INVALID_PARAMS,
|
|
85
|
+
# Resource errors
|
|
86
|
+
ErrorCode.RESOURCE_NOT_FOUND: JSONRPCErrorCode.METHOD_NOT_FOUND,
|
|
87
|
+
ErrorCode.RESOURCE_ALREADY_EXISTS: JSONRPCErrorCode.INVALID_REQUEST,
|
|
88
|
+
# Rate limiting
|
|
89
|
+
ErrorCode.RATE_LIMIT_EXCEEDED: JSONRPCErrorCode.RATE_LIMITED,
|
|
90
|
+
# Database errors (all map to internal error)
|
|
91
|
+
ErrorCode.DB_CONNECTION_ERROR: JSONRPCErrorCode.INTERNAL_ERROR,
|
|
92
|
+
ErrorCode.DB_QUERY_ERROR: JSONRPCErrorCode.INTERNAL_ERROR,
|
|
93
|
+
ErrorCode.DB_CONSTRAINT_VIOLATION: JSONRPCErrorCode.INVALID_PARAMS,
|
|
94
|
+
# Business logic
|
|
95
|
+
ErrorCode.BUSINESS_RULE_VIOLATION: JSONRPCErrorCode.INVALID_REQUEST,
|
|
96
|
+
# Generic errors
|
|
97
|
+
ErrorCode.INTERNAL_ERROR: JSONRPCErrorCode.INTERNAL_ERROR,
|
|
98
|
+
ErrorCode.SERVICE_UNAVAILABLE: JSONRPCErrorCode.INTERNAL_ERROR,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return mapping.get(error_code, JSONRPCErrorCode.INTERNAL_ERROR)
|
|
102
|
+
|
|
103
|
+
def to_jsonrpc_error(self) -> Dict[str, Any]:
|
|
104
|
+
"""
|
|
105
|
+
Convert to JSON-RPC 2.0 error format
|
|
106
|
+
|
|
107
|
+
Returns a dictionary conforming to JSON-RPC 2.0 error object specification.
|
|
108
|
+
"""
|
|
109
|
+
error_data: Dict[str, Any] | None = None
|
|
110
|
+
|
|
111
|
+
# Build error data if we have details or metadata
|
|
112
|
+
if self.details or self.request_id:
|
|
113
|
+
error_data = {}
|
|
114
|
+
|
|
115
|
+
if self.details:
|
|
116
|
+
error_data["details"] = self.details
|
|
117
|
+
|
|
118
|
+
# Include original error code for clients that understand it
|
|
119
|
+
error_data["error_code"] = self.code.value
|
|
120
|
+
|
|
121
|
+
# Include timestamp for debugging
|
|
122
|
+
error_data["timestamp"] = self.timestamp.isoformat() + "Z"
|
|
123
|
+
|
|
124
|
+
# Include request ID if available
|
|
125
|
+
if self.request_id:
|
|
126
|
+
error_data["request_id"] = self.request_id
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
"jsonrpc": "2.0",
|
|
130
|
+
"error": {
|
|
131
|
+
"code": self.ws_error_code,
|
|
132
|
+
"message": self.message,
|
|
133
|
+
"data": error_data,
|
|
134
|
+
},
|
|
135
|
+
"id": self.request_id,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def from_app_error(
|
|
140
|
+
cls,
|
|
141
|
+
error: AppError,
|
|
142
|
+
request_id: Optional[str] = None,
|
|
143
|
+
close_connection: bool = False,
|
|
144
|
+
) -> "WebSocketError":
|
|
145
|
+
"""
|
|
146
|
+
Create WebSocketError from existing AppError
|
|
147
|
+
|
|
148
|
+
This allows seamless conversion of any AppError to WebSocket format.
|
|
149
|
+
"""
|
|
150
|
+
return cls(
|
|
151
|
+
code=error.code,
|
|
152
|
+
message=error.message,
|
|
153
|
+
details=error.details,
|
|
154
|
+
request_id=request_id,
|
|
155
|
+
close_connection=close_connection,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class WebSocketAuthError(WebSocketError):
|
|
160
|
+
"""Authentication error - always closes connection"""
|
|
161
|
+
|
|
162
|
+
def __init__(
|
|
163
|
+
self,
|
|
164
|
+
message: str = "Authentication required",
|
|
165
|
+
request_id: Optional[str] = None,
|
|
166
|
+
details: Optional[Dict[str, Any]] = None,
|
|
167
|
+
):
|
|
168
|
+
super().__init__(
|
|
169
|
+
code=ErrorCode.AUTH_REQUIRED,
|
|
170
|
+
message=message,
|
|
171
|
+
ws_error_code=JSONRPCErrorCode.AUTH_REQUIRED,
|
|
172
|
+
request_id=request_id,
|
|
173
|
+
details=details,
|
|
174
|
+
close_connection=True, # Always close on auth failure
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class WebSocketTokenExpiredError(WebSocketError):
|
|
179
|
+
"""Token expired error - requires re-authentication"""
|
|
180
|
+
|
|
181
|
+
def __init__(
|
|
182
|
+
self, request_id: Optional[str] = None, details: Optional[Dict[str, Any]] = None
|
|
183
|
+
):
|
|
184
|
+
super().__init__(
|
|
185
|
+
code=ErrorCode.AUTH_TOKEN_EXPIRED,
|
|
186
|
+
message="Token expired",
|
|
187
|
+
ws_error_code=JSONRPCErrorCode.TOKEN_EXPIRED,
|
|
188
|
+
request_id=request_id,
|
|
189
|
+
details=details,
|
|
190
|
+
close_connection=True, # Force reconnection with new token
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class WebSocketRateLimitError(WebSocketError):
|
|
195
|
+
"""Rate limit exceeded error"""
|
|
196
|
+
|
|
197
|
+
def __init__(
|
|
198
|
+
self,
|
|
199
|
+
retry_after: int,
|
|
200
|
+
request_id: Optional[str] = None,
|
|
201
|
+
limit: Optional[int] = None,
|
|
202
|
+
window: Optional[int] = None,
|
|
203
|
+
):
|
|
204
|
+
details: Dict[str, Any] = {
|
|
205
|
+
"retry_after": retry_after,
|
|
206
|
+
"limit": limit,
|
|
207
|
+
"window": window,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
super().__init__(
|
|
211
|
+
code=ErrorCode.RATE_LIMIT_EXCEEDED,
|
|
212
|
+
message=f"Rate limit exceeded. Retry after {retry_after} seconds",
|
|
213
|
+
ws_error_code=JSONRPCErrorCode.RATE_LIMITED,
|
|
214
|
+
details={k: v for k, v in details.items() if v is not None},
|
|
215
|
+
request_id=request_id,
|
|
216
|
+
close_connection=False, # Don't close, client can retry
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class WebSocketValidationError(WebSocketError):
|
|
221
|
+
"""Validation error with field details"""
|
|
222
|
+
|
|
223
|
+
def __init__(
|
|
224
|
+
self,
|
|
225
|
+
message: str,
|
|
226
|
+
validation_errors: Optional[list[Any]] = None,
|
|
227
|
+
request_id: Optional[str] = None,
|
|
228
|
+
):
|
|
229
|
+
details: Dict[str, Any] = {}
|
|
230
|
+
if validation_errors:
|
|
231
|
+
details["validation_errors"] = validation_errors
|
|
232
|
+
|
|
233
|
+
super().__init__(
|
|
234
|
+
code=ErrorCode.VALIDATION_FAILED,
|
|
235
|
+
message=message,
|
|
236
|
+
ws_error_code=JSONRPCErrorCode.INVALID_PARAMS,
|
|
237
|
+
details=details if details else None,
|
|
238
|
+
request_id=request_id,
|
|
239
|
+
close_connection=False,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class WebSocketMethodNotFoundError(WebSocketError):
|
|
244
|
+
"""Method not found error"""
|
|
245
|
+
|
|
246
|
+
def __init__(
|
|
247
|
+
self,
|
|
248
|
+
method: str,
|
|
249
|
+
request_id: Optional[str] = None,
|
|
250
|
+
available_methods: Optional[list[str]] = None,
|
|
251
|
+
):
|
|
252
|
+
details: Dict[str, Any] = {"method": method}
|
|
253
|
+
if available_methods:
|
|
254
|
+
details["available_methods"] = available_methods
|
|
255
|
+
|
|
256
|
+
super().__init__(
|
|
257
|
+
code=ErrorCode.RESOURCE_NOT_FOUND,
|
|
258
|
+
message=f"Method not found: {method}",
|
|
259
|
+
ws_error_code=JSONRPCErrorCode.METHOD_NOT_FOUND,
|
|
260
|
+
details=details,
|
|
261
|
+
request_id=request_id,
|
|
262
|
+
close_connection=False,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class WebSocketInternalError(WebSocketError):
|
|
267
|
+
"""Internal server error"""
|
|
268
|
+
|
|
269
|
+
def __init__(
|
|
270
|
+
self,
|
|
271
|
+
message: str = "Internal server error",
|
|
272
|
+
request_id: Optional[str] = None,
|
|
273
|
+
original_error: Optional[str] = None,
|
|
274
|
+
):
|
|
275
|
+
details: Dict[str, Any] = {}
|
|
276
|
+
if original_error:
|
|
277
|
+
details["original_error"] = str(original_error)
|
|
278
|
+
|
|
279
|
+
super().__init__(
|
|
280
|
+
code=ErrorCode.INTERNAL_ERROR,
|
|
281
|
+
message=message,
|
|
282
|
+
ws_error_code=JSONRPCErrorCode.INTERNAL_ERROR,
|
|
283
|
+
details=details if details else None,
|
|
284
|
+
request_id=request_id,
|
|
285
|
+
close_connection=False,
|
|
286
|
+
)
|