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.appreturns; its methods register routes and middlewares, andlisten()runs it. - spider.ListenOptions: The argument of
server.listen:.portand.host, both optional. - spider.Job: A periodic task of a feature's
jobs;spider.everybuilds one. - spider.every: A job for a feature's
jobs:spider.every(60_000, run)callsrunonce a minute. - spider.Boot: What a feature's
boot()receives: an allocator and the server'sIo. - spider.StaticConfig: Where static files are served from.
- spider.static: Static file serving; the server uses it by itself for the
static_dirof the config. - spider.health: The handlers behind
/upand/_spider/health, whichspider.appregisters.
spider.app
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).
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
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
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:
var server = spider.app(.{});
defer server.deinit();
try server
.use(spider.logger)
.mountFeatures(features)
.onError(onError)
.listen(.{});
Fields
spider_arena: std.heap.ArenaAllocatorspider_gpa: std.heap.DebugAllocator(.{})allocator: std.mem.Allocatorgpa: std.mem.Allocatorrouter: Routerdecorations: Tglobal_middlewares: std.ArrayListUnmanaged(MiddlewareFn) = .emptypath_middlewares: [32]PathMiddlewareEntry = undefinedpath_middleware_count: usize = 0error_handler: ?ErrorHandler = nullstatic_config: StaticConfig = .{ .dir = "./public", .prefix = "/" }config: Config = default_configviews_index: ?views_mod.ViewsIndex = nullws_route_hubs: std.ArrayListUnmanaged(WsRouteHub) = .emptysse_hub: ?Hub = nullsse_threaded: ?std.Io.Threaded = nullsse_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 = nullinterval_threads: std.ArrayListUnmanaged(IntervalEntry) = .emptyboot_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
pub fn deinit(self: *Self) void
Frees what the server allocated. Call it with defer after spider.app.
Server.use
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
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
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.
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
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
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
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 underspider.forceHttps.
An unknown key is a compile error, and so is .public together with .roles, .org_roles or .authenticated.
server.get("/posts/:id", show, .{ .public = true })
Server.post
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
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
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
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
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
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:
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
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.
server.wsWith("/chat", chat, .{ .authenticated = true })
Server.wsInterval
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
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
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
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
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
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
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
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
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.
_ = server.mountModules(.{ notify, @import("spider_audit") })
.mountFeatures(features);
Server.migrations
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:
// 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
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:
const report = server.check(arena, io);
try std.testing.expect(report.ok());
Server.migrate
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
pub fn mountFeature(self: *Self, comptime F: type) *Self
Registers one feature namespace (usually its mod.zig):
routes: everypub fnwith no parameters returningspider.Groupis mounted (e.g. build(), buildWebhook()).jobs: a tuple ofspider.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
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
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
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
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
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
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
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.
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
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 = nullhost: ?[]const u8 = null
spider.Job
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: u64run: *const fn (*Hub) void
spider.every
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:
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
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
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 fromdir: with/assets,/assets/app.cssis the fileapp.cssofdir. It is compared as text:/assetsalso 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 withstatic_max_file_bytesinspider.config.zig.
spider.health
spider.health: the two health-check handlers every spider.app has, at /up and /_spider/health.
spider.health.init
pub fn init() void
Records the time health counts the uptime from. spider.app and spider.appWithConfig call it.
spider.health.up
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
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).
Related
What the declarations above take or give back, without a name of its own in spider.
StaticConfig
pub const StaticConfig = static_mod.StaticConfig
Where static files are served from; see Server.staticDir.