errors.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. from __future__ import annotations
  2. from enum import StrEnum
  3. from typing import Any
  4. class ErrorCode(StrEnum):
  5. RUN_START_FAILED = "RUN_START_FAILED"
  6. RUN_NOT_FOUND = "RUN_NOT_FOUND"
  7. INVALID_REQUEST = "INVALID_REQUEST"
  8. INVALID_SOURCE = "INVALID_SOURCE"
  9. DB_CONFIG_MISSING = "DB_CONFIG_MISSING"
  10. DB_CONNECTION_FAILED = "DB_CONNECTION_FAILED"
  11. DB_PERMISSION_DENIED = "DB_PERMISSION_DENIED"
  12. DB_SCHEMA_NOT_READY = "DB_SCHEMA_NOT_READY"
  13. RUNTIME_WRITE_FAILED = "RUNTIME_WRITE_FAILED"
  14. POLICY_BUNDLE_NOT_FOUND = "POLICY_BUNDLE_NOT_FOUND"
  15. PLATFORM_CONFIG_MISSING = "PLATFORM_CONFIG_MISSING"
  16. PLATFORM_REQUEST_FAILED = "PLATFORM_REQUEST_FAILED"
  17. QUERY_GENERATION_FAILED = "QUERY_GENERATION_FAILED"
  18. SENSITIVE_KEYS = {
  19. "password",
  20. "token",
  21. "access_token",
  22. "refresh_token",
  23. "api_key",
  24. "apikey",
  25. "secret",
  26. "dsn",
  27. "authorization",
  28. "cookie",
  29. "session",
  30. "credential",
  31. }
  32. class ContentAgentError(Exception):
  33. def __init__(
  34. self,
  35. error_code: ErrorCode | str,
  36. message: str,
  37. detail: dict[str, Any] | None = None,
  38. status_code: int = 500,
  39. ) -> None:
  40. super().__init__(message)
  41. self.error_code = ErrorCode(error_code)
  42. self.message = message
  43. self.detail = sanitize_error_detail(detail or {})
  44. self.status_code = status_code
  45. def response_detail(self) -> dict[str, Any]:
  46. return error_response(self.error_code, self.message, self.detail)
  47. def error_response(
  48. error_code: ErrorCode | str,
  49. message: str,
  50. detail: dict[str, Any] | None = None,
  51. ) -> dict[str, Any]:
  52. return {
  53. "error_code": ErrorCode(error_code).value,
  54. "message": message,
  55. "detail": sanitize_error_detail(detail or {}),
  56. }
  57. def sanitize_error_detail(value: Any) -> Any:
  58. if isinstance(value, dict):
  59. result: dict[str, Any] = {}
  60. for key, item in value.items():
  61. lowered = str(key).lower()
  62. if lowered in SENSITIVE_KEYS:
  63. result[key] = "<redacted>"
  64. else:
  65. result[key] = sanitize_error_detail(item)
  66. return result
  67. if isinstance(value, list):
  68. return [sanitize_error_detail(item) for item in value]
  69. if isinstance(value, tuple):
  70. return [sanitize_error_detail(item) for item in value]
  71. return value
  72. def error_from_exception(
  73. exc: Exception,
  74. *,
  75. default_code: ErrorCode = ErrorCode.RUN_START_FAILED,
  76. detail: dict[str, Any] | None = None,
  77. ) -> ContentAgentError:
  78. if isinstance(exc, ContentAgentError):
  79. return exc
  80. return ContentAgentError(
  81. default_code,
  82. _safe_message(default_code),
  83. {
  84. **(detail or {}),
  85. "exception_type": type(exc).__name__,
  86. },
  87. )
  88. def _safe_message(error_code: ErrorCode) -> str:
  89. messages = {
  90. ErrorCode.RUN_START_FAILED: "run start failed",
  91. ErrorCode.RUNTIME_WRITE_FAILED: "runtime write failed",
  92. ErrorCode.INVALID_SOURCE: "invalid source",
  93. ErrorCode.INVALID_REQUEST: "invalid request",
  94. ErrorCode.POLICY_BUNDLE_NOT_FOUND: "policy bundle not found",
  95. ErrorCode.PLATFORM_CONFIG_MISSING: "platform config missing",
  96. ErrorCode.PLATFORM_REQUEST_FAILED: "platform request failed",
  97. ErrorCode.QUERY_GENERATION_FAILED: "query generation failed",
  98. }
  99. return messages.get(error_code, error_code.value.lower())