Request lifecycle¶
This page traces a single HTTP request through cms-api, from the outermost
middleware to the response (or error envelope). The path is the same for every
block; only the auth dependency and the store call differ.
sequenceDiagram
participant C as Client
participant CORS as CORSMiddleware
participant RL as rate_limit_middleware
participant R as Router (by prefix)
participant AUTH as Block auth dependency
participant S as Store
participant CACHE as cache layer
participant H as Error handler
C->>CORS: HTTP request
CORS->>RL: pass through
RL->>RL: limit_user (IP + path window)
alt over limit
RL-->>C: 429 (retry-after)
else within limit
RL->>R: resolve route by prefix
R->>AUTH: validate_admin / validate_worker_api_key
alt auth fails
AUTH-->>C: 401 / 403 / 503
else authorized
AUTH->>S: handler calls get_public_store()
S-->>CACHE: payload + last_modified
CACHE->>CACHE: ETag / Last-Modified / Cache-Control
alt If-None-Match / If-Modified-Since matches
CACHE-->>C: 304 Not Modified
else
CACHE-->>C: 200 JSON + cache headers
end
end
end
Note over H: any APIError raised in-handler -> JSON error envelope
1. Middleware¶
Two middleware layers wrap every request, added in main.py:
rate_limit_middleware-- delegates tolimit_user(cms_api.middlewares.limitation). It is a fixed-window, in-memory counter keyed by client IP + request path (MAX_REQUESTS = 120perWINDOW_SECONDS = 60). Over the limit returns 429 with aretry-afterheader. It needs no Redis at import or request time, so the app boots and tests run with no external services. Lifespan-scope events bypass the limiter.CORSMiddleware-- permissive in the scaffold (allow_origins=["*"], all methods/headers, credentials allowed).
2. Routing by prefix¶
FastAPI resolves the route by its mount prefix (/api, /admin/v1,
/public/v1, /public/v2). See the Architecture
mount map.
3. Block-level auth dependency¶
The matched block runs its gate as a FastAPI dependency:
/admin/v1(except login) ->validate_admin(401/403)./public/v2->validate_worker_api_key(401/503)./public/v1and/api-> no gate.
See Auth flow for the full rules.
4. Store call¶
The route handler obtains a store via Depends(get_public_store)
(cms_api.public.store.get_public_store) and calls a read method. In production
this returns a SqlPublicStore; tests override the dependency with a
MemoryPublicStore. Each store method returns a (payload, last_modified)
tuple. See Store design.
5. Response shaping and caching¶
Public responses are returned through public_cached_response
(cms_api.public.cache), which:
- canonicalizes the payload to deterministic JSON (
sort_keys, compact separators) and computes a SHA-256ETagover the body; - sets
Cache-Controlfrom the per-endpoints-maxage/swrvalues (or the v1 default when both areNone), plusVary: Host, Accept-Language; - sets
Last-Modified(HTTP-date, UTC) when the store supplied alast_modified; - returns 304 Not Modified when the request's
If-None-Matchmatches the computed ETag, orIf-Modified-Sincecoverslast_modified(compared at second resolution).
The per-endpoint cache values for v2 live in
cms_api.public.store.v2_common.CACHE_HINTS:
| Endpoint | s-maxage |
swr |
|---|---|---|
settings |
300 | 86400 |
pages |
300 | 600 |
authors |
300 | 600 |
lists |
120 | 600 |
sitemap |
3600 | 3600 |
ping |
-- | no cache, no DB |
The v1 default directive (used when a call passes neither value) is
public, max-age=60, s-maxage=300, stale-while-revalidate=60, preserved
byte-for-byte for the v1 call sites.
6. Errors¶
Any handler (or store) may raise APIError
(cms_api.common.errors). register_error_handlers(app), called once in
main.py, translates it into the canonical envelope:
{
"error": {
"code": "resource_not_found",
"message": "Site not found",
"details": { "site": "12" }
}
}
code is a machine-stable snake_case identifier, message is human-readable,
and details is optional structured metadata. The HTTP status comes from the
APIError.status_code (404 for not-found, 400 for validation, 403/503 for the
preview gate, etc.).