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_bytesis 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 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 oron*attribute, so an injected<script>or handler is refused.connect-src 'self': the WebSocket andfetchreach only the page’s own origin. CSP Level 3 matchesws:andwss: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 ownwss://hostwithsecure_headers_with_csp.img-src 'self' data:: no third-party image hosts.style-srckeeps'unsafe-inline', whichattribute.styleneeds.frame-ancestors 'self'is the CSP equivalent ofX-Frame-Options: SAMEORIGIN(clickjacking);object-src 'none'andbase-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'",
)