Skip to content
errium

Django REST Framework

A settings hook with no middleware, and the 400-vs-422 decision.

pip install "errium[drf]"

Integration

No middleware, no registration call — just a settings key:

settings.py
REST_FRAMEWORK = {
    "EXCEPTION_HANDLER": "errium_drf.errium_exception_handler",
}

errium_exception_handler(exc, context) is a plain function. DRF resolves it from that dotted path.

To add your own classifiers, list them in settings (dotted paths; a class is instantiated with no arguments), or build a handler bound to its own engine:

settings.py
ERRIUM = {"CLASSIFIERS": ["myapp.classifiers.QuotaExceededClassifier"]}
from errium_drf import make_exception_handler

errium_exception_handler = make_exception_handler(classifiers=[...])  # or engine=...

See custom classifiers.

The 400-vs-422 decision

Full coverage from a settings hook alone

APIView.dispatch() wraps every view call in except Exception: and routes it to handle_exception(), which calls the configured exception handler.

DRF’s own default handler returns None for anything that isn’t an APIException, Http404, or PermissionDenied — and a None return makes handle_exception re-raise into Django’s plain 500 handling.

errium_exception_handler never returns None. The GenericExceptionClassifier fallback covers everything the specific classifiers don’t, so an uncaught KeyError in a view gets the standard contract like anything else. That is how the adapter reaches catch-everything parity with the other three through the settings hook alone, with no extra middleware.

Headers

APIView.handle_exception sets auth_header and wait on the exception before calling the handler. Errium mirrors DRF’s own behaviour and turns those into WWW-Authenticate and Retry-After response headers, so auth challenges and throttling keep working as DRF clients expect. Every error response also carries X-Trace-Id, and request.errium_trace_id is available in your views.

Classifiers

ClassifierPriorityHandles
DRFValidationErrorClassifier210DRF ValidationError, keeping its own status
DRFAPIExceptionClassifier200The APIException family
DjangoHttp404Classifier200Http404
DjangoPermissionDeniedClassifier200Django’s PermissionDenied → 403
DjangoExceptionClassifier190SuspiciousOperation → 400, RequestDataTooBig → 413, Django ValidationError → 400, ObjectDoesNotExist → 404

Note the last one is Django’s own django.core.exceptions.PermissionDenied, which is distinct from DRF’s PermissionDenied — the latter is an APIException and is handled by the row above it.

Validation details

DRF’s ValidationError.detail is a recursive tree of already human-readable ErrorDetail strings, which is a different shape from Pydantic’s loc/type/msg list. So this adapter does not use ValidationNormalizer — it has its own flatten_drf_errors, which walks that tree and flattens it to the same {field: message} map: several messages for one field are joined with "; ", and DRF’s non_field_errors becomes the key _. See Validation.

Database errors from Django’s ORM are classified too: a unique violation is 409 DUPLICATE_RESOURCE, other django.db errors are DATABASE_ERROR.

A body larger than Django’s DATA_UPLOAD_MAX_MEMORY_SIZE raises RequestDataTooBig, which is now a 413 PAYLOAD_TOO_LARGE rather than a 500.