Skip to content
C.W.K.
Stream
Lesson 06 of 08 · published

Production Patterns — Middleware, CORS, Errors, Logs

~12 min · middleware, cors, errors

Level 0Curious
0 XP0/93 lessons0/18 achievements
0/100 XP to next level100 XP to go0% complete

CORS — an explicit list, never a wildcard

cwkPippa's CORS is an explicit allowlist, and it is longer than you would guess: local dev ports, the tailnet address of each Mac that runs a client, and the served origin of every sibling app whose browser mints its own PIN session — about thirty entries — plus one pattern for browser-extension and desktop-webview origins. New client? Add its origin, on purpose. The pattern needs the same care as the list, because it is where an allowlist quietly widens: Starlette full-matches it against the origin, so a trailing .* after a host also admits any domain that merely starts with that host. And the shortcut people reach for when CORS errors pile up, allow_origins=['*'], is a security hole even on a private box. CORSMiddleware itself starts closed: with no origins configured it allows none.

Where CORS sits matters as much as what it allows. It is the outermost middleware on purpose: when the PIN check refuses a request, the 401 must still carry CORS headers, or the browser hides the response body behind a CORS error and the client cannot tell 'wrong PIN' from 'server down'.

Middleware — a stack whose order is the design

Every ordinary request passes through seven layers, outermost first: CORS; PIN auth; a user resolver that binds who is asking into request scope; an audit layer that records admin overrides; gzip for /api JSON, built so it never touches an SSE stream; conditional GET, inside gzip so its ETag is taken over the uncompressed body and a phone that already holds a reply gets an empty 304 (added 2026-10-10; mobile track, lesson 8); and innermost of all, private doors, so a path that names a private conversation reaches only the routes listed for it (2026-09-26). One more layer was registered after CORS, so strictly it wraps the rest: the family gateway (2026-09-22), which acts on a single gateway path and hands every other request straight through. The chat-versus-cron mutex is not middleware — routes call it explicitly, per conversation (Heart track, lesson 4). Middleware stays light and generic; anything expensive or specific belongs in a route or a background task.

Errors — fail loud, fail honest

No silent catches. If the adapter raises, the route surfaces the error event over SSE so the frontend can show 'I got stuck — try again.' Never swallow exceptions to make the chat 'look fine.' That's the broken-chat sin from a different angle.

Logs — for the operator, not the record

The server's log is plain timestamped lines, tagged with the vessel or account they concern — easy to grep, not JSON. That is a deliberate division of labor: the durable record of a conversation is not the log at all, it is the JSONL (Truth track). The log exists so an operator can reconstruct a failure; the JSONL exists so nothing Dad saw is ever lost.

Production starts after 'it works on my Mac'. Check that every allowed origin matches a URL that is actually served, and that an SSE error turns into a visible UI state instead of hiding inside an HTTP 200. A config that looks right and a request that actually travels that path are different claims.

Logs have a boundary too. Dumping raw prompts or credentials because it is convenient buys observability and creates a different incident. Keep the shape a diagnosis needs — event kind, duration, provider, outcome — and summarize or drop sensitive payloads. A good log is not a ledger of everything; it is honest enough to reconstruct the failure.

And resist gathering policy into middleware. Because it touches every route, it is tempting to put business rules there, and then it becomes a hidden ancestor. Keep middleware to what is truly common to every request, and let domain judgment show up in named services and routes.

Code

CORS — explicit allowlist, outermost on purpose·python
from fastapi.middleware.cors import CORSMiddleware

# Abbreviated. The real list names every family origin explicitly:
# local dev ports, each client Mac's tailnet address, and each sibling
# app that mints its own PIN session from the browser (~30 entries).
_allowed_origins = [
    'http://localhost:5173',
    'http://127.0.0.1:5173',
    'http://100.x.y.z:5173',   # a Mac on the tailnet
    'http://localhost:8500',   # a sibling app's served origin
    'tauri://localhost',       # a desktop app's webview
]

# Added after every middleware but the family gateway, which handles one
# path and passes the rest through, so it is the OUTERMOST layer for
# every ordinary request: a 401 from the PIN
# check must still carry CORS headers, or the browser hides the body.
app.add_middleware(
    CORSMiddleware,
    allow_origins=_allowed_origins,
    # Starlette full-matches this pattern. Anchor the host and name the
    # extension ids: a trailing .* after tauri\.localhost would also admit
    # https://tauri.localhost.attacker.example.
    allow_origin_regex=r'chrome-extension://(<extension-id>)|https?://tauri\.localhost(:\d+)?',
    allow_credentials=True,
    allow_methods=['*'],
    allow_headers=['*'],
)

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Pippa— warm💛 by Ttori— warm

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.