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

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

A CORS policy. Build with new or allow_all and the setters, then turn it into middleware with middleware.

pub opaque type Config

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.

✨ Search Document