roadrunner_cors (roadrunner v0.10.5)

View Source

Cross-Origin Resource Sharing (CORS) middleware.

Answers the browser's preflight OPTIONS request and decorates cross-origin responses with the right Access-Control-* headers, driven by a per-instance policy. Deny-by-default: a request whose Origin is not allowed gets no Access-Control-Allow-Origin, so the browser blocks it.

Wire it with its config (there is no useful bare form, origins is required):

middlewares => [
    {roadrunner_cors, #{
        origins => [~"https://app.example.com"],
        methods => [~"GET", ~"POST"],
        credentials => true
    }}
]

Config

KeyDefaultMeaning
origins(required)[binary()] allowlist, fun((binary()) -> boolean()) predicate, or any
methods[~"GET", ~"HEAD", ~"POST"]methods echoed in a preflight's Access-Control-Allow-Methods
headers[][binary()] echoed in Access-Control-Allow-Headers, or reflect to mirror the request's Access-Control-Request-Headers
expose[][binary()] for Access-Control-Expose-Headers
credentialsfalsewhen true, emit Access-Control-Allow-Credentials: true
max_age(omitted)preflight cache lifetime in seconds (Access-Control-Max-Age)

The policy is validated and compiled once when the listener starts (and on reload_routes/2), so a bad value crashes at startup with {invalid_cors_opt, Key, Value} rather than failing silently on a request.

Behaviour

  • No Origin header: not a CORS request, passed through untouched.
  • Preflight (OPTIONS with Access-Control-Request-Method): short-circuited with a 204 carrying the Access-Control-* set, without calling the handler.
  • Simple / actual request: the handler runs, then Access-Control-Allow-Origin (plus credentials / exposed headers when configured) is added to its response.

Vary: Origin is added to every CORS response, allowed or not, so a shared cache never serves one origin's response to another; an existing Vary (e.g. Accept-Encoding from roadrunner_compress) is appended to rather than replaced.

Credentials and the wildcard

With credentials => true the response echoes the concrete request Origin rather than *, because a browser rejects Access-Control-Allow-Origin: * together with Access-Control-Allow-Credentials: true. origins => any without credentials emits the literal *.

Summary

Types

The compiled policy produced by init/1 and consumed by call/3.

Types

config()

-type config() ::
          #{origins := origins(),
            methods => [binary()],
            headers => [binary()] | reflect,
            expose => [binary()],
            credentials => boolean(),
            max_age => non_neg_integer()}.

origins()

-type origins() :: any | [binary()] | fun((binary()) -> boolean()).

state()

-type state() ::
          #cors{origins :: origins(),
                credentials :: boolean(),
                reflect_headers :: boolean(),
                preflight_static :: roadrunner_http:headers(),
                actual_static :: roadrunner_http:headers()}.

The compiled policy produced by init/1 and consumed by call/3.

Functions

call/3

init(Config)

-spec init(config()) -> state().