ADR 0010: CORS middleware wraps the error catcher

Status:

Accepted

Context

Starlette’s default error middleware is outermost, so a real 500 ships without CORS headers. A browser then drops the response and the dashboard shows a bare “Network Error” with no status - hiding the actual backend fault.

Decision

A pure-ASGI CatchUnhandledErrorsMiddleware is registered first and CORSMiddleware second (app/main.py). Because add_middleware prepends, CORS ends up outermost and wraps the catcher. The catcher converts an unhandled exception into a JSON 500 from inside the stack, so the response flows back out through CORS and gets its Access-Control-Allow-* headers; the traceback is still logged and sent to Sentry.

Consequences

  • Error responses keep their CORS headers, so the browser (and the dashboard) see the real 500 instead of an opaque “Network Error”.

  • The registration order is load-bearing and easy to break, so the why is documented next to the registration.

See also