errors.py 3.7 KB

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