Middleware Reference
Middleware type hierarchy for the Elide server pipeline.
Middleware intercepts requests and responses in declaration order. Each middleware listed at the server level wraps every request; middleware listed at the route level wraps only requests matching that route.
Middleware is applied as a stack: the first middleware in the listing runs outermost (it sees the raw request first and the final response last).
Concrete middleware types form a discriminated union via
typealias Middleware. Use the concrete open classes directly:
Compress — on-the-fly response compression
RateLimit — per-key sliding-window request rate limiting
Cors — Cross-Origin Resource Sharing headers
Headers — add, set, or remove arbitrary request/response headers
BasicAuth — HTTP Basic Authentication with bcrypt credentials
RequestId — inject a unique ID into each request
AccessLog — structured per-request logging with format control
Rewrite — rewrite request paths before route matching
BodyLimit — reject request bodies exceeding a byte threshold
TailscaleAuth — Tailscale identity-aware authentication
ErrorPage — custom HTML error pages per status code
JwtAuth — JWT/OAuth token validation with static keys
SecurityHeaders — inject standard security response headers (HSTS, CSP, etc.)
IpFilter — CIDR-based IP allow/deny filtering
ConnectionLimit — per-IP concurrent connection limiting
> This page is auto-generated from the PKL schema. See the guide for usage examples.
Types
HttpMethod
HTTP method string, shared with Route.pkl.
typealias HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS" | "TRACE"AccessLogFormat
Access-log output format.
"combined" — Apache combined log format (IP, user, method, path, status, size, referrer, user-agent)
"common" — Apache common log format (no referrer or user-agent)
"json" — one JSON object per line, machine-parseable
"ecs" — Elastic Common Schema, for Elasticsearch/Kibana ingest
typealias AccessLogFormat = "combined" | "common" | "json" | "ecs"JwtAlgorithm
JWT signing algorithm identifier.
RSA: "RS256", "RS384", "RS512".
ECDSA: "ES256", "ES384", "ES512".
HMAC (symmetric): "HS256", "HS384", "HS512".
EdDSA: "EdDSA" (Ed25519).
typealias JwtAlgorithm = "RS256" | "RS384" | "RS512" | "ES256" | "ES384" | "ES512" | "HS256" | "HS384" | "HS512" | "EdDSA"Middleware
Discriminated union of all middleware types.
Use one of the concrete middleware classes listed above. Multiple
middleware instances can be combined in a Listing on a
server block or route.
typealias Middleware = Compress | RateLimit | Cors | Headers | BasicAuth | RequestId | AccessLog | Rewrite | BodyLimit | TailscaleAuth | ErrorPage | JwtAuth | SecurityHeaders | IpFilter | ConnectionLimit---
Compress
Open class — can be extended.
On-the-fly response compression.
Compresses response bodies using content negotiation: the server selects
the best algorithm from algorithms that the client supports (via the
Accept-Encoding request header). Responses smaller than minSize
bytes are sent uncompressed to avoid overhead on tiny payloads.
Example
new Compress {
algorithms { "br"; "gzip" }
minSize = 512
}| Field | Type | Default | Description |
|---|---|---|---|
type | String | "compression" (fixed) | (fixed) |
algorithms | Listing | new { "zstd"; "br"; "gzip" } | Compression algorithms offered to clients, in server preference order. |
minSize | UInt | 1024 | Minimum response body size (in bytes) required to trigger compression. |
contentTypes | Listing | null | Content-Type patterns eligible for compression. |
algorithms
Compression algorithms offered to clients, in server preference order.
The first algorithm that the client also supports wins. Defaults to zstd, then Brotli, then gzip.
minSize
Minimum response body size (in bytes) required to trigger compression.
Responses with a body smaller than this threshold are sent uncompressed. Default: 1024 bytes.
contentTypes
Content-Type patterns eligible for compression.
Glob-style patterns are supported (e.g., "text/*"). When null,
the built-in default list is used: text/*, application/json,
application/xml, application/javascript, and similar.
---
RateLimit
Open class — can be extended.
Per-key sliding-window rate limiting.
Tracks request counts within a sliding time window, keyed by client
identity (IP address by default). When a client exceeds the limit,
the server responds with 429 Too Many Requests and a Retry-After
header indicating when capacity is restored.
Example
new RateLimit {
requests = 100
window = 60.s
key = "ip"
}| Field | Type | Default | Description |
|---|---|---|---|
type | String | "rateLimit" (fixed) | (fixed) |
requests | UInt(isPositive) | (required) | Maximum number of requests allowed within window. Must be positive. |
window | Duration | 1.s | Duration of the sliding window over which requests are counted. |
burst | UInt? | null | Burst allowance above requests before throttling begins. |
key | String | "ip" | Key function that determines how requests are grouped for limiting. |
statusCode | UInt | 429 | HTTP status code returned when the rate limit is exceeded. |
window
Duration of the sliding window over which requests are counted.
Default: 1 second.
burst
Burst allowance above requests before throttling begins.
When set, this many additional requests are permitted in a burst
before rate limiting kicks in. When null (default), no extra burst
is allowed beyond requests.
key
Key function that determines how requests are grouped for limiting.
Supported values:
"ip" — client IP address (default)
"header: — value of a named request header
"query: — value of a query parameter
"cookie: — value of a named cookie
Requests sharing the same key value share one rate-limit bucket.
statusCode
HTTP status code returned when the rate limit is exceeded.
Default: 429 (Too Many Requests).
---
Cors
Open class — can be extended.
Cross-Origin Resource Sharing (CORS) headers.
Controls which origins, methods, and headers are permitted in
cross-origin requests and preflight (OPTIONS) responses. The server
emits Access-Control-* response headers based on these settings.
Example
new Cors {
allowedOrigins { "https:<<>>
allowedMethods { " GET"; "POST" }
credentials = true
}| Field | Type | Default | Description |
|---|---|---|---|
type | String | "cors" (fixed) | (fixed) |
allowedOrigins | Listing | (empty) | Origins allowed to make cross-origin requests. |
allowedMethods | Listing | null | HTTP methods permitted for cross-origin requests. |
allowedHeaders | Listing | null | Request headers clients are allowed to send in cross-origin requests. |
exposeHeaders | Listing | null | Response headers the browser is allowed to expose to client-side JS. |
maxAge | Duration | 86400.s | How long the browser may cache a preflight response. |
credentials | Boolean | false | Whether cross-origin requests may include credentials (cookies, TLS |
allowedOrigins
Origins allowed to make cross-origin requests.
Use "" to permit all origins. Note: "" cannot be combined with
credentials = true (browsers reject this combination). An empty
listing rejects all cross-origin requests.
allowedMethods
HTTP methods permitted for cross-origin requests.
When null, the server reflects the method from the
Access-Control-Request-Method preflight header.
allowedHeaders
Request headers clients are allowed to send in cross-origin requests.
When null, all headers are permitted (reflected from
Access-Control-Request-Headers).
exposeHeaders
Response headers the browser is allowed to expose to client-side JS.
Headers not listed here are hidden from fetch() responses. When
null, no extra headers are exposed beyond the CORS-safelisted set.
maxAge
How long the browser may cache a preflight response.
Higher values reduce the number of preflight OPTIONS requests at the
cost of slower propagation of CORS policy changes. Default: 24 hours.
credentials
Whether cross-origin requests may include credentials (cookies, TLS
client certificates, Authorization headers).
When true, allowedOrigins must list explicit origins (not "*").
---
Headers
Open class — can be extended.
Add, set, or remove arbitrary request and response headers.
Mutations are applied in this order per direction (request or response):
1. remove — delete the header if present
2. set — overwrite the header with a single value
3. add — append an additional value (multi-value header)
Example
new Headers {
responseSet { ["X-Served-By"] = "elide" }
requestRemove { "X-Forwarded-For" }
}| Field | Type | Default | Description |
|---|---|---|---|
type | String | "headers" (fixed) | (fixed) |
requestSet | Mapping | (empty) | Headers to set on the request before the handler sees it. |
requestAdd | Mapping | (empty) | Headers to append to the request before the handler sees it. |
requestRemove | Listing | (empty) | Request header names to remove before the handler sees the request. |
responseSet | Mapping | (empty) | Headers to set on the response after the handler produces it. |
responseAdd | Mapping | (empty) | Headers to append to the response after the handler produces it. |
responseRemove | Listing | (empty) | Response header names to remove from the response. |
requestSet
Headers to set on the request before the handler sees it.
If the header already exists, its value is overwritten.
requestAdd
Headers to append to the request before the handler sees it.
If the header already exists, the new value is added alongside it.
responseSet
Headers to set on the response after the handler produces it.
If the header already exists, its value is overwritten.
responseAdd
Headers to append to the response after the handler produces it.
If the header already exists, the new value is added alongside it.
---
BasicAuth
Open class — can be extended.
HTTP Basic Authentication.
Validates credentials from the Authorization: Basic header against a
static user list with bcrypt-hashed passwords. Returns 401 Unauthorized
with a WWW-Authenticate challenge when credentials are missing or invalid.
Example
new BasicAuth {
realm = "Admin"
users { ["admin"] = "$2b$10$..." }
exclude { "/healthz" }
}| Field | Type | Default | Description |
|---|---|---|---|
type | String | "basicAuth" (fixed) | (fixed) |
realm | String | "Elide" | Realm string displayed in the browser's credential prompt. |
users | Mapping | (required) | Allowed users as username to bcrypt-hash pairs. |
exclude | Listing | (empty) | Path prefixes excluded from authentication. |
realm
Realm string displayed in the browser's credential prompt.
Default: "Elide".
users
Allowed users as username to bcrypt-hash pairs.
At least one entry is required. Generate hashes with
elide auth hash or any standard bcrypt tool. A cost
factor of 10 or higher is recommended.
exclude
Path prefixes excluded from authentication.
Requests whose path starts with any listed prefix bypass Basic Auth entirely. Useful for health-check or readiness probe endpoints.
---
RequestId
Open class — can be extended.
Inject a unique request identifier into every request.
Generates a UUID v4 and places it in a configurable request header. The ID is forwarded to upstream handlers and automatically included inAccessLog records, enabling end-to-end request tracing.
| Field | Type | Default | Description |
|---|---|---|---|
type | String | "requestId" (fixed) | (fixed) |
header | String | "X-Request-Id" | Header name used to carry the request ID. |
trustIncoming | Boolean | false | When true, preserve an existing value in header instead of |
header
Header name used to carry the request ID.
Default: "X-Request-Id". The same header is copied to the response.
trustIncoming
When true, preserve an existing value in header instead of
generating a new one.
Enable this when a load balancer or API gateway upstream already injects request IDs and you want to propagate them unchanged.
---
AccessLog
Open class — can be extended.
Per-request structured access logging.
Emits one log record per request after the response is sent. When used
at the server level, every request is logged. When attached to a route,
only matching requests are logged. This middleware can coexist with
LoggingSettings.accessLog for route-scoped or format-specific overrides.
AccessLogFormat for available output formats.
| Field | Type | Default | Description |
|---|---|---|---|
type | String | "accessLog" (fixed) | (fixed) |
format | AccessLogFormat | "json" | Log output format. |
output | String | "stdout" | Log output destination. |
includeHeaders | Listing | (empty) | Request header names whose values are included in each log record. |
minStatus | UInt | 0 | Minimum HTTP response status code to log. |
condition | String? | null | Conditional logging filter expression. |
format
Log output format.
Default: "json" (one JSON object per line). See AccessLogFormat
for all options.
output
Log output destination.
Accepts "stdout", "stderr", or an absolute file path. When a file
path is used, the file is created if it does not exist and appended to
otherwise.
includeHeaders
Request header names whose values are included in each log record.
Useful for capturing correlation IDs, client hints, or other
application-specific headers. See RequestId for automatic ID
injection.
minStatus
Minimum HTTP response status code to log.
Responses with a status below this value are silently dropped.
Default: 0 (log everything). Set to 400 to log only errors.
condition
Conditional logging filter expression.
When set, only requests matching the condition produce a log record.
When null (default), all requests are logged (subject to
minStatus). Supported formats:
"status:4xx" — log only 4xx responses
"status:4xx-5xx" — log 4xx and 5xx responses
"status:500" — log only status 500
"status:400-599" — log status 400 through 599
"path:/api/*" — log requests whose path starts with /api/
"path_regex:^/api/.*" — log requests matching a regex
---
Rewrite
Open class — can be extended.
Rewrite the request path before route matching.
Applies a regex substitution to the request URI path (and optionally the query string). The rewrite runs before route matching, so downstream routes and handlers see the rewritten path.
Example
new Rewrite {
from = "^/v1/(.*)"
to = "/api/$1"
}| Field | Type | Default | Description |
|---|---|---|---|
type | String | "rewrite" (fixed) | (fixed) |
from | String(!isEmpty) | (required) | Regular expression matched against the request URI path. |
to | String(!isEmpty) | (required) | Replacement pattern substituted for the matched portion of the path. |
replaceAll | Boolean | false | When true, replace all occurrences of the pattern in the path. |
from
Regular expression matched against the request URI path.
Captured groups can be referenced in to using $1, $2, etc.
Must not be empty.
to
Replacement pattern substituted for the matched portion of the path.
Supports capture-group back-references ($1, $2, ...). Must not
be empty.
replaceAll
When true, replace all occurrences of the pattern in the path.
When false (default), only the first match is replaced.
---
BodyLimit
Open class — can be extended.
Reject request bodies that exceed a configurable size limit.
Returns 413 Content Too Large when the Content-Length header exceeds
maxBytes, or when a chunked/streaming body grows beyond the limit
mid-transfer. The connection is closed immediately upon rejection.
Example
new BodyLimit { maxBytes = 10_000_000 } // 10 MB| Field | Type | Default | Description |
|---|---|---|---|
type | String | "bodyLimit" (fixed) | (fixed) |
maxBytes | UInt(isPositive) | (required) | Maximum allowed request body size in bytes. Must be positive. |
---
TailscaleAuth
Open class — can be extended.
Tailscale identity-aware request authentication.
Resolves the Tailscale identity of each incoming request via the local
tailscaled whois API and optionally evaluates per-route access
policies. Requires --tailscale to be active at runtime.
Evaluation order: deny rules are checked before allow rules. If a request matches a deny rule, it is rejected regardless of allow rules.
WheninjectHeaders is true, the following headers are added to
every authenticated request:
`Tailscale-User-Login`, `Tailscale-User-Name`,
`Tailscale-Node-Name`, `Tailscale-Node-Tags`| Field | Type | Default | Description |
|---|---|---|---|
type | String | "tailscaleAuth" (fixed) | (fixed) |
injectHeaders | Boolean | true | Inject Tailscale identity headers on every authenticated request. |
allowAll | Boolean? | null | Unconditionally allow all tailnet members without further checks. |
allowUsers | Listing | null | Tailscale login names (email addresses) permitted access. |
denyUsers | Listing | null | Tailscale login names denied access. |
allowTags | Listing | null | Tailscale ACL tags whose nodes are permitted access. |
denyTags | Listing | null | Tailscale ACL tags whose nodes are denied access. |
injectHeaders
Inject Tailscale identity headers on every authenticated request.
Default: true.
allowAll
Unconditionally allow all tailnet members without further checks.
When true, overrides allowUsers and allowTags (all tailnet
members are permitted). Identity headers are still injected when
injectHeaders is true. When null, the allow/deny lists apply.
allowUsers
Tailscale login names (email addresses) permitted access.
When null, user-based filtering is not applied.
denyUsers
Tailscale login names denied access.
Deny rules are evaluated before allow rules.
allowTags
Tailscale ACL tags whose nodes are permitted access.
For example, "tag:web" or "tag:prod". When null, tag-based
filtering is not applied.
denyTags
Tailscale ACL tags whose nodes are denied access.
Deny rules are evaluated before allow rules.
---
ErrorPage
Open class — can be extended.
Custom HTML error page for a specific HTTP status code.
Replaces the default error response body with a user-provided HTML
template. Template variables are substituted per-request:
{{status}} — the numeric HTTP status code (e.g., 404)
{{message}} — the HTTP reason phrase (e.g., Not Found)
{{path}} — the request URI path
Templates are loaded at startup and cached in memory. Add one
ErrorPage per status code you want to customize.
Example
new ErrorPage {
status = 404
template = "./errors/404.html"
}| Field | Type | Default | Description |
|---|---|---|---|
type | String | "errorPage" (fixed) | (fixed) |
status | UInt(this >= 400 && this < 600) | (required) | HTTP status code this error page applies to. |
template | String(!isEmpty) | (required) | File path or inline HTML template for the error page. |
status
HTTP status code this error page applies to.
Must be in the 400-599 range (client or server error).
template
File path or inline HTML template for the error page.
When the value looks like a file path (contains / or \), the file
is loaded at startup. Otherwise, it is treated as inline HTML.
Supports {{status}}, {{message}}, and {{path}} placeholders.
---
JwtKey
Open class — can be extended.
A static public key used for JWT signature verification.
SeeJwtAuth for how keys are matched to incoming tokens.
| Field | Type | Default | Description |
|---|---|---|---|
kid | String? | null | Key ID (kid) for key selection. |
alg | JwtAlgorithm | (required) | The signing algorithm this key is used with. |
pem | String(!isEmpty) | (required) | PEM-encoded public key in SPKI or PKCS#8 format. |
kid
Key ID (kid) for key selection.
When set, only tokens whose JOSE header kid matches this value
will be verified with this key. When null, the key is tried for
all tokens that match alg.
pem
PEM-encoded public key in SPKI or PKCS#8 format.
For HMAC algorithms (HS256/HS384/HS512), this is the
base64-encoded shared secret.
---
JwtAuth
Open class — can be extended.
JWT/OAuth token authentication.
Validates JSON Web Tokens (JWTs) using static public keys with ring
signature verification. Supports RSA (RS256/RS384/RS512), ECDSA
(ES256/ES384/ES512), HMAC (HS256/HS384/HS512), and EdDSA.
Tokens can be extracted from multiple sources (checked in order):
"bearer" — Authorization: Bearer header
"cookie: — value of a named cookie
"query: — value of a query parameter
"header: — value of a custom header
Returns 401 Unauthorized when no valid token is found.
Example
new JwtAuth {
keys { new JwtKey { alg = "RS256"; pem = read("./public.pem") } }
audiences { "https:<<>>
exclude { " /healthz" }
}| Field | Type | Default | Description |
|---|---|---|---|
type | String | "jwtAuth" (fixed) | (fixed) |
keys | Listing | (required) | Static public keys for signature verification. |
algorithms | Listing | new { "RS256" } | Algorithms accepted for token verification. |
issuers | Listing | (empty) | Allowed token issuers (iss claim). |
audiences | Listing | (empty) | Allowed token audiences (aud claim). |
clockSkewSecs | UInt | 30 | Clock skew tolerance (in seconds) for exp and nbf claim checks. |
tokenSources | Listing | (empty) | Ordered list of locations to extract the JWT from. |
exclude | Listing | (empty) | Path prefixes excluded from JWT authentication. |
injectClaimsHeaders | Boolean | false | When true, inject validated JWT claims as request headers. |
claimsHeaderPrefix | String | "X-Jwt-Claim-" | Prefix prepended to claim names when injecting claim headers. |
keys
Static public keys for signature verification.
At least one key must be provided. See JwtKey for key format.
When a token's kid header is present, only keys with a matching
kid are tried; otherwise all keys with a matching alg are tried.
algorithms
Algorithms accepted for token verification.
Tokens signed with an algorithm not in this list are rejected even
if a matching key exists. This prevents algorithm-confusion attacks.
Default: ["RS256"].
issuers
Allowed token issuers (iss claim).
When non-empty, tokens whose iss claim does not match any entry are
rejected. When empty (default), any issuer is accepted.
audiences
Allowed token audiences (aud claim).
When non-empty, tokens whose aud claim does not include at least one
listed value are rejected. When empty (default), any audience is
accepted.
clockSkewSecs
Clock skew tolerance (in seconds) for exp and nbf claim checks.
Accounts for clock drift between the token issuer and this server. Default: 30 seconds.
tokenSources
Ordered list of locations to extract the JWT from.
The first source that yields a token wins. Supported values:
"bearer" — Authorization: Bearer header
"cookie: — named cookie
"query: — query parameter
"header: — custom request header
When empty (default), "bearer" is used at runtime.
exclude
Path prefixes excluded from JWT authentication.
Requests whose path starts with any listed prefix bypass token validation. Useful for health checks and public endpoints.
injectClaimsHeaders
When true, inject validated JWT claims as request headers.
Each claim becomes a header named — for
example, the sub claim becomes X-Jwt-Claim-Sub.
claimsHeaderPrefix
Prefix prepended to claim names when injecting claim headers.
Default: "X-Jwt-Claim-".
---
IpFilter
Open class — can be extended.
CIDR-based IP allow/deny filtering.
Controls access based on the client's IP address. When both allow and
deny are specified, deny takes precedence (a denied IP is rejected
even if it also matches an allow entry). Rejected requests receive
403 Forbidden.
When both lists are empty, this middleware is a no-op.
Example
new IpFilter {
allow { "10.0.0.0/8"; "172.16.0.0/12" }
deny { "10.0.0.1/32" }
}| Field | Type | Default | Description |
|---|---|---|---|
type | String | "ipFilter" (fixed) | (fixed) |
allow | Listing | (empty) | CIDR ranges to allow. |
deny | Listing | (empty) | CIDR ranges to deny. |
allow
CIDR ranges to allow.
When non-empty, only client IPs matching at least one entry are
permitted. Supports both IPv4 and IPv6 notation (e.g.,
"10.0.0.0/8", "::1/128").
deny
CIDR ranges to deny.
Deny takes precedence over allow. Supports both IPv4 and IPv6 notation.
---
SecurityHeaders
Open class — can be extended.
Standard security response headers (HSTS, X-Frame-Options, CSP, etc.).
Injects security-related headers into every response. Each field
corresponds to one header. When a field is null, the built-in
default value is used. Set a field to "" (empty string) to suppress
that header entirely.
Example
new SecurityHeaders {
hsts = "max-age=63072000; includeSubDomains; preload"
contentSecurityPolicy = "default-src 'self'"
}| Field | Type | Default | Description |
|---|---|---|---|
type | String | "securityHeaders" (fixed) | (fixed) |
hsts | String? | null | Value for the Strict-Transport-Security header. |
xContentTypeOptions | String? | null | Value for the X-Content-Type-Options header. |
xFrameOptions | String? | null | Value for the X-Frame-Options header. |
xXssProtection | String? | null | Value for the X-XSS-Protection header. |
referrerPolicy | String? | null | Value for the Referrer-Policy header. |
contentSecurityPolicy | String? | null | Value for the Content-Security-Policy header. |
hsts
Value for the Strict-Transport-Security header.
Controls HTTPS enforcement in browsers. When null, the built-in
default is used. Example: "max-age=31536000; includeSubDomains".
xContentTypeOptions
Value for the X-Content-Type-Options header.
Prevents MIME-type sniffing. Built-in default: "nosniff".
xFrameOptions
Value for the X-Frame-Options header.
Controls whether the page can be embedded in an .
Typical values: "DENY" or "SAMEORIGIN".
xXssProtection
Value for the X-XSS-Protection header.
Legacy XSS filter hint for older browsers. Modern browsers ignore this in favor of CSP.
referrerPolicy
Value for the Referrer-Policy header.
Controls how much referrer information is sent with requests.
Common values: "no-referrer", "strict-origin-when-cross-origin".
contentSecurityPolicy
Value for the Content-Security-Policy header.
Controls which resources the browser is allowed to load. This is the single most impactful security header for web applications.
---
ConnectionLimit
Open class — can be extended.
Per-IP concurrent connection limiting.
Tracks the number of open connections per client IP address. When a
client exceeds maxPerIp concurrent connections, additional connection
attempts are rejected with 503 Service Unavailable.
RateLimit.
| Field | Type | Default | Description |
|---|---|---|---|
type | String | "connectionLimit" (fixed) | (fixed) |
maxPerIp | UInt(isPositive) | (required) | Maximum number of concurrent connections allowed from a single IP |
maxPerIp
Maximum number of concurrent connections allowed from a single IP address. Must be positive.
---