"""Safe, JSON:API-compatible exception handling.""" import logging import uuid from rest_framework.exceptions import APIException from rest_framework_json_api.exceptions import exception_handler as jsonapi_exception_handler logger = logging.getLogger(__name__) ERROR_ID_HEADER = "X-Error-ID" class APIConflictException(APIException): """A deliberately raised, non-sensitive resource conflict.""" status_code = 409 default_detail = "The request conflicts with the current resource state." default_code = "conflict" class InternalServerError(APIException): """Sanitized representation of an unexpected server exception.""" status_code = 500 default_detail = "An unexpected server error occurred." default_code = "internal_error" def hardened_exception_handler(exc, context): """Render expected errors normally and sanitize unexpected exceptions. Unexpected exception details and tracebacks are logged server-side under a generated error identifier. Only that opaque identifier is returned to the client, in a response header, so operators can correlate a report safely. """ response = jsonapi_exception_handler(exc, context) if response is not None: return response error_id = uuid.uuid4().hex logger.error( "Unhandled API exception; error_id=%s", error_id, exc_info=exc, ) response = jsonapi_exception_handler(InternalServerError(), context) response[ERROR_ID_HEADER] = error_id return response class HardenedExceptionHandlingMixin: """Opt a DRF view into :func:`hardened_exception_handler`.""" def get_exception_handler(self): return hardened_exception_handler