Skip to main content

Errium – Intelligent API Error Normalization for FastAPI

PyPI version Python versions License: MIT CI Docs

Errium is a lightweight, framework-agnostic error normalization and translation middleware for modern APIs. It intercepts uncaught exceptions, HTTP exceptions, and request validation errors, standardizing them into clean, consistent, and frontend-safe JSON responses.

📖 Read the documentation → errium.dagmawi.et — installation, per-framework guides, core concepts, and architecture.


⚡ The Problem

Building APIs with modern frameworks (like FastAPI) often yields inconsistent error responses, causing friction for frontend teams:

  • Ugly FastAPI Validation Errors: Deeply nested, verbose, and difficult to parse direct Pydantic formats.
  • Inconsistent Backend Responses: Uncaught internal exceptions return unhandled stack trace leaks or plain text errors depending on where they occurred.
  • Frontend Integration Pain: Frontend engineers are forced to write custom parsers for every microservice, parsing varying response layouts.

🚀 The Solution

Errium provides a unified error classification, normalization, and formatting pipeline:

  1. Intelligent Middleware (ErriumMiddleware): Transparently intercepts all request lifecycles.
  2. Classification Engine (ClassificationEngine): Dynamically resolves error categories, status codes, and user-facing messages.
  3. Beautification & Normalization: Transforms nested, complex errors into flat, friendly key-value details.
  4. Environment-Aware Sanitization: Exposes detailed backtrace logs in development and secures system internals in production.

🌟 Features

  • 🟢 Unified Error Format: Every single API error response uses the exact same structured JSON contract.
  • 💅 Validation Beautifier: Automatically maps common validation types (e.g. missing, invalid emails, nulls) into clean, capitalized localized messages.
  • 🆔 Trace IDs: Seamlessly correlates client-facing responses with server-side application logs.
  • 🛡️ Dev vs. Prod Mode: Exposes traceback objects, raw exception names, and actionable debug hints in development, while sanitizing server details in production.
  • 🔌 Extensible Plugin Classifier: Register custom classifiers with sorting priority evaluation.

📦 Installation

Errium is published on PyPI and requires Python 3.11+.

uv pip install errium
# Or using traditional pip
pip install errium

The base install ships the core plus the FastAPI adapter. Each other framework is an extra:

uv pip install "errium[flask]"   # Flask
uv pip install "errium[ninja]"   # Django Ninja
uv pip install "errium[drf]"     # Django REST Framework
uv pip install "errium[django]"  # errium_django helpers only (error pages outside Ninja/DRF)

Note: the base install requires fastapi/starlette, so the Flask, Ninja and DRF extras install them too. Their adapters never import FastAPI at runtime; it is install footprint only.

Note: Errium depends on fastapi, not fastapi[standard], so it stays light for Flask and Django users. If you were relying on Errium to pull in a server, install one yourself (uv pip install uvicorn).

All five packages ship a py.typed marker, so type checkers see Errium's annotations directly.


🛠️ Usage Example

Integrating Errium into your FastAPI codebase takes one line:

from fastapi import FastAPI
import errium

app = FastAPI()

# Adds the middleware (uncaught exceptions) and registers handlers for
# HTTPException (including routing 404/405) and request validation errors.
errium.install(app)

# Your endpoints go here...

errium.install(app) is equivalent to the manual wiring below, which still works but also needs the HTTPException handler to normalise HTTPExceptions (Starlette resolves those inside user middleware, so the middleware alone never sees them):

from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException
from errium import ErriumMiddleware
from errium.handlers.http_handler import http_exception_handler
from errium.handlers.validation_handler import validation_exception_handler

app.add_middleware(ErriumMiddleware)
app.add_exception_handler(HTTPException, http_exception_handler)
app.add_exception_handler(RequestValidationError, validation_exception_handler)

Mounted sub-apps (app.mount("/v1", v1_app)) do not inherit the parent's middleware or handlers: call errium.install(v1_app) on each mounted app.

A structured HTTPException(detail={...}) / detail=[...] keeps message as the HTTP phrase (e.g. "Bad Request") and exposes the value in details (dict entries as {key: str(value)}, list items as {"0": ..., "1": ...}). Headers set on the exception (Retry-After, WWW-Authenticate, X-*) are preserved.

Flask is supported too, via the errium[flask] extra:

uv pip install "errium[flask]"
from flask import Flask
from errium_flask import ErriumFlask

app = Flask(__name__)

# Registers a single catch-all error handler covering HTTP exceptions,
# uncaught exceptions, and pydantic validation errors.
ErriumFlask(app)

# Your routes go here...

Django Ninja is supported too, via the errium[ninja] extra:

uv pip install "errium[ninja]"
from ninja import NinjaAPI
from errium_ninja import register_errium

api = NinjaAPI()

# Registers handlers covering Ninja's ValidationError, HttpError (and its
# AuthenticationError/AuthorizationError/Throttled subclasses), Django's
# Http404, and generic exceptions.
register_errium(api)

# Your endpoints go here...

Django REST Framework is supported too, via the errium[drf] extra:

uv pip install "errium[drf]"
# settings.py
REST_FRAMEWORK = {
    "EXCEPTION_HANDLER": "errium_drf.errium_exception_handler",
}

That's it — every exception raised inside a DRF view (APIException family, Django's Http404/ PermissionDenied, and any other uncaught exception) now returns Errium's standardized response. Note: validation errors keep DRF's own 400 status code rather than the 422 the other adapters use, matching DRF's established convention.

Configuration

Variable Effect
ERRIUM_DEBUG true/1/yes/on (case-insensitive, whitespace ignored) adds a debug block and stops sanitizing 5xx messages. Anything else stays off. The mode is logged once at import (INFO on the errium logger), with a WARNING if debug is on while ERRIUM_ENV/APP_ENV/ENVIRONMENT/ENV says production.
ERRIUM_TRUST_REQUEST_ID Reuse a valid inbound X-Request-ID as the trace_id.

errium.__version__ (and errium_core, errium_flask, errium_ninja, errium_drf, errium_django) report the installed version.

FastAPI middleware order (CORS)

Starlette runs middleware in reverse order of registration: the one added last is outermost. Add CORSMiddleware after Errium so it wraps it; otherwise an uncaught-exception 500 is produced outside CORS and the browser reports a CORS failure instead of showing the JSON body.

errium.install(app)                    # adds ErriumMiddleware
app.add_middleware(CORSMiddleware, allow_origins=["https://app.example.com"])  # after

Known limitations

  • Mid-stream failures cannot be normalized. If a StreamingResponse / StreamingHttpResponse generator raises after the first chunk, the status line and headers are already sent; the connection just ends. A generator failing before its first byte on FastAPI still returns the 200 it started with. Validate and fetch what you can before returning a streaming response.
  • Mounted FastAPI sub-apps need their own errium.install(sub_app).
  • For Django, errors raised before Django middleware runs (or when DEBUG = True) may still render Django's own page; see "Django project setup" above.

Custom classifiers

Map your own exception types to a status, category and message. The one-liner works on every adapter at once (it is a process-wide registry consulted by all engines):

import errium

class OutOfStock(Exception): ...

errium.register(OutOfStock, status=409, message="That item is out of stock.")

# or, as a class decorator (category defaults to the standard one for the status):
@errium.register(status=429, category="RATE_LIMITED")
class SlowDown(Exception): ...

For full control, write a classifier (priority property + classify(exc) -> ClassifiedError | None) and pass it to the adapter. Higher priority runs first; built-in adapter classifiers are 200+, register(...) uses 300.

Adapter How to pass classifiers
FastAPI errium.install(app, classifiers=[...]), or app.add_middleware(ErriumMiddleware, classifiers=[...])
Flask ErriumFlask(app, classifiers=[...])
Django Ninja register_errium(api, classifiers=[...])
DRF REST_FRAMEWORK["EXCEPTION_HANDLER"] = "errium_drf.errium_exception_handler" plus ERRIUM = {"CLASSIFIERS": ["myapp.errors.QuotaClassifier"]}, or make_exception_handler(classifiers=[...])
errium_django ERRIUM = {"CLASSIFIERS": [...]} in Django settings

Every adapter also takes engine= to replace the default ClassificationEngine outright (build_engine(...) in each adapter package builds the default one). On FastAPI, one engine is shared by the middleware and both exception handlers.

Trace IDs and logging

Every error response carries a trace_id (also sent as an X-Trace-Id response header), and Errium logs each handled exception on the "errium" logger — ERROR with the traceback for 5xx, a one-line WARNING for 4xx — with that same trace_id in the message, so a client-reported id can be matched to a log line. Errium never configures logging handlers; attach your own:

import logging
logging.getLogger("errium").addHandler(logging.StreamHandler())

To use the id in your own code: request.state.errium_trace_id (FastAPI), flask.g.errium_trace_id (Flask), request.errium_trace_id (Django Ninja / DRF), or errium_core.tracing.current_trace_id() anywhere in the request's context. Set ERRIUM_TRUST_REQUEST_ID=true to reuse a valid inbound X-Request-ID (≤128 characters of letters, digits and ._:-) as the trace_id; by default a fresh uuid4 is generated.

Django project setup: errors outside the view layer

Ninja's and DRF's handlers only see exceptions raised inside their views. An unknown URL, a URL converter mismatch (/item/abc/ for <int:pk>), Ninja's own 405, and an exception in a Django middleware are rendered by Django itself as HTML. Cover them with the errium_django helpers (pip install "errium[django]", or the ninja/drf extras):

# urls.py (root URLconf)
handler400 = "errium_django.handler400"
handler403 = "errium_django.handler403"
handler404 = "errium_django.handler404"
handler500 = "errium_django.handler500"

# settings.py
MIDDLEWARE = [
    "errium_django.ErriumDjangoMiddleware",  # first, so it wraps everything below
    # ...
]
ERRIUM_JSON_PATH_PREFIXES = ["/api/"]  # optional; this is the default

A request gets the JSON envelope only if it prefers JSON (Accept: application/json) or its path starts with one of ERRIUM_JSON_PATH_PREFIXES; browsers and other HTML clients still get Django's normal error pages. ErriumDjangoMiddleware also converts exceptions raised by later middleware and plain Django views, and turns a bare 405 (Ninja's method check) into the envelope while keeping its Allow header. Exceptions in middleware listed before it are only covered by handler500. Django's technical 500 page replaces handler500 while DEBUG = True.


🔍 Before vs. After

❌ Before Errium (Ugly FastAPI Validation)

{
  "detail": [
    {
      "type": "missing",
      "loc": [
        "body",
        "password"
      ],
      "msg": "Field required",
      "input": null
    }
  ]
}

After Errium (Cleaned, Beautified Response)

{
  "success": false,
  "status_code": 422,
  "code": "VALIDATION_ERROR",
  "message": "Validation failed.",
  "trace_id": "87b003a8-7c15-4a6c-9c76-a05b22b109e2",
  "timestamp": "2026-05-27T12:00:00Z",
  "details": {
    "password": "Password is required."
  }
}

🗺️ Roadmap

Errium is designed framework-agnostically at the core. We are planning the following integrations:

  • Flask Adapter Layer
  • Django Ninja Adapter Layer
  • Django REST Framework Adapter Layer
  • Express.js Adapter Layer (JavaScript port)
  • AI-Powered Developer Suggestions & Self-Healing Hints

See the roadmap (or ROADMAP.md) for more detail on what's done and what's planned.

Metadata

Release files for errium 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for errium 0.2.0
File Size Uploaded
errium-0.2.0.tar.gz 150.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for errium 0.2.0
File Interpreter ABI Platform
errium-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 201.2 kB

Release files / errium-0.2.0.tar.gz

Download URL errium-0.2.0.tar.gz
Size 150.0 kB
Tags Source
SHA-256 checksum
How to use checksums
47fbe3495b7bcc7b6171deca5fe037d3c4cbb90481304913676abf21a471c557
BLAKE2b-256 checksum
How to use checksums
a12274dff7e853ee4fac16d67d3174ebf0a082a31e25e755cd6856c1dcd1a94c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.13

Release files / errium-0.2.0-py3-none-any.whl

Download URL errium-0.2.0-py3-none-any.whl
Size 51.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cd42539079903089f30a19591b73b517d06120940bb898bdfc271d3e23834288
BLAKE2b-256 checksum
How to use checksums
17c9f7e20d3c32c8c6d6ffb410be7b5b022c6435d01d45efd3179cbc90fd13c1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.13

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page