-
Notifications
You must be signed in to change notification settings - Fork 0
How It Works
One middleware, attached per router at rextension.PriorityCORS (200) —
outside authentication and rate limiting.
CORS (200) → rate limit (300) → auth (400) → … → handler
A browser will not let script read a response whose CORS headers are missing — including a 401, a 429 or a 500. Without them a cross-origin client sees an opaque network failure instead of the status the server actually sent, and the developer sees "CORS error" instead of "unauthorized".
So the headers must be present whatever the inner layers decide, which means the middleware has to be outside them.
Origin header absent? → pass through, add nothing
(not a cross-origin browser request)
Origin present → add `Vary: Origin`
not in the allowlist → pass through, add nothing else
allowed → Access-Control-Allow-Origin: <the origin>
Access-Control-Allow-Credentials (if enabled)
Access-Control-Expose-Headers (if configured)
…and it is a preflight → add the preflight headers, then let the router
answer OPTIONS from its Allow set
Including the refusal path. Without it a shared cache can serve a response
containing Access-Control-Allow-Origin: https://a.example to a request from
https://b.example — which either leaks the response to an origin that should
not read it, or blocks an origin that should. The refusal is origin-dependent
too, so it needs the header as much as the acceptance does.
Required when credentials are allowed — browsers reject "*" on a credentialed
response — and better even without them, because it keeps the response accurate
for the origin it was produced for.
A disallowed origin gets no 403. The request proceeds, and the browser enforces the refusal by withholding the response from script.
Returning 403 would be worse in two ways:
- A non-browser client is not subject to CORS at all, and would be refused for sending a header it is free to send.
- The browser would report a server error rather than a policy decision.
If you want to block requests by origin, that is an authorization rule, not a CORS one.
A same-origin request carries no Origin header for most methods, and such a
request needs no CORS decision — so no headers are added and nothing is
enforced. Treating "" as allowed would make every allowlist meaningless.
A preflight is an OPTIONS request carrying
Access-Control-Request-Method. An OPTIONS request without that header is an
ordinary request, and is handled as one.
On a preflight the middleware adds:
-
Vary: Access-Control-Request-Method,Vary: Access-Control-Request-Headers -
Access-Control-Allow-Methods— see below -
Access-Control-Allow-Headers— configured list, or the requested headers echoed back Access-Control-Max-Age
…and then hands off to the router, which answers 204 No Content with an
Allow header.
The extension registers no routes. The router already answers OPTIONS from
its Allow set, and a second handler competing for the same path would either
conflict at registration or shadow the router's answer — which is the accurate
one, because it is derived from the routes that exist.
When AllowedMethods is not configured, the middleware wraps the
ResponseWriter and copies the router's Allow header into
Access-Control-Allow-Methods at WriteHeader time.
It has to happen then: the router sets Allow while handling the request, which
is after the middleware's pre-handler code has run. Reading it earlier would
read nothing.
The payoff is that advertised methods are derived from the routes that exist, so they cannot drift — and a route added later is advertised without anyone updating a list.
If the router answers without an Allow header — a 404, most likely — the
middleware advertises GET, HEAD, POST, PUT, PATCH, DELETE rather than nothing.
An empty Access-Control-Allow-Methods fails the preflight for every method,
including ones the application does serve elsewhere.
The wrapper forwards Flush, so nothing downstream loses streaming behaviour.
Everything that can be precomputed is, at composition time: the joined header
lists and the Max-Age string are built once per router, not per request. The
per-request work is one header read, one allowlist scan and a handful of header
writes.
rextension-cors — Cross-Origin Resource Sharing for Rex · MIT · © 2026 Kryovyx
Ecosystem