Middleware
A middleware is a function that runs around a handler. It can stop the request, let it through, or change the response on the way back.
The code on this page is from Posts, the example app in examples/posts (source). Its JSON API has two middlewares.
Writing one
It receives the request and next, the rest of the chain. Calling next(c) runs the handler and gives back its response.
/// Lets a request through only when it carries the API key in the
/// X-Api-Key header. Anything else ends here with 401: the handler never
/// runs.
pub fn requireKey(c: *spider.Ctx, next: spider.NextFn) anyerror!Response {
const sent = c.header("X-Api-Key") orelse return error.Unauthorized;
if (!std.mem.eql(u8, sent, apiKey())) return error.Unauthorized;
return next(c);
}
Not calling next ends the request there: the handler never runs.
Changing the response
Call next first, then work on what came back. Memory for the request comes from c.arena and is freed when the request ends.
/// Tells browsers and proxies not to keep a copy of the answer. It calls
/// `next` first, then adds a header to what came back.
pub fn noStore(c: *spider.Ctx, next: spider.NextFn) anyerror!Response {
var res = try next(c);
const headers = try c.arena.alloc([2][]const u8, res.headers.len + 1);
@memcpy(headers[0..res.headers.len], res.headers);
headers[res.headers.len] = .{ "Cache-Control", "no-store" };
res.headers = headers;
return res;
}
One group of routes
A group can have its own middlewares. They run for the routes of that group only, in the order they were added:
/// The same posts as JSON, for programs. Two middlewares wrap every route
/// of this group, and only of this group: the first asks for the API key,
/// the second marks the answers as not to be cached.
pub fn api() spider.Group {
var g = spider.Group.init("/api/posts");
_ = g
.defaults(.{ .public = true })
.use(api_handlers.requireKey)
.use(api_handlers.noStore)
.get("", api_handlers.list, .{})
.get("/:id", api_handlers.show, .{})
.post("", api_handlers.create, .{});
return g;
}
The pages of Posts are another group, and neither middleware touches them.
The whole app
use on the server puts a middleware on every request, before the ones of a group. Posts has two: the logger, and the one that reads the login cookie.
/// Builds the server and listens. main() calls it, and so do the tests in
/// src/app_test.zig.
pub fn serve(allocator: std.mem.Allocator, io: std.Io) !void {
// What the server needs before it starts goes here. Posts needs
// nothing yet.
_ = allocator;
_ = io;
var server = spider.app(.{});
defer server.deinit();
// Routes live with their feature (src/features/<name>/routes.zig);
// mountFeatures registers every feature listed in src/features/mod.zig.
// `spider routes` prints what got registered.
try server
.use(spider.logger)
// Reads the login cookie (or a bearer token) and tells the routes
// who is there; a route that is not `.public` needs a login.
.use(spider.session.middleware())
.mountFeatures(features)
.onError(onError)
// Port and host come from spider.config.zig; the PORT variable
// (the environment or .env) replaces the port.
.listen(.{});
}
server.useAt("/admin/*", check) puts one on every path under a prefix instead, whatever group the routes are in.
Ready-made
spider.loggerprints one line per request: status, method, path and time.spider.session.middleware()reads the login cookie and tells the routes who is signed in. See Login.spider.gzipcompresses responses for clients that accept it.spider.varyHtmxaddsVary: HX-Requestto HTML responses.spider.forceHttps(.{ .default_base_url = "https://example.com" })redirects plain HTTP to HTTPS behind a proxy.
Errors
An error returned by a handler or a middleware goes to the error handler given to onError. spider.errorHandler is the ready-made one, and a new project uses it. Posts wraps it in a function of its own, to answer the API in JSON and to show its 404 page:
/// Where every error a handler or a middleware returns ends up. The API
/// answers in JSON; a missing post or page gets the 404 page, a post that
/// is someone else's the 403 page; anything else gets Spider's own answer.
fn onError(c: *spider.Ctx, err: anyerror) anyerror!spider.Response {
if (std.mem.startsWith(u8, c.getPath(), "/api/")) return api_errors(c, err);
if (err == error.NotFound) {
return c.view("not-found", .{ .title = "Page not found" }, .{ .status = .not_found });
}
return fallback(c, err);
}
const fallback = spider.errorHandler(.{
.unauthorized_redirect = "/auth/login",
.forbidden_view = "forbidden",
});
const api_errors = spider.errorHandler(.{ .always_json = true });