59 lines
1.7 KiB
Python
59 lines
1.7 KiB
Python
"""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
|