Skip to content

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 to limit_user (cms_api.middlewares.limitation). It is a fixed-window, in-memory counter keyed by client IP + request path (MAX_REQUESTS = 120 per WINDOW_SECONDS = 60). Over the limit returns 429 with a retry-after header. 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/v1 and /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-256 ETag over the body;
  • sets Cache-Control from the per-endpoint s-maxage / swr values (or the v1 default when both are None), plus Vary: Host, Accept-Language;
  • sets Last-Modified (HTTP-date, UTC) when the store supplied a last_modified;
  • returns 304 Not Modified when the request's If-None-Match matches the computed ETag, or If-Modified-Since covers last_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.).