Skip to content
errium

Error categories

The seventeen categories, and how status codes map onto them.

ErrorCategory is a StrEnum defining the closed set of values the response’s code field can take. Seventeen categories, and nothing else.

from enum import StrEnum


class ErrorCategory(StrEnum):
    VALIDATION = "VALIDATION_ERROR"
    AUTHENTICATION = "AUTHENTICATION_ERROR"
    AUTHORIZATION = "AUTHORIZATION_ERROR"
    NOT_FOUND = "RESOURCE_NOT_FOUND"
    DUPLICATE = "DUPLICATE_RESOURCE"
    DATABASE = "DATABASE_ERROR"
    INTERNAL = "INTERNAL_SERVER_ERROR"
    BAD_REQUEST = "BAD_REQUEST"
    METHOD_NOT_ALLOWED = "METHOD_NOT_ALLOWED"
    NOT_ACCEPTABLE = "NOT_ACCEPTABLE"
    TIMEOUT = "TIMEOUT"
    CONFLICT = "CONFLICT"
    GONE = "GONE"
    PAYLOAD_TOO_LARGE = "PAYLOAD_TOO_LARGE"
    UNSUPPORTED_MEDIA_TYPE = "UNSUPPORTED_MEDIA_TYPE"
    RATE_LIMITED = "RATE_LIMITED"
    SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE"

The categories

code valueMemberTypical status
VALIDATION_ERRORVALIDATION422 (400 on DRF)
AUTHENTICATION_ERRORAUTHENTICATION401
AUTHORIZATION_ERRORAUTHORIZATION403
RESOURCE_NOT_FOUNDNOT_FOUND404
DUPLICATE_RESOURCEDUPLICATE409 (database uniqueness violations)
DATABASE_ERRORDATABASE500
INTERNAL_SERVER_ERRORINTERNAL500
BAD_REQUESTBAD_REQUEST400
METHOD_NOT_ALLOWEDMETHOD_NOT_ALLOWED405
NOT_ACCEPTABLENOT_ACCEPTABLE406
TIMEOUTTIMEOUT408, 504
CONFLICTCONFLICT409
GONEGONE410
PAYLOAD_TOO_LARGEPAYLOAD_TOO_LARGE413
UNSUPPORTED_MEDIA_TYPEUNSUPPORTED_MEDIA_TYPE415
RATE_LIMITEDRATE_LIMITED429
SERVICE_UNAVAILABLESERVICE_UNAVAILABLE502, 503

Status code mapping

category_for_status_code() is the shared table every HTTP exception classifier uses — FastAPI, Flask, Ninja and DRF alike. Defining it once in the framework-agnostic core is what keeps the four adapters consistent.

StatusCategory
400BAD_REQUEST
401AUTHENTICATION_ERROR
403AUTHORIZATION_ERROR
404RESOURCE_NOT_FOUND
405METHOD_NOT_ALLOWED
406NOT_ACCEPTABLE
408TIMEOUT
409CONFLICT
410GONE
413PAYLOAD_TOO_LARGE
415UNSUPPORTED_MEDIA_TYPE
422VALIDATION_ERROR
429RATE_LIMITED
502, 503SERVICE_UNAVAILABLE
504TIMEOUT
any other 4xxBAD_REQUEST
any other statusINTERNAL_SERVER_ERROR

The status code is never changed by the mapping. A 418 becomes BAD_REQUEST and still reports status_code: 418; the category only describes it.

Categories not reached by status

DATABASE_ERROR and DUPLICATE_RESOURCE are produced by DatabaseExceptionClassifier, which inspects the exception itself rather than a status code. A uniqueness violation becomes DUPLICATE_RESOURCE with a 409; any other database error becomes DATABASE_ERROR with a 500. It recognises SQLAlchemy, Django’s ORM and the common drivers — see Core building blocks.

That is why a plain HTTP 409 (an HTTPException(409), say) is CONFLICT while a database unique violation is DUPLICATE_RESOURCE: the first is a status code, the second is a diagnosis.

What production keeps

In production a response with status 500 or above has its message replaced with a generic one. 502, 503 and 504 keep their SERVICE_UNAVAILABLE / TIMEOUT code so a client can still react to “the upstream is down”; every other 5xx becomes INTERNAL_SERVER_ERROR. See Debug vs. production.