Skip to content

How It Works

wiki edited this page Sep 4, 2026 · 1 revision

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

Why it runs outside everything

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.

Per request

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

Vary: Origin on every response with an Origin

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.

The concrete origin is echoed, never "*"

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.

Refusal is silent

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.

The empty origin is never allowed

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.

Preflights

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.

No competing OPTIONS routes

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.

The Allow header becomes Allow-Methods

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.

Cost

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.