howdy/cors
Cross-origin resource sharing (CORS) middleware.
Browsers block JavaScript on one origin from reading responses served by
another unless the response says it is allowed. This middleware adds
those headers and answers the OPTIONS preflight requests browsers send
before non-simple cross-origin requests.
import howdy/cors
let policy =
cors.new()
|> cors.allow_origins(["https://app.example.com"])
|> cors.allow_methods([http.Get, http.Post, http.Delete])
|> cors.allow_headers(["content-type", "authorization"])
|> cors.allow_credentials()
|> cors.max_age(600)
howdy.new()
|> howdy.middleware(cors.middleware(policy))
|> howdy.middleware(require_api_key)
Add it before any middleware that could reject a request, such as
authentication or rate limiting. Preflights carry no credentials, so an
auth middleware that ran first would answer them with 401 and the
browser would never make the real request.
For a public API with no cookies or auth headers, cors.allow_all() is
a complete policy.
How requests are handled
- Requests without an
originheader are not cross-origin. They pass through without CORS headers, but still vary by Origin when the policy can allow cross-origin requests. - A preflight is an
OPTIONSrequest withoriginandaccess-control-request-methodheaders. The middleware answers it directly with204and never calls the handler. If the origin is not allowed, the204carries no CORS headers, so the browser blocks the real request. - Any other request with an
originruns as normal, and the response gainsaccess-control-allow-originand friends when the origin is allowed. This includes error responses, so the browser can read a422from a handler rather than reporting a CORS failure.
Preflights reach the middleware because the router answers OPTIONS
for any path that has routes, through the controller’s middleware, even
when no OPTIONS route was declared. Paths with no routes at all are
404 before any middleware runs, so they carry no CORS headers.
Origins
An origin is the scheme, host and port a page was loaded from, such as
https://app.example.com or http://localhost:5173. Give the full
origin with no path or trailing slash. Sandboxed pages send the literal
origin null, which you may list explicitly.
Allowing any origin together with credentials is refused, because it lets
every website on the internet make authenticated requests on behalf of
your users. If you really want that, allow_origins_matching with a
function that always returns True states the intent explicitly.
Types
Values
pub fn allow_all() -> Config
A policy for public APIs: any origin, the default methods, and whatever request headers the browser asks for. Credentials stay disallowed.
pub fn allow_any_header(config: Config) -> Config
Allow whatever request headers the browser asks for on a preflight, by
echoing access-control-request-headers back.
pub fn allow_any_origin(config: Config) -> Config
Allow every origin. Responses carry access-control-allow-origin: *,
which browsers refuse to combine with credentials.
pub fn allow_credentials(config: Config) -> Config
Allow cookies, authorization headers and client certificates on
cross-origin requests. Requires a specific origin policy: combining this
with allow_any_origin panics when the middleware is built.
pub fn allow_headers(
config: Config,
headers: List(String),
) -> Config
Request headers browsers may send beyond the safelist, such as
content-type for JSON bodies or authorization.
pub fn allow_methods(
config: Config,
methods: List(http.Method),
) -> Config
The methods browsers may use for cross-origin requests, sent on
preflights as access-control-allow-methods.
pub fn allow_origins(
config: Config,
origins: List(String),
) -> Config
Allow exactly these origins. Each must be null or of the form
scheme://host with an optional port; anything else panics as a
configuration error. Comparison is case-insensitive.
pub fn allow_origins_matching(
config: Config,
allow: fn(String) -> Bool,
) -> Config
Allow origins the function accepts. The function receives the origin
header exactly as sent, such as https://tenant-a.example.com, and
runs on every cross-origin request.
pub fn expose_headers(
config: Config,
headers: List(String),
) -> Config
Response headers browser JavaScript may read. Without this, scripts see
only the safelisted headers such as content-type.
pub fn max_age(config: Config, seconds: Int) -> Config
How many seconds browsers may cache a preflight response. Negative values are clamped to zero.
pub fn middleware(
config: Config,
) -> fn(
context.Context(Nil),
fn(context.Context(Nil)) -> response.Response(content.Content),
) -> response.Response(content.Content)
Turn a policy into middleware. Panics if the policy allows any origin together with credentials.
pub fn new() -> Config
A policy that allows no origins. Add some with allow_origins,
allow_any_origin or allow_origins_matching.
Methods default to GET, HEAD, POST, PUT, PATCH and DELETE.
No request headers beyond the browser’s safelist are allowed, no
response headers are exposed, credentials are not allowed, and
preflights are not cached.