Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

http.middleware

import http

http exposes this as http.middleware, so import http is enough and the names are called as http.middleware.*. import http.middleware reaches the same definitions directly.

The middleware every public HTTP service ends up needing: CORS, access logging, the security headers a browser acts on, authentication, and a request rate limit.

Each function here returns a middleware - a function of (request, response, next)

  • ready to hand to HttpServer.use():
import http
import http.middleware

var server = http.server(3000)

server.use(middleware.logger())
server.use(middleware.security_headers())
server.use(middleware.cors({ origins: ['https://app.example.com'] }))

Order matters: middleware run outermost first, so a logger added first sees the final status of everything added after it.

Functions

cors()

http.middleware.cors(options: ?dict) -> function(3)

Answers CORS preflights and adds the cross-origin headers a browser needs before it will let script read a response from another origin.

origins is the list of origins allowed, matched exactly. The literal '*' allows any origin, but only for requests that carry no credentials: a wildcard together with Access-Control-Allow-Credentials is rejected by every browser, so setting credentials reflects the request’s own origin instead - which means the origins list becomes the only thing standing between an attacker’s page and an authenticated response, and it needs to be an explicit list rather than a wildcard.

Parameters

  • options (?dict) — origins (default ['*']), methods, headers (allowed request headers; defaults to reflecting what was asked for), expose (response headers script may read), credentials (default false), max_age (seconds a preflight may be cached, default 86400)

Returns function(3)

logger()

http.middleware.logger(options: ?dict) -> function(3)

Writes one line per request once the response is finished.

The default format is the Common Log Format extended with the response time, which every log analyser already understands. Pass format to build the line yourself, or sink to send it somewhere other than standard output.

Parameters

  • options (?dict) — sink (a function taking the line), format (a function taking (request, response, milliseconds) and returning the line), trust_proxy (whether to log the forwarded client address rather than the peer)

Returns function(3)

security_headers()

http.middleware.security_headers(options: ?dict) -> function(3)

Adds the response headers a browser acts on to harden a page.

Every one of them is opt-out, because the right value depends on the application:

HeaderDefaultWhat it does
X-Content-Type-Optionsnosniffstops the browser second-guessing a declared content type
X-Frame-OptionsDENYrefuses to be framed, which is what clickjacking needs
Referrer-Policystrict-origin-when-cross-originkeeps paths and queries out of outbound referrers
Strict-Transport-Securityone yearonly sent over HTTPS, where it is meaningful
Content-Security-Policynot settoo application-specific to guess at

Parameters

  • options (?dict) — any of the header values above by lowercase name (content_security_policy, frame_options, referrer_policy, hsts_max_age, hsts_subdomains, content_type_options), each nil to omit

Returns function(3)

basic_auth()

http.middleware.basic_auth(verify, realm: ?string) -> function(3)

Requires HTTP Basic authentication.

verify is called with the username and password and returns whether they are acceptable; it should compare secrets with http.util.secure_equals() rather than ==, so that a wrong guess and a nearly-right one take the same time.

A nil verifier disables the middleware: it passes every request straight through rather than refusing one. That makes blanking the verifier a way to switch authentication off without unpicking the chain around it.

Parameters

  • verify (?function(2)) — nil to disable
  • realm (?string) — the realm named in the challenge; defaults to 'Restricted'

Returns function(3)

bearer_auth()

http.middleware.bearer_auth(verify, realm: ?string) -> function(3)

Requires a bearer token.

A nil verifier disables the middleware, as with basic_auth().

Parameters

  • verify (?function(1)) — called with the token; returns whether it is acceptable, or a value to attach to request.context as 'user'. nil to disable
  • realm (?string)

Returns function(3)

parse_basic()

http.middleware.parse_basic(header: string)

Parses an HTTP Basic Authorization header into a username and password.

Parameters

  • header (string)

Returns — ?list: [username, password], or nil if the header is not a well-formed Basic credential

parse_bearer()

http.middleware.parse_bearer(header: string) -> ?string

Parses a Bearer Authorization header into its token.

Parameters

  • header (string)

Returns ?string

jwt_auth()

http.middleware.jwt_auth(verifier, options: ?dict) -> function(3)

Requires a valid JSON Web Token, verified by the jwt module.

verifier is either a jwt.Verifier, or any function taking the token and returning its claims:

import http.middleware
import jwt

server.use(middleware.jwt_auth(
  jwt.Verifier(secret, { algorithms: ['HS256'], audience: 'api' })
))

The function form is what covers a key set resolved by kid, or anything else the jwt module can do that a fixed verifier cannot:

server.use(middleware.jwt_auth(@(token) {
  return jwt.verify_with_jwks(token, keys, { audience: 'api' })
}))

On success the claims land on request.context['claims'], and the sub claim - the usual place an issuer puts the account the token speaks for

  • on request.context['user'].

Failures follow RFC 6750 §3: a request with no token is answered 401 with a bare Bearer challenge, one whose token does not verify is answered 401 with error="invalid_token", and one whose token is valid but lacks a required scope is answered 403 with error="insufficient_scope".

A nil verifier disables the middleware: every request passes straight through, unauthenticated. Nothing here raises, so registering it never needs a catch block around it.

Parameters

  • verifier (Verifier|function(1)|nil) — nil to disable
  • options (?dict) — realm (default 'api'), optional (attach the claims when a valid token is present but do not refuse a request without one), and scopes (a list every token must carry)

Returns function(3)

Note: Like HttpRequest.validate(), this deliberately does not import the jwt module. The verifier is built by the caller, which keeps the token format entirely the application’s business and means a server that authenticates nothing never pays to load it.

rate_limit()

http.middleware.rate_limit(options: ?dict) -> function(3)

Limits how many requests one client may make in a window of time.

The counter lives in memory, so it is per worker: with workers isolates the effective limit is limit times workers. That is a deliberate trade - a shared counter would need shared state, and this is meant to blunt a runaway client rather than to meter billing.

Parameters

  • options (?dict) — limit (requests per window, default 60), window (seconds, default 60), key (a function of the request returning the bucket key; defaults to the client address), trust_proxy

Returns function(3)

request_id()

http.middleware.request_id(header: ?string) -> function(3)

Attaches a unique identifier to every request, echoing back one the client supplied so a trace can be followed across services.

The identifier lands on request.context['request_id'] and in the response’s X-Request-Id.

Parameters

  • header (?string) — the header to read and write; defaults to 'X-Request-Id'

Returns function(3)

force_https()

http.middleware.force_https(options: ?dict) -> function(3)

Sends every request that arrived over cleartext to the same URL over HTTPS.

Parameters

  • options (?dict) — status (default 308, which preserves the method), port (the HTTPS port, when it is not 443)

Returns function(3)

etag()

http.middleware.etag(min_size: ?number) -> function(3)

Computes a weak ETag over a finished response body and answers 304 Not Modified when the client already has that version.

A handler that already set its own ETag is left alone - it knows something about the resource that hashing the bytes does not.

Parameters

  • min_size (?number) — bodies smaller than this are not tagged; defaults to 128 bytes

Returns function(3)


2026, Richard Ore and Zuri contributors