Zig 0.17.0

Server

On this page:

  • spider.app: Builds the server of an app: var server = spider.app(.{});.
  • spider.appWithConfig: The same server, with the config given in code instead of spider.config.zig.
  • spider.Server: The server type spider.app returns; its methods register routes and middlewares, and listen() runs it.
  • spider.ListenOptions: The argument of server.listen: .port and .host, both optional.
  • spider.Job: A periodic task of a feature's jobs; spider.every builds one.
  • spider.every: A job for a feature's jobs: spider.every(60_000, run) calls run once a minute.
  • spider.Boot: What a feature's boot() receives: an allocator and the server's Io.
  • spider.StaticConfig: Where static files are served from.
  • spider.static: Static file serving; the server uses it by itself for the static_dir of the config.
  • spider.health: The handlers behind /up and /_spider/health, which spider.app registers.

spider.app

zig
pub fn app(decorations: anytype) AppType(@TypeOf(decorations))

Builds the server of an app, with the config of spider.config.zig. It already answers /up (for load balancers) and /_spider/health, both public, and serves the static directory. It also loads the .env files (see spider.env).

zig
var server = spider.app(.{});
defer server.deinit();

decorations is a struct of values handlers may ask for by type: a handler that takes, after *spider.Ctx, a parameter of the type of one of its fields receives that field (up to four such parameters). Only routes registered on the server itself can: a spider.Group route, or a handler that takes spider.Path / spider.Form / spider.Loaded, cannot. Most apps pass .{}.

spider.appWithConfig

zig
pub fn appWithConfig(config: Config) Server(EmptyDeco)

The same server as spider.app(.{}), with the config given in code instead of read from spider.config.zig (tests, small programs). It has no decorations.

spider.Server

zig
pub fn Server(comptime T: type) type

The server type. Apps get one from spider.app(.{}) and do not name it. Every method that registers something returns the server, so the calls chain:

zig
var server = spider.app(.{});
defer server.deinit();
try server
    .use(spider.logger)
    .mountFeatures(features)
    .onError(onError)
    .listen(.{});

Fields

  • spider_arena: std.heap.ArenaAllocator
  • spider_gpa: std.heap.DebugAllocator(.{})
  • allocator: std.mem.Allocator
  • gpa: std.mem.Allocator
  • router: Router
  • decorations: T
  • global_middlewares: std.ArrayListUnmanaged(MiddlewareFn) = .empty
  • path_middlewares: [32]PathMiddlewareEntry = undefined
  • path_middleware_count: usize = 0
  • error_handler: ?ErrorHandler = null
  • static_config: StaticConfig = .{ .dir = "./public", .prefix = "/" }
  • config: Config = default_config
  • views_index: ?views_mod.ViewsIndex = null
  • ws_route_hubs: std.ArrayListUnmanaged(WsRouteHub) = .empty
  • sse_hub: ?Hub = null
  • sse_threaded: ?std.Io.Threaded = null
  • sse_heartbeat_ms: ?u64 = null: Requested via sseHeartbeat()/sseSweep(); started by listen() once the hub runs on the server's Io (see bindHubs).
  • sse_sweep_ms: ?u64 = null
  • interval_threads: std.ArrayListUnmanaged(IntervalEntry) = .empty
  • boot_hooks: std.ArrayListUnmanaged(BootFn) = .empty: Features' boot() hooks, run by listen() before the first accept.
  • module_entries: std.ArrayListUnmanaged(module_mod.Entry) = .empty: Mounted modules, in the order they start (see mountModules).
  • module_boot_hooks: std.ArrayListUnmanaged(BootFn) = .empty: Modules' boot() hooks, run before the features'.
  • module_shutdown_hooks: std.ArrayListUnmanaged(ShutdownFn) = .empty: Modules' shutdown() hooks, run by deinit() in reverse order.
  • module_mounts: u8 = 0: How many times mountModules ran: more than once is refused by listen().
  • app_migrations: []const migration_mod.Migration = &.{}: The app's own migrations (see migrations()), after the modules'.

Server.deinit

zig
pub fn deinit(self: *Self) void

Frees what the server allocated. Call it with defer after spider.app.

Server.use

zig
pub fn use(self: *Self, m: MiddlewareFn) *Self

