Skip to content

Configuration

wiki edited this page Sep 4, 2026 · 1 revision

Configuration

cfg := cors.NewConfig(
	cors.WithAllowedOrigins("https://app.example.com"),
	cors.WithAllowCredentials(true),
	cors.WithExposedHeaders("X-Request-Id"),
	cors.WithMaxAge(10*time.Minute),
)

app := rex.New(cors.WithCORS(cfg))

cors.WithCORS(nil) takes the defaults — which allow no origin.

Fields

type Config struct {
	Policy         rextension.OriginPolicy
	AllowedMethods []string
	AllowedHeaders []string
	ExposedHeaders []string
	MaxAge         time.Duration
}

Policy — the allowlist

type OriginPolicy struct {
	AllowedOrigins   []string
	AllowCredentials bool
}

Origins are matched exactly — scheme, host and port:

"https://app.example.com"       // right
"app.example.com"               // never matches: no scheme
"https://app.example.com:8443"  // a different origin than the same host on 443

There is no pattern or wildcard-subdomain matching, deliberately. A naive suffix check for .example.com matches evil-example.com; a prefix check for https://app.example matches https://app.example.attacker.com. Both have appeared in real advisories.

"*" allows any origin and is valid only without credentials. The combination is refused at startup rather than emitted, because the Fetch specification requires a concrete origin on a credentialed response and browsers discard anything else — a silent failure that reads as a server bug from the client's side.

Default: empty — nothing allowed.

AllowedMethods

Advertised in Access-Control-Allow-Methods on a preflight response.

Leave it empty. An empty list means the router's own Allow set for that path is used, which is derived from the routes that actually exist and therefore cannot drift — and a route added later is advertised without anyone remembering to update a list.

Set it only when you deliberately want to advertise less than you serve.

Default: empty.

AllowedHeaders

The request headers a client may send, advertised in Access-Control-Allow-Headers.

Empty echoes the requested headers back. That is permissive but honest: a browser only asks for headers the page is actually trying to send, and an allowlist that omits one produces a failure the developer reads as "CORS is broken" rather than as a policy decision.

Set it when you want the allowlist to be an actual policy:

cors.WithAllowedHeaders("Content-Type", "Authorization", "X-Request-Id")

Default: empty.

ExposedHeaders

Response headers a cross-origin script may read, via Access-Control-Expose-Headers. Without this a script can read only the CORS-safelisted response headers.

The default is not empty:

[]string{"X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset", "Retry-After"}

Those are sent by rextension-ratelimit on every response and are useless to a cross-origin client unless exposed — a client that cannot read its remaining quota discovers the limit by exceeding it.

WithExposedHeaders replaces the defaults rather than adding to them. Include the X-RateLimit-* headers explicitly if you still want them:

cors.WithExposedHeaders(
	"X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset", "Retry-After",
	"X-Request-Id",
)

MaxAge

How long a browser may cache a preflight result.

Default: 10 minutes. Browsers cap this — Chrome at 2 hours, Firefox at 24 — so a larger value is silently clamped rather than honoured.

Options

WithAllowedOrigins(origins…) set the exact origins permitted
WithAllowCredentials(bool) permit cookies and HTTP authentication
WithPolicy(rextension.OriginPolicy) set the whole policy at once, for sharing with CSRF
WithAllowedMethods(methods…) override the advertised methods
WithAllowedHeaders(headers…) set the request headers a client may send
WithExposedHeaders(headers…) replace the readable response headers
WithMaxAge(d) preflight cache duration

NewConfig(opts…) applies them over NewDefaultConfig() and tolerates a nil option in the list.

Validation

Policy.Valid() is checked in OnInitialize, and a contradictory policy — "*" with credentials, a bare hostname with no scheme — aborts startup:

cors: origin "app.example.com" is not a valid origin: missing scheme

That is deliberate. Both mistakes otherwise produce a policy that looks configured and allows nothing, discovered only from a browser console.

Reading the policy back

ext := cors.NewCORSExtension(cfg).(interface{ Policy() rextension.OriginPolicy })
policy := ext.Policy()

Useful for handing the same policy to the CSRF configuration when it was built inside the CORS config rather than outside it.

Clone this wiki locally