Listeners & Serving
By default a Weld server listens on one port and serves every route from it. This page covers the declarations that control how it serves — additional listeners and virtual hosts, CORS, response compression, and health checks with graceful shutdown. They're all written in your .weld source (and, where noted, tuned by environment variables the compiled server reads).
Listeners & virtual hosts
By default a server has one listener on the --port/PORT port. A listen block declares another one, identified by a port, a host (matched against the request's Host header), or both. Bind a route or hub to a listener with on <name> — or wrap a group in an on <name> { … } block so the on isn't repeated per route:
listen admin { port 9090 } # a second port
listen api { host "api.example.com" } # same default port, matched by Host
listen intl { host "*.internal.corp" } # any single subdomain label
on api {
route GET "/" -> string { respond "api root" }
route GET "/users" -> string { respond "api users" }
}
route GET "/metrics" on admin -> string { respond "weld_up 1\n" }
route GET "/" -> string { respond "default" } # no `on` → the default listenerPorts. A listener with a
portbinds it (handy for admin/metrics on a separate port). With noport, the listener shares the default--port/PORTport and is told apart by itshost. Each named listener also reads<NAME>_PORTat startup (e.g.ADMIN_PORT), falling back to its literal — so ports are configurable per deploy.Virtual hosts. Several listeners may share one port; incoming requests are demultiplexed by the
Hostheader. Matching is case-insensitive and ignores the port (api.example.com:8080matchesapi.example.com). Ahostis an exact name, a bare*(any host), or a single*.suffixwildcard matching exactly one subdomain label (a.example.com✓, but notexample.comora.b.example.com). A host-specific listener wins over a host-agnostic one sharing the port.Each listener owns its routes. A request is served by exactly one listener — the one its port and
Hostresolve to — and only that listener's routes match. Routes on the default listener are not a fallback for a host-specific one (this mirrors nginx'sserver_name); a request toapi.example.comabove gets a 404 for/metrics, since/metricsis onadmin. To share an endpoint across several listeners, bind it to all of them with a list —on [api, web](oron [api, web] { … }for a group) — so a common/healthisn't repeated per host:weldon [api, intl] { route GET "/health" -> string { respond "ok" } # served for both hosts }A gRPC
service … on <listener>can't sethost(gRPC routes by HTTP/2:authority, not theHostheader). Multiple listeners are threaded-driver only; not--greenyet.
CORS
A cors { … } block configures Cross-Origin Resource Sharing for every route on a listener — put it inside a listen block, or at the top level for the default listener. Weld then answers preflight OPTIONS automatically and adds the Access-Control-Allow-* headers to responses; no per-route boilerplate.
cors {
origin "https://app.example.com", "https://admin.example.com" # or `origin "*"`
headers "content-type", "authorization" # allowed request headers (omit → reflect request's)
expose "x-request-id" # response headers JS may read
credentials # allow cookies/Authorization
max_age 86400 # cache the preflight
}
route GET "/api/users" -> [User] { … } # inherits the policy- Origins may be a static list,
"*", or a predicateorigin(o => <bool>)that runs for every request'sOrigin— call your ownfns for custom logic (subdomain matching, dynamic allowlists). A matching origin is reflected back withVary: Origin. - Methods default to the methods of the routes on that listener (plus
OPTIONS); override withmethods GET, POST, …. credentialscan't be combined withorigin "*"(the spec forbids it) — Weld rejects it at compile time; use an explicit list or a predicate.- Preflight is answered automatically, but a
route OPTIONS "/path"handler takes precedence — write one to control a specific path's preflight yourself. - Per-route override is done with plain headers:
cors offopts the route out of the automatic headers, then set your own withheader "access-control-allow-origin" …(and, if needed, aroute OPTIONSfor its preflight). CORS response headers are just HTTP headers.
Response compression
compress on a listener (or at the top level, for the default listener) turns on response compression for every route on it. Per request Weld reads Accept-Encoding and, for text-ish content types whose body is at least min_size bytes, sends the body gzip- or deflate- encoded, adding Content-Encoding and Vary: Accept-Encoding.
compress # top level → the default listener (defaults: min_size 256)
listen api {
port 8080
compress { min_size 1024 } # or tune it per listener
}
route GET "/report" on api -> string { type "text/csv" respond big_csv() } # compressed
route GET "/logo.png" compress off -> bytes { type "image/png" respond bytes } # opted out- Negotiation: gzip is preferred over deflate; if the client accepts neither, the body is sent as-is (still with
Vary: Accept-Encoding). - What's compressed:
text/*andjson/javascript/xml/svg/wasm/csv/yaml/NDJSON/ event-stream types, only when the body is ≥min_size(default 256 bytes). Already-compressed media (images, video, zip) is skipped automatically. - Per-route override:
compressopts a single route in (on a listener that has none), andcompress offopts one out. - Scope: applies to buffered responses. Chunked/streaming (
emit) responses aren't compressed yet. Only gzip/deflate are supported — brotli isn't in the Zig standard library.
Health checks & graceful shutdown
On SIGTERM/SIGINT the server shuts down gracefully rather than dropping connections:
- Readiness fails immediately —
ready()returnsfalse, so a readiness probe returns 503 and the load balancer stops routing new requests to this instance. - It keeps serving for
SHUTDOWN_DELAY_SECONDS(default0) so that 503 has time to propagate to the balancer before the socket closes — set this to a few seconds behind a load balancer that routes by readiness. - It stops accepting new connections and lets in-flight requests finish, up to
SHUTDOWN_GRACE_SECONDS(default25), then exits.
Expose a readiness endpoint (typically on a private listener) and point your orchestrator's probe at it:
route GET "/readyz" on admin -> string {
if ready() { respond "ok" } else { status 503 respond "draining" }
}Liveness is just any always-200 route (/livez → respond "ok"). See examples/health.weld.