Adds a middleware that runs on every request, in the order added, before the route's own access checks, as many as are added. It also runs for a request no route matches (on its way to a 404), but not for a static file, which is answered before any middleware.

Server.useAt

zig
pub fn useAt(self: *Self, path: []const u8, m: MiddlewareFn) *Self

Adds a middleware for path and everything under it: "/admin" covers /admin and /admin/users, not /administrators. "/admin/" and "/admin/*" mean the same as "/admin"; "/" covers every request. It runs after the global ones, matched route or not. path is not copied. At most 32 in the whole server, the ones of mounted groups included: one more panics.

Server.onError

zig
pub fn onError(self: *Self, handler: ErrorHandler) *Self

Sets where every error a handler or a middleware returns ends up, and the error.NotFound of a request no route matches. Without one, the error becomes its default status (spider.statusForError) with a plain text body, or JSON for a caller that wants JSON. If the function itself fails, the answer is a plain 500.

zig
fn onError(c: *spider.Ctx, err: anyerror) !spider.Response {
    if (err == error.NotFound) {
        return c.view("not-found", .{}, .{ .status = .not_found });
    }
    return fallback(c, err);
}
const fallback = spider.errorHandler(.{});

Server.staticDir

zig
pub fn staticDir(self: *Self, dir: []const u8) *Self

Serves the files of dir from the root of the site, in place of the static_dir of the config. dir is not copied.

Server.staticAt

zig
pub fn staticAt(self: *Self, dir: []const u8, prefix: []const u8) *Self

Serves the files of dir under the path prefix, in place of the static_dir of the config: a server has one static directory, and the last of staticDir / staticAt wins.

Server.get

zig
pub fn get(self: *Self, path: []const u8, handler: anytype, comptime config: anytype) *Self

Registers a GET route.

path may have :name segments ("/posts/:id"), read in the handler with c.params.get("id"); a * segment matches any one segment. A literal segment wins over a :name at the same place. A route registered twice for one method and path logs a warning, and the later one wins.

handler is fn (*spider.Ctx) !spider.Response; it may also take spider.Path, spider.Form and spider.Loaded parameters.

config says who may call the route and is required, even when empty (.{}, which declares nothing: the route is open to whoever the app's middlewares let through):

  • .public = true: anyone; auth middlewares let the request pass.
  • .authenticated = true: anyone signed in; 401 otherwise.
  • .roles = &.{"admin"}: a user with one of these roles; 403 otherwise.
  • .org_roles = &.{"owner"}: one of these roles in the active organization, or in any of the user's when none is active; 403 otherwise.
  • .policy = ...: a rule of the app (spider.policy, spider.resourcePolicy, spider.policySet); 403, or 401 without a user.
  • .quiet_log = true: a successful request is not logged.
  • .allow_http = true: served over plain HTTP under spider.forceHttps.

An unknown key is a compile error, and so is .public together with .roles, .org_roles or .authenticated.

zig
server.get("/posts/:id", show, .{ .public = true })

Server.post

zig
pub fn post(self: *Self, path: []const u8, handler: anytype, comptime config: anytype) *Self

Registers a POST route. Path, handler and config as in get.

Server.put

zig
pub fn put(self: *Self, path: []const u8, handler: anytype, comptime config: anytype) *Self

Registers a PUT route. Path, handler and config as in get.

Server.delete

zig
pub fn delete(self: *Self, path: []const u8, handler: anytype, comptime config: anytype) *Self

Registers a DELETE route. Path, handler and config as in get.

Server.patch

zig
pub fn patch(self: *Self, path: []const u8, handler: anytype, comptime config: anytype) *Self

Registers a PATCH route. Path, handler and config as in get.

Server.head

zig
pub fn head(self: *Self, path: []const u8, handler: anytype, comptime config: anytype) *Self

Registers a HEAD route. Path, handler and config as in get.

Server.ws

zig
pub fn ws(self: *Self, path: []const u8, comptime handler: fn (*Ws) anyerror!void) *Self

Registers a WebSocket route (a GET). The handler runs once per connection; when it returns, the connection is closed:

zig
fn chat(ws: *spider.Ws) !void {
    while (try ws.next()) |message| {
        _ = message;
    }
}

An error the handler returns is logged (with the request id) and the client gets a close frame with code 1011 before the connection closes; onError is not called, since no HTTP answer can be sent any more.

Each WebSocket route has a hub of its own (c.wsHub()). The route takes no config, so it declares no access: use wsWith to say who may open it. With require_route_access on, listen() refuses to start while a route registered with ws exists. A request that is not a WebSocket upgrade gets an empty 200.

Server.wsWith

zig
pub fn wsWith(self: *Self, path: []const u8, comptime handler: fn (*Ws) anyerror!void, comptime config: anytype) *Self

ws() with a route config, like get: .public, .authenticated, .roles, .org_roles, .policy, .quiet_log, .allow_http. The checks run on the request that asks for the socket: one that fails them is answered 401 or 403 and the protocol is not switched.

zig
server.wsWith("/chat", chat, .{ .authenticated = true })

Server.wsInterval

zig
pub fn wsInterval(self: *Self, path: []const u8, ms: u64, comptime callback: fn (*Hub) void) *Self

Registers a WebSocket route whose clients only listen (what they send is read and dropped), and calls callback with the route's hub every ms milliseconds, to send to them. The calls start with listen(), on a thread of their own. Like ws, the route takes no config.

Server.sseInterval

zig
pub fn sseInterval(self: *Self, ms: u64, comptime callback: fn (*Hub) void) *Self

Calls callback with the SSE hub every ms milliseconds, on its own thread: a place to publish events on a schedule. The calls start with listen(), the first one ms after it.

Server.sseHeartbeat

zig
pub fn sseHeartbeat(self: *Self, interval_ms: ?u64) *Self

Turns on the heartbeat of the SSE hub: the comment : heartbeat sent to every stream on every interval, which keeps connections open through proxies and load balancers that close idle ones. interval_ms null uses Hub.default_heartbeat_ms (30 s). It starts in listen(); if its thread cannot be started, an error is logged and the server runs without it.

Server.sseSweep

zig
pub fn sseSweep(self: *Self, interval_ms: ?u64) *Self

Turns on the SSE hub's sweep of dead connections — independent of sseHeartbeat, so a channel that's both quiet and never emitted to doesn't accumulate zombie connections indefinitely. interval_ms null uses Hub.default_sweep_ms (60 s). It starts in listen(); if its thread cannot be started, an error is logged and the server runs without it.

Server.sse

zig
pub fn sse(self: *Self, path: []const u8, comptime handler: fn (*Sse) anyerror!void) *Self

Registers a Server-Sent Events route (a GET). The handler receives the stream of one client; see spider.Sse. When it returns, the connection is closed. It declares no access: use sseWith to say who may call it.

Server.sseWith

zig
pub fn sseWith(self: *Self, path: []const u8, comptime handler: fn (*Sse) anyerror!void, comptime config: anytype) *Self

sse() with a route config, the same one as get(): .public, .authenticated, .roles, .org_roles, .policy, .quiet_log, .allow_http.

Server.health

zig
pub fn health(self: *Self, path: []const u8, comptime handler: anytype) *Self

Registers a GET route with an empty config. The server already answers /up.

Server.mountFeatures

zig
pub fn mountFeatures(self: *Self, comptime features: type) *Self

Registers every feature of an app's features module: for each pub const <name> that is a namespace, see mountFeature(). Features are visited in declaration order; route matching doesn't depend on registration order. A namespace that declares spider_module is a module: it is skipped here and mounted with mountModules().

Server.mountModules

zig
pub fn mountModules(self: *Self, comptime modules: anytype) *Self

Mounts modules: namespaces that declare pub const spider_module = spider.module(.{ ... }) (see spider.module). At compile time it checks every manifest, the exact signatures of check, boot, shutdown, routes and jobs (a route builder with parameters is an error, not skipped), that each name is mounted once and that every module a module requires is in the same call, without cycles. It fails the build with a message naming the module otherwise.

Modules start in dependency order (ties keep the order given), before the features: listen() runs every module's checks, then the modules' boot(), then the features'. deinit() runs the modules' shutdown() in reverse order. Mount every module in one call: listen() refuses a second one.

zig
_ = server.mountModules(.{ notify, @import("spider_audit") })
    .mountFeatures(features);

Server.migrations

zig
pub fn migrations(self: *Self, comptime list: []const migration_mod.Migration) *Self

The app's own migrations, embedded in the binary: they run after the modules' ones, with the same guarantees (a transaction per migration, with its schema_migrations row, under a lock; a checksum that refuses an applied migration whose SQL changed), and listen() refuses to start while one is pending. Keys are the bare names, as spider migrate has always recorded the files of src/core/db/migrations/, so the rows already there count as applied. The list comes from spider.addMigrations in build.zig:

zig
// build.zig
exe.root_module.addImport("migrations", spider_build.addMigrations(b, spider_dep, "src/core/db/migrations"));
// main.zig
_ = server.mountModules(.{notify}).migrations(@import("migrations").all);

Fails the build when a name is not NNNN_description, when the names are not in increasing order, or when a file has no SQL.

Server.check

zig
pub fn check(self: *Self, arena: std.mem.Allocator, io: Io) module_mod.Report

Runs what listen() checks before it starts, for every mounted module and the app's own migrations: Spider's checks (required environment variables set; migrations applied, with the checksum they were applied with) and each module's check(). Returns the report, allocated in arena; report.ok() is true when nothing failed. Useful in an app's tests:

zig
const report = server.check(arena, io);
try std.testing.expect(report.ok());

Server.migrate

zig
pub fn migrate(self: *Self, arena: std.mem.Allocator) !usize

Applies every pending migration: the mounted modules' ones first, in the order the modules start, then the app's own (see migrations()). Each runs in its own transaction with its schema_migrations row (key module/name, or the bare name for the app's, with the checksum of its SQL); a migration that cannot run in a transaction (CREATE INDEX CONCURRENTLY) runs alone, outside one. Rows from before checksums get theirs. Returns how many ran (0 when there are none, with or without a database). Fails with error.NoDatabase when the app is built without PostgreSQL, error.DatabaseNotInitialized before spider.pg.init, error.MigrationChanged when an applied migration's SQL differs from what ran, or the database's error (the failed migration is rolled back; the ones before it stay applied). What spider migrate runs (SPIDER_MIGRATE=1).

Server.mountFeature

zig
pub fn mountFeature(self: *Self, comptime F: type) *Self

Registers one feature namespace (usually its mod.zig):

  • routes: every pub fn with no parameters returning spider.Group is mounted (e.g. build(), buildWebhook()).
  • jobs: a tuple of spider.every(ms, fn (*spider.Hub) void).
  • boot: pub fn boot(b: spider.Boot) !void, run by listen() before the first connection (not when only listing routes). A boot() that fails stops listen(), which returns its error. Anything else in the namespace is ignored. mount() stays for groups that live elsewhere.

Server.mount

zig
pub fn mount(self: *Self, child: Group) *Self

Registers the routes of a spider.Group. Features are mounted by mountFeatures; this is for a group built elsewhere.

Server.writeRoutes

zig
pub fn writeRoutes(self: *Self, w: *std.Io.Writer) !void

The route table: method, path, access and flags, sorted by path. Printed by listen() instead of serving when SPIDER_ROUTES is set (what spider routes does).

Server.writeRoutesJson

zig
pub fn writeRoutesJson(self: *Self, w: *std.Io.Writer) !void

The route listing as one line of JSON (SPIDER_ROUTES=json, read by spider routes --json/--check/--lock/--diff): {"auth":bool,"routes":[{"method","path","access","public", "authenticated","roles","org_roles","quiet_log","allow_http", "policy"},"jobs_ms":[..,"duplicates":n}

Server.hasAuth

zig
pub fn hasAuth(self: *Self) bool

True when a middleware installed with use()/useAt() authenticates requests (Spider's providers, or one marked with spider.markAuthMiddleware).

Server.requireRouteAccess

zig
pub fn requireRouteAccess(self: *Self) *Self

Every route must say who may call it (.public, .authenticated, .roles, .org_roles or .policy): listen() refuses to start otherwise, listing the ones that don't. Same as require_route_access = true in spider.config.zig. Off by default.

Server.checkRouteAccess

zig
pub fn checkRouteAccess(self: *Self) !void

The boot check behind require_route_access: logs every route without declared access and fails with error.RouteAccessUndeclared. Does nothing when the option is off.

Server.listen

zig
pub fn listen(self: *Self, options: ListenOptions) !void

Starts the server and serves until the process ends. Before the first connection it runs the features' boot() hooks and starts their jobs.

zig
try server.listen(.{});
try server.listen(.{ .port = 8080, .host = "0.0.0.0" });

With SPIDER_ROUTES set it prints the route table and returns instead (what spider routes runs). With SPIDER_CHECK set it prints the modules' report (see check) and returns, with error.ModulesNotReady when a module failed (spider check --modules). With SPIDER_MIGRATE set it applies the migrations, the modules' and then the app's (see migrate), and returns (spider migrate; see also spider.migrating).

Before it binds the address it runs every module's checks and fails with error.ModulesNotReady, the report in the log, when one is not ready. It also fails with error.RouteAccessUndeclared under require_route_access, with the error of a module's or a feature's boot(), or with the system's error when the address cannot be bound (a port in use, a host that is not an IP address).

spider.ListenOptions

zig
pub const ListenOptions = struct

The argument of Server.listen. .{} takes both from the app's config.

The port is, in this order: spider dev --port, .port here, the PORT variable (environment or .env), then spider.config.zig. .host replaces Config.host. Under spider.testing.start both are ignored: the server listens on 127.0.0.1, on the port the test chose.

Fields

  • port: ?u16 = null
  • host: ?[]const u8 = null

spider.Job

zig
pub const Job = struct

A periodic task a feature declares: pub const jobs = .{spider.every(60_000, retryLoop)}; Runs on its own thread every every_ms with the SSE hub (like sseInterval). listen() starts it; the first run comes one interval later.

Fields

  • every_ms: u64
  • run: *const fn (*Hub) void

spider.every

zig
pub fn every(every_ms: u64, run: *const fn (*Hub) void) Job

A job that runs run every every_ms milliseconds, for a feature's jobs:

zig
pub const jobs = .{spider.every(60_000, cleanUp)};

fn cleanUp(hub: *spider.Hub) void {
    _ = hub;
    // runs once a minute, on its own thread
}

spider.Boot

zig
pub const Boot = struct

Passed to a feature's or a module's boot() and to a module's shutdown() (see Server.mountFeatures and Server.mountModules).

Fields

  • allocator: std.mem.Allocator: Thread-safe, lives for the whole process.
  • io: Io: The server's Io.

spider.StaticConfig

The same as spider.static.StaticConfig.

spider.static

Static file serving. The server does it by itself, before routing, for the static_dir of the config (or Server.staticDir / Server.staticAt); apps only see StaticConfig.

spider.static.StaticConfig

zig
pub const StaticConfig = struct

Where static files are read from and under which URL path they are served. Apps set it through Config.static_dir, Server.staticDir(dir) or Server.staticAt(dir, prefix).

Files are answered before routing and before any middleware, to any method: they are always public. The prefix itself serves index.html. A path with .. in it, or one that names no file, goes on to the routes. The content type comes from the extension (application/octet-stream when unknown). Every answer has an ETag (a matching If-None-Match gets 304) and Cache-Control: no-cache, or public, max-age=31536000, immutable when the URL has a v query parameter (/app.css?v=3).

Fields

  • dir: []const u8 = "./public": The directory of the files, relative to the working directory. Empty: no static files.
  • prefix: []const u8 = "/": The start of the URL paths answered from dir: with /assets, /assets/app.css is the file app.css of dir. It is compared as text: /assets also takes /assets-old/x.css, as the file -old/x.css.
  • max_file_bytes: usize = 10 * 1024 * 1024: The largest file served. A file is read whole into memory for each request that gets it, so this bounds what one request can take. A bigger file is not served (the request goes on to the routes, which answer 404) and the server logs which file and which limit. Set it with static_max_file_bytes in spider.config.zig.

spider.health

spider.health: the two health-check handlers every spider.app has, at /up and /_spider/health.

spider.health.init

zig
pub fn init() void

Records the time health counts the uptime from. spider.app and spider.appWithConfig call it.

spider.health.up

zig
pub fn up(c: *Ctx) !Response

Answers 200 with the text OK. Registered at /up, public and left out of the request log: what a load balancer or a deploy script polls.

spider.health.health

zig
pub fn health(c: *Ctx) !Response

Answers 200 with {"status":"ok","uptime_seconds":N}, N being the seconds since init. Registered at /_spider/health, public and left out of the request log. It checks nothing else (not the database).

What the declarations above take or give back, without a name of its own in spider.

StaticConfig

zig
pub const StaticConfig = static_mod.StaticConfig

Where static files are served from; see Server.staticDir.