brazier/middleware

Types

CORS configuration.

pub type CorsConfig {
  CorsConfig(
    allow_origins: List(String),
    allow_methods: List(String),
    allow_headers: List(String),
    max_age: Int,
  )
}

Constructors

  • CorsConfig(
      allow_origins: List(String),
      allow_methods: List(String),
      allow_headers: List(String),
      max_age: Int,
    )

    Arguments

    allow_origins

    Allowed origins (e.g., [“https://example.com”]). Use [“*”] for any.

    allow_methods

    Allowed HTTP methods.

    allow_headers

    Allowed headers.

    max_age

    Max age for preflight cache (seconds).

A middleware function. Takes a request and a next function, returns a response. Can modify the request before passing to next, modify the response after, or short-circuit by returning a response without calling next.

pub type Middleware =
  fn(
    request.Request(server.Connection),
    fn(request.Request(server.Connection)) -> response.Response(
      server.ResponseBody,
    ),
  ) -> response.Response(server.ResponseBody)

Values

pub fn at(
  path: String,
  mw: fn(
    request.Request(server.Connection),
    fn(request.Request(server.Connection)) -> response.Response(
      server.ResponseBody,
    ),
  ) -> response.Response(server.ResponseBody),
) -> fn(
  request.Request(server.Connection),
  fn(request.Request(server.Connection)) -> response.Response(
    server.ResponseBody,
  ),
) -> response.Response(server.ResponseBody)

Apply middleware only when the request path is exactly path (ignoring empty segments and a trailing slash). Applied when any reading of the path matches.

middleware.at("/login", middleware.rate_limit(login_limiter))
pub fn body_parser(
  max_bytes: Int,
) -> fn(
  request.Request(server.Connection),
  fn(request.Request(server.Connection)) -> response.Response(
    server.ResponseBody,
  ),
) -> response.Response(server.ResponseBody)

Body limit middleware: refuses a request whose declared body is not acceptable before the handler runs. It reads no body.

The length is checked with the listener’s own framing rules (transport/http.declared_body_length, RFC 9112 section 6):

  • a request with neither Content-Length nor Transfer-Encoding has an empty body and passes;
  • a Content-Length that is not 1-19 ASCII digits, or more than one, is answered with 400;
  • Transfer-Encoding is answered with 501 (400 with a Content-Length);
  • a length above max_bytes is answered with 413.

The listener applies the same rules with its own max_body_bytes before any middleware runs, so this middleware is a tighter limit for the routes it wraps.

Reference: Bandit (Bandit.Headers.get_content_length) and Cowboy answer a bad Content-Length with 400 and treat a missing one as an empty body; Plug.Parsers answers an oversized body with 413.

pub fn compress() -> fn(
  request.Request(server.Connection),
  fn(request.Request(server.Connection)) -> response.Response(
    server.ResponseBody,
  ),
) -> response.Response(server.ResponseBody)

Compression middleware — compresses response body with gzip when the client sends Accept-Encoding: gzip. Only compresses text-based responses (HTML, CSS, JS, JSON).

pub fn cors(
  config: CorsConfig,
) -> fn(
  request.Request(server.Connection),
  fn(request.Request(server.Connection)) -> response.Response(
    server.ResponseBody,
  ),
) -> response.Response(server.ResponseBody)

CORS middleware — sets Access-Control-* headers.

pub fn csp_with_connect_src(
  policy: String,
  sources: List(String),
) -> Result(String, String)

policy with sources added to its connect-src, for a route that connects somewhere the app-wide policy does not allow (a WebTransport gateway, an EventSource host). Give that route the result with at, only or methods around secure_headers_with_csp; the app-wide secure_headers() keeps the policy the inner one set.

middleware.csp_with_connect_src(middleware.default_csp, [
  "https://localhost:4433",
])
// Ok("default-src 'self'; ...; connect-src 'self' https://localhost:4433; ...")

A policy without connect-src governs connections with default-src, so the new directive starts from the default-src sources rather than narrowing to sources alone. 'none' is dropped from the starting sources: next to another source it means nothing. A source already present is not repeated, and every other directive is kept in its place.

Errors, with the reason logged, when sources is empty or one of them is not a single CSP source expression (empty, whitespace, ;, ,, a non-printable or non-ASCII character, or 'none'); when policy holds a , (more than one policy), a non-printable or non-ASCII character, or the same directive twice (browsers enforce only the first); and when it has neither connect-src nor default-src, so connections are already unrestricted.

Reference: CSP Level 3, “Parse a serialized CSP”, the serialized-source-list grammar, and “Get the effective directive for request” (default-src governs a missing connect-src).

pub fn default_cors() -> CorsConfig

Default CORS config allowing all origins.

pub const default_csp: String

Default Content-Security-Policy value.

  • script-src 'self': only scripts served by the app run. Brazier’s pages load the client bundle with <script src> and render no inline script or on* attribute, so an injected <script> or handler is refused.
  • connect-src 'self': the WebSocket and fetch reach only the page’s own origin. CSP Level 3 matches ws: and wss: on the same host and port against 'self'. Safari 15 and earlier did not (WebKit bugs 201591 and 235873, fixed in WebKit r292266), so an app that must support them adds its own wss://host with secure_headers_with_csp.
  • img-src 'self' data:: no third-party image hosts.
  • style-src keeps 'unsafe-inline', which attribute.style needs.
  • frame-ancestors 'self' is the CSP equivalent of X-Frame-Options: SAMEORIGIN (clickjacking); object-src 'none' and base-uri 'self' close plugin and <base> injection.

Reference: CSP Level 3, “Does url match expression in origin with redirect count?” (the 'self' upgrade to ws:/wss:).

pub fn except(
  prefix: String,
  mw: fn(
    request.Request(server.Connection),
    fn(request.Request(server.Connection)) -> response.Response(
      server.ResponseBody,
    ),
  ) -> response.Response(server.ResponseBody),
) -> fn(
  request.Request(server.Connection),
  fn(request.Request(server.Connection)) -> response.Response(
    server.ResponseBody,
  ),
) -> response.Response(server.ResponseBody)

Apply middleware to all paths EXCEPT prefix and below it (segment boundary). Useful for skipping auth on public routes. The middleware is skipped only when EVERY reading of the path is under the prefix, so /public/../admin and /public/%2E%2E/admin still get it.

middleware.except("/public", auth.require_auth(store))
pub fn group(
  middlewares: List(
    fn(
      request.Request(server.Connection),
      fn(request.Request(server.Connection)) -> response.Response(
        server.ResponseBody,
      ),
    ) -> response.Response(server.ResponseBody),
  ),
) -> fn(
  request.Request(server.Connection),
  fn(request.Request(server.Connection)) -> response.Response(
    server.ResponseBody,
  ),
) -> response.Response(server.ResponseBody)

Group multiple middleware into one. All run in order on matching requests.

middleware.only("/admin", middleware.group([
  auth.require_auth(store),
  auth.csrf_protection(store),
  middleware.rate_limit(limiter),
]))
pub fn hsts(
  max_age_seconds: Int,
  include_subdomains: Bool,
) -> Result(
  fn(
    request.Request(server.Connection),
    fn(request.Request(server.Connection)) -> response.Response(
      server.ResponseBody,
    ),
  ) -> response.Response(server.ResponseBody),
  String,
)

HTTP Strict Transport Security. Add this when the app is served over HTTPS (Brazier does not terminate TLS: this is for deployments behind a TLS proxy). Browsers ignore the header on plain-http responses (RFC 6797 section 8.1), so it cannot pin an http-only deployment. A typical production value is one year: hsts(31_536_000, True). Errors on a negative max-age.

pub fn logger() -> fn(
  request.Request(server.Connection),
  fn(request.Request(server.Connection)) -> response.Response(
    server.ResponseBody,
  ),
) -> response.Response(server.ResponseBody)

Logger middleware — logs request method, path, and response status.

pub fn methods(
  allowed: List(http.Method),
  mw: fn(
    request.Request(server.Connection),
    fn(request.Request(server.Connection)) -> response.Response(
      server.ResponseBody,
    ),
  ) -> response.Response(server.ResponseBody),
) -> fn(
  request.Request(server.Connection),
  fn(request.Request(server.Connection)) -> response.Response(
    server.ResponseBody,
  ),
) -> response.Response(server.ResponseBody)

Apply middleware only for specific HTTP methods.

middleware.methods([http.Post, http.Put], middleware.rate_limit(limiter))
pub fn only(
  prefix: String,
  mw: fn(
    request.Request(server.Connection),
    fn(request.Request(server.Connection)) -> response.Response(
      server.ResponseBody,
    ),
  ) -> response.Response(server.ResponseBody),
) -> fn(
  request.Request(server.Connection),
  fn(request.Request(server.Connection)) -> response.Response(
    server.ResponseBody,
  ),
) -> response.Response(server.ResponseBody)

Apply middleware only when the request path is prefix or below it (segment boundary). Applied when any reading of the path matches.

middleware.only("/admin", auth.require_auth(store))
middleware.only("/api", middleware.rate_limit(limiter))
pub fn pipeline(
  middlewares: List(
    fn(
      request.Request(server.Connection),
      fn(request.Request(server.Connection)) -> response.Response(
        server.ResponseBody,
      ),
    ) -> response.Response(server.ResponseBody),
  ),
  handler: fn(request.Request(server.Connection)) -> response.Response(
    server.ResponseBody,
  ),
) -> fn(request.Request(server.Connection)) -> response.Response(
  server.ResponseBody,
)

Chain multiple middleware into a single handler. Middleware execute in order: first middleware wraps the second, etc. The innermost handler is the actual app handler.

Example:

let handler = middleware.pipeline(
  [middleware.logger(), middleware.cors(cors_config)],
  my_app_handler,
)
pub fn rate_limit(
  limiter: rate_limit.RateLimiter,
) -> fn(
  request.Request(server.Connection),
  fn(request.Request(server.Connection)) -> response.Response(
    server.ResponseBody,
  ),
) -> response.Response(server.ResponseBody)

Rate limiting middleware — limits requests per client IP. Returns 429 Too Many Requests when the limit is exceeded.

The client is the socket peer address. Forwarding headers (X-Forwarded-For, Forwarded) are ignored, so a client cannot dodge the limit by inventing addresses. Behind a reverse proxy use rate_limit_behind_proxies, otherwise every client shares the proxy’s budget.

pub fn rate_limit_behind_proxies(
  limiter: rate_limit.RateLimiter,
  trusted: ip.TrustedProxies,
) -> fn(
  request.Request(server.Connection),
  fn(request.Request(server.Connection)) -> response.Response(
    server.ResponseBody,
  ),
) -> response.Response(server.ResponseBody)

Rate limiting keyed on the client IP resolved through trusted proxies: only the forwarding header that trusted names is read, only when the socket peer is one of its proxies, and the client is the right-most hop that is not a trusted proxy. Build trusted with ip.parse_trusted_proxies or transport.trusted_proxies(limits), so it matches SecurityLimits.trusted_proxies and forwarding_header.

pub fn request_id() -> fn(
  request.Request(server.Connection),
  fn(request.Request(server.Connection)) -> response.Response(
    server.ResponseBody,
  ),
) -> response.Response(server.ResponseBody)

Request ID middleware — generates a unique ID for each request. Adds x-request-id to the response headers and logs it. Useful for tracing requests through distributed systems.

pub fn secure_headers() -> fn(
  request.Request(server.Connection),
  fn(request.Request(server.Connection)) -> response.Response(
    server.ResponseBody,
  ),
) -> response.Response(server.ResponseBody)

Security headers middleware — sets common security headers. Reference: OWASP secure headers project. Uses the default CSP policy. For custom CSP, use secure_headers_with_csp.

pub fn secure_headers_with_csp(
  csp: String,
) -> fn(
  request.Request(server.Connection),
  fn(request.Request(server.Connection)) -> response.Response(
    server.ResponseBody,
  ),
) -> response.Response(server.ResponseBody)

Security headers middleware with a custom Content-Security-Policy. Use this when the default CSP is too strict (e.g., external stylesheets, fonts, or images from CDNs).

A header the response already carries is kept: the handler or an inner middleware set it for this response, so it is the more specific value (an upload’s default-src 'none'; sandbox, an error page’s X-Frame-Options: DENY, a route’s own policy from csp_with_connect_src). Phoenix gets the same precedence by running put_secure_browser_headers before the controller.

Example:

middleware.secure_headers_with_csp(
  "default-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; connect-src 'self'",
)
✨ Search Document