Dispatch flow
Every request goes through the same pipeline. Knowing the order makes it obvious where to plug in custom logic.
Pipeline
- Client IP β
get_client_ip(request)readsREMOTE_ADDR. If the proxy is inTRUSTED_PROXIES, falls back toX-Real-IPthenX-Forwarded-For. - Rate limit / abuse β
_enforce_rate_limit. Blocks IPs already flagged, applies the per-route limits and triggers a 24h block on abuse. - Authentication β
_authenticate. TriesAuthorization: Bearerfirst (whenBEARER_RESOLVERis configured), thenX-Api-Key, then session cookie. Setsself.userandself.account. - Token (optional) β
_enforce_token. Only whenENFORCE_TOKEN=True, validatesX-Tokenagainst the session token. - Origin (optional) β
get_allowed_domain. WhenALLOWED_ORIGINSis set, requires the requestOrigin(withRefererfallback) to match. - Tenant β
aset_tenant(account_id)switches the active database connection. - Method resolution β
get_methodlooks atroutesfor a custom match; falls back to standard CRUD. - Cache β when
cache=Trueand method is GET, builds key + namespace, runsbefore_cache, checks Redis. On hit, returns immediately. - pre_process β your hook to run anything before the body is parsed.
- Body parsing β for POST/PATCH, parses JSON; if a Pydantic schema is set, validates and replaces
self.bodywith the validated dict. - Filters / pagination / ordering β for list GET only.
- Handler β the chosen method (
get/post/patch/deleteor custom route). - Serialize β runs
dehydrate, applieslist_schemaif any, runspost_process, writes the response and saves to cache.
Full flow (ASCII)
Side-arrows ββΊ mark override points β methods you can replace on your resource. The dotted box shows the methods called underneath, so you see where to plug in.
ββββββββββββββββββββββββββββ
β incoming request β
ββββββββββββββ¬ββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββ
β SecurityMiddleware β β scanner / UA / 4xx-flood
β blocks β 403 (24h) β instant block
ββββββββββββββ¬ββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββ
β AuthMiddleware β β cookie β request.user
β (non-resource views) β for templates etc.
ββββββββββββββ¬ββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββ
β BaseResource.dispatch β
ββββββββββββββ¬ββββββββββββββ
β
βββββββββββββββββββββΌββββββββββββββββββββ
βΌ βΌ βΌ
[1] get_client_ip [2] _enforce_rate_limit [3] _authenticate
REMOTE_ADDR abuse β 403 24h Bearer (opt-in)
+ X-Real-IP 429 if too fast β X-Api-Key
if proxy trusted β cookie
β self.user
β self.account
β
βΌ
ββββββββββββββββββββββββββββ
β authenticated=True β no session β 401
β but no session? β
ββββββββββββββ¬ββββββββββββββ
β
βΌ
[4] _enforce_token (X-Token HMAC, when ENFORCE_TOKEN)
[5] get_allowed_domain (Origin/Referer, when ALLOWED_ORIGINS)
[6] aset_tenant(account.id) β switch tenant DB
β
βΌ
ββββββββββββββββββββββββββββ
β get_method (routes) β β custom handler
β β OR default get/post/...
ββββββββββββββ¬ββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββ
β method allowed? β β 405 if not
ββββββββββββββ¬ββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββ
β GET + cache=True ? β
βββ¬βββββββββββββββββββββββββββ¬ββ
β yes β no
βΌ β
ββββββββββββββββββββββββββββ β
β before_cache(request) β β
β build cache_key + ns β β
β Redis GET β β
βββββββ¬ββββββββββββββ¬βββββββ β
β HIT β MISS β
βΌ βΌ βΌ
return cached ββββββββ¬βββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββ
β pre_process(request) β β your hook
ββββββββββββββ¬ββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββ
β POST/PATCH ? β
βββ¬βββββββββββββββββββββββββββ¬ββ
β yes β no
βΌ β
ββββββββββββββββββββββββββββ β
β parse JSON body β β
β Pydantic validate β β
β (create/update_schema) β β
β β 422 on failure β β
β hydrate(body) hook β β
ββββββββββββββ¬ββββββββββββββ β
β β
ββββββββββββ¬ββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββ
β GET list (no id) ? β
βββ¬βββββββββββββββββββββββββββ¬ββ
β yes β no
βΌ β
ββββββββββββββββββββββββββββ β
β build_filters(request) β β
β get_filters(request) β ?filter= β
β paginate(request) β β
β ordenate(request) β β
ββββββββββββββ¬ββββββββββββββ β
β β
ββββββββββββ¬ββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββ
β apply queryset_filter β
ββββββββββββββ¬ββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββ
β handler runs β
β β
β βββββββββββββββ ββββββββββββββββ β
β β GET β ββΊ β get(request) β override to replace entirely β
β ββββββββ¬βββββββ ββββββββ¬ββββββββ β
β β β β
β β βββββββββββ΄ββββββββββββ β
β β βΌ βΌ β
β β has self.id? no id β
β β β β β
β β βΌ βΌ β
β β ββββββββββββββββ βββββββββββββββ β
β β β get_obj(id) β ββΊ β get_objs β ββΊ override for β
β β ββββββββ¬ββββββββ β (request) β custom listing, β
β β β ββββββββ¬βββββββ aggregations, β
β β βΌ βΌ non-ORM source β
β β ββββββββββββββββ βββββββββββββββ β
β β β alter_detail β ββΊ βalter_list β ββΊ reshape result β
β β ββββββββββββββββ βββββββββββββββ β
β β
β βββββββββββββββ ββββββββββββββββ β
β β POST β ββΊ β post(request)β β
β ββββββββ¬βββββββ ββββββββ¬ββββββββ β
β β β β
β β βΌ β
β β ββββββββββββββββββββββ β
β β β create_obj(req, β ββΊ override for side β
β β β body) β effects (email, β
β β ββββββββββββββββββββββ€ webhook), or replace β
β β β β’ create_fields β β when no model exists β
β β β β’ auto-fill β β
β β β created_by / β β
β β β updated_by / β β
β β β owner_id β β
β β β β’ blank/null check β β
β β β β’ acreate β β
β β β β’ custom_* save β β
β β β β’ m2m save β β
β β β β’ get_obj(new_id) β ββΊ uses your get_obj β
β β ββββββββββββββββββββββ β
β β
β βββββββββββββββ ββββββββββββββββ β
β β PATCH β ββΊ β patch(request)β β
β ββββββββ¬βββββββ ββββββββ¬ββββββββ β
β β β β
β β βΌ β
β β ββββββββββββββββββββββ β
β β β update_obj(id,body)β ββΊ override to enforce β
β β ββββββββββββββββββββββ€ field-level rules, β
β β β β’ update_fields β β or replace entirely β
β β β β’ aget(pk=id) β β
β β β β’ self.diff[k] = β β
β β β {old, new} β β
β β β β’ aupdate(...) β β
β β β β’ get_obj(id) β β
β β ββββββββββββββββββββββ β
β β
β βββββββββββββββ ββββββββββββββββ β
β β DELETE β ββΊ βdelete(request)β β
β ββββββββ¬βββββββ ββββββββ¬ββββββββ β
β β β β
β β βΌ β
β β ββββββββββββββββββββββ β
β β β delete_obj(id) β ββΊ override for β
β β ββββββββββββββββββββββ€ soft-delete, cascade, β
β β β β’ _ownership_filterβ audit β
β β β β’ adelete β β
β β ββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββ
β
βΌ
exception? ββyesβββΆ HTTPException β render JSON
β any other β log +
β sanitized 500
β (DEBUG=False)
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β serialize(result) β
β β
β for each row in result: β
β ββββββββββββββββββββ β
β β dehydrate(row) β ββΊ override to strip secrets, add computed β
β ββββββββββββββββββββ fields, format dates β
β β
β ββββββββββββββββββββ β
β β list_schema β ββΊ Pydantic-shape every row (drops extras, β
β β (if set) β coerces types) β set list_schema attribute β
β ββββββββββββββββββββ β
β β
β ββββββββββββββββββββ β
β β post_process β ββΊ override for last-chance modification β
β β (response) β (audit logs from self.diff, etc.) β
β ββββββββββββββββββββ β
β β
β ββββββββββββββββββββ β
β β save_cache β ββΊ only when cache=True and method=GET β
β β (if cache=True) β override to change TTL / namespace β
β ββββββββββββββββββββ β
β β
β JsonResponse(... encoder=CustomJSONEncoder ...) β
ββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββ
β invalidate_cache β β writes only
β (POST/PATCH/DELETE) β by namespace
ββββββββββββββ¬ββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββ
β ExceptionMiddleware β β if HTTPException
β renders JSON β escaped
ββββββββββββββ¬ββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββ
β JSON response β
ββββββββββββββββββββββββββββWhere to hook
| Need | Hook |
|---|---|
| Inject context, fetch related data | pre_process(request) |
| Add request data into the cache key | before_cache(request) β mutate self.cache_key |
| Mutate request body before validation | hydrate(body) |
| Mutate one row before response | dehydrate(row) |
| Reshape the whole list | alter_list(results) |
| Reshape the whole detail | alter_detail(result) |
| Last-chance modification | post_process(response) |
Sanitized 500
In production (DEBUG=False), unhandled exceptions inside the handler are caught and turned into a JSON 500 with no stack trace. HTTPException is always re-raised because it is the controlled-error mechanism.
0-mcp by Stamatios Stamou Jr β github.com/ssjunior/0-mcp