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.

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
pkl
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).

pkl
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.

pkl
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

pkl
new Compress {
  algorithms { "br"; "gzip" }
  minSize = 512
}
FieldTypeDefaultDescription
typeString"compression" (fixed)(fixed)
algorithmsListingnew { "zstd"; "br"; "gzip" }Compression algorithms offered to clients, in server preference order.
minSizeUInt1024Minimum response body size (in bytes) required to trigger compression.
contentTypesListing?nullContent-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

pkl
new RateLimit {
  requests = 100
  window = 60.s
  key = "ip"
}
FieldTypeDefaultDescription
typeString"rateLimit" (fixed)(fixed)
requestsUInt(isPositive)(required)Maximum number of requests allowed within window. Must be positive.
windowDuration1.sDuration of the sliding window over which requests are counted.
burstUInt?nullBurst allowance above requests before throttling begins.
keyString"ip"Key function that determines how requests are grouped for limiting.
statusCodeUInt429HTTP 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

pkl
new Cors {
  allowedOrigins { "https:<<>>
  allowedMethods { "GET"; "POST" }
  credentials = true
}
FieldTypeDefaultDescription
typeString"cors" (fixed)(fixed)
allowedOriginsListing(empty)Origins allowed to make cross-origin requests.
allowedMethodsListing?nullHTTP methods permitted for cross-origin requests.
allowedHeadersListing?nullRequest headers clients are allowed to send in cross-origin requests.
exposeHeadersListing?nullResponse headers the browser is allowed to expose to client-side JS.
maxAgeDuration86400.sHow long the browser may cache a preflight response.
credentialsBooleanfalseWhether 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

pkl
new Headers {
  responseSet { ["X-Served-By"] = "elide" }
  requestRemove { "X-Forwarded-For" }
}
FieldTypeDefaultDescription
typeString"headers" (fixed)(fixed)
requestSetMapping(empty)Headers to set on the request before the handler sees it.
requestAddMapping(empty)Headers to append to the request before the handler sees it.
requestRemoveListing(empty)Request header names to remove before the handler sees the request.
responseSetMapping(empty)Headers to set on the response after the handler produces it.
responseAddMapping(empty)Headers to append to the response after the handler produces it.
responseRemoveListing(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

pkl
new BasicAuth {
  realm = "Admin"
  users { ["admin"] = "$2b$10$..." }
  exclude { "/healthz" }
}
FieldTypeDefaultDescription
typeString"basicAuth" (fixed)(fixed)
realmString"Elide"Realm string displayed in the browser's credential prompt.
usersMapping(!isEmpty)(required)Allowed users as username to bcrypt-hash pairs.
excludeListing(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 in AccessLog records, enabling end-to-end request tracing.
FieldTypeDefaultDescription
typeString"requestId" (fixed)(fixed)
headerString"X-Request-Id"Header name used to carry the request ID.
trustIncomingBooleanfalseWhen 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.

See AccessLogFormat for available output formats.
FieldTypeDefaultDescription
typeString"accessLog" (fixed)(fixed)
formatAccessLogFormat"json"Log output format.
outputString"stdout"Log output destination.
includeHeadersListing(empty)Request header names whose values are included in each log record.
minStatusUInt0Minimum HTTP response status code to log.
conditionString?nullConditional 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

pkl
new Rewrite {
  from = "^/v1/(.*)"
  to = "/api/$1"
}
FieldTypeDefaultDescription
typeString"rewrite" (fixed)(fixed)
fromString(!isEmpty)(required)Regular expression matched against the request URI path.
toString(!isEmpty)(required)Replacement pattern substituted for the matched portion of the path.
replaceAllBooleanfalseWhen 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

pkl
new BodyLimit { maxBytes = 10_000_000 }  // 10 MB
FieldTypeDefaultDescription
typeString"bodyLimit" (fixed)(fixed)
maxBytesUInt(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.

When injectHeaders is true, the following headers are added to every authenticated request:
pkl
`Tailscale-User-Login`, `Tailscale-User-Name`,
`Tailscale-Node-Name`, `Tailscale-Node-Tags`
FieldTypeDefaultDescription
typeString"tailscaleAuth" (fixed)(fixed)
injectHeadersBooleantrueInject Tailscale identity headers on every authenticated request.
allowAllBoolean?nullUnconditionally allow all tailnet members without further checks.
allowUsersListing?nullTailscale login names (email addresses) permitted access.
denyUsersListing?nullTailscale login names denied access.
allowTagsListing?nullTailscale ACL tags whose nodes are permitted access.
denyTagsListing?nullTailscale 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

pkl
new ErrorPage {
  status = 404
  template = "./errors/404.html"
}
FieldTypeDefaultDescription
typeString"errorPage" (fixed)(fixed)
statusUInt(this >= 400 && this < 600)(required)HTTP status code this error page applies to.
templateString(!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.

See JwtAuth for how keys are matched to incoming tokens.
FieldTypeDefaultDescription
kidString?nullKey ID (kid) for key selection.
algJwtAlgorithm(required)The signing algorithm this key is used with.
pemString(!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

pkl
new JwtAuth {
  keys { new JwtKey { alg = "RS256"; pem = read("./public.pem") } }
  audiences { "https:<<>>
  exclude { "/healthz" }
}
FieldTypeDefaultDescription
typeString"jwtAuth" (fixed)(fixed)
keysListing(!isEmpty)(required)Static public keys for signature verification.
algorithmsListingnew { "RS256" }Algorithms accepted for token verification.
issuersListing(empty)Allowed token issuers (iss claim).
audiencesListing(empty)Allowed token audiences (aud claim).
clockSkewSecsUInt30Clock skew tolerance (in seconds) for exp and nbf claim checks.
tokenSourcesListing(empty)Ordered list of locations to extract the JWT from.
excludeListing(empty)Path prefixes excluded from JWT authentication.
injectClaimsHeadersBooleanfalseWhen true, inject validated JWT claims as request headers.
claimsHeaderPrefixString"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

pkl
new IpFilter {
  allow { "10.0.0.0/8"; "172.16.0.0/12" }
  deny  { "10.0.0.1/32" }
}
FieldTypeDefaultDescription
typeString"ipFilter" (fixed)(fixed)
allowListing(empty)CIDR ranges to allow.
denyListing(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

pkl
new SecurityHeaders {
  hsts = "max-age=63072000; includeSubDomains; preload"
  contentSecurityPolicy = "default-src 'self'"
}
FieldTypeDefaultDescription
typeString"securityHeaders" (fixed)(fixed)
hstsString?nullValue for the Strict-Transport-Security header.
xContentTypeOptionsString?nullValue for the X-Content-Type-Options header.
xFrameOptionsString?nullValue for the X-Frame-Options header.
xXssProtectionString?nullValue for the X-XSS-Protection header.
referrerPolicyString?nullValue for the Referrer-Policy header.
contentSecurityPolicyString?nullValue 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