Middlewares
On this page:
- spider.logger: Middleware that logs one line per request:
server.use(spider.logger). - spider.loggerWith: Request logger with options (quiet paths, stream opens) — see modules/logger.zig.
- spider.logFn: std.log function with UTC timestamps:
pub const std_options: std.Options = .{ .logFn = spider.logFn }; - spider.gzip: Middleware that compresses response bodies of 1024 bytes or more for clients that accept gzip.
- spider.forceHttps:
spider.forceHttps(.{ .default_base_url = "https://example.com" }): a middleware that redirects plain HTTP to HTTPS behind a TLS proxy (honors route .allow_http). - spider.ForceHttpsOptions: Options of
spider.forceHttps. - spider.varyHtmx: Middleware that adds
Vary: HX-Requestto HTML responses. - spider.errorHandler:
spider.errorHandler(.{}): a ready-made onError that answers JSON, an htmx toast or a page (see modules/errors.zig). - spider.markAuthMiddleware: Tells Spider that
mwauthenticates requests (an app's own session middleware); Spider's providers mark theirs. See modules/auth_marker.zig. - spider.ErrorHandlerOptions: Options of
spider.errorHandler. - spider.dbgRequest: Middleware that prints each request (method, target, headers and body) to stderr, for debugging.
- spider.dbgResponse: Middleware that prints each response (status, headers and body) to stderr, for debugging.
spider.logger
pub fn middleware(c: *Ctx, next: NextFn) anyerror!Response
The request logger (spider.logger): one line per request on stderr, written when the response is ready:
2026-09-26T17:36:33.178Z [200] GET /tickets/42 3.1ms rid=… user=… org=…
user is the auth subject (an id, never email/name) and org the active org, both filled in by the auth middleware later in the chain ("-" when absent). The query string is left out: it can carry tokens.
A successful request (status under 400) of a route with .quiet_log is not logged. A request that fails with an error is logged with the status that error has by default (spider.statusForError), its name and its detail. An SSE or WebSocket stream gets its line when it ends, with stream before how long it stayed open.
spider.loggerWith
pub fn with(comptime opts: Options) MiddlewareFn
The request logger with options (spider.loggerWith): a middleware for server.use.
server.use(spider.loggerWith(.{ .quiet_paths = &.{"/keepalive"} }))
spider.logFn
pub fn logFn(
comptime level: std.log.Level,
comptime scope: @EnumLiteral(),
comptime format: []const u8,
args: anytype,
) void
Drop-in std.log function that prefixes every line with a UTC timestamp:
pub const std_options: std.Options = .{ .logFn = spider.logFn };
A line is 2026-09-26T17:36:33.178Z error (scope): message, written to stderr; the scope is left out for the default one.
spider.gzip
pub fn middleware(c: *Ctx, next: NextFn) anyerror!Response
Compresses the response body with gzip when the request's Accept-Encoding mentions gzip and the body has at least 1024 bytes, and adds content-encoding: gzip and vary: accept-encoding. Smaller bodies, responses without a body and streams (SSE, WebSocket) pass unchanged. The compressed body is allocated in the request arena.
server.use(spider.gzip)
spider.forceHttps
pub fn forceHttps(comptime opts: Options) MiddlewareFn
A middleware that redirects a request that arrived over plain HTTP to the same path and query on the HTTPS origin (302). The scheme is read from proto_header: the app must be behind a proxy that sets it. A request without that header is taken as HTTPS and passes. Routes with .allow_http = true and the allow_http_paths are not redirected.
server.use(spider.forceHttps(.{
.default_base_url = "https://example.test",
.allow_http_paths = &.{ "/legacy*", "/exact" },
}))
spider.ForceHttpsOptions
pub const Options = struct
Options of spider.forceHttps. Only default_base_url is required.
Fields
base_url_env: []const u8 = "BASE_URL": Env var with the public origin ("https://example.com", no final slash), read for each request that is redirected.default_base_url: []const u8: Used when the env var is unset.proto_header: []const u8 = "X-Forwarded-Proto": Header carrying the original scheme. Without it (dev, no proxy) the request is taken as HTTPS and never redirected. When it lists several values (a chain of proxies: "https, http"), the first one counts: the one the client used.allow_http_paths: []const []const u8 = &.{}: Request targets that are never redirected: an exact match (the query string counts), or a prefix when the entry ends in*. Unlike a route's.allow_http, it also covers paths no route matches.
spider.varyHtmx
pub fn varyHtmx(c: *Ctx, next: NextFn) anyerror!Response
A middleware that adds Vary: HX-Request to every response whose content type is text/html, so a cache keeps the htmx fragment and the full page of one URL apart. Other responses pass unchanged.
server.use(spider.varyHtmx)
spider.errorHandler
pub fn errorHandler(comptime opts: Options) ErrorHandler
A ready-made handler for Server.onError: it turns an error returned by a handler or a middleware into the response its caller can use (JSON, an htmx toast event, a page or plain text; see the top of this file). The status is the one of spider.statusForError.
server.onError(spider.errorHandler(.{ .template_not_found_is_404 = true }))
An app that treats some errors itself calls it for the rest:
const fallback = spider.errorHandler(.{
.unauthorized_redirect = "/auth/login",
.forbidden_view = "forbidden",
});
spider.markAuthMiddleware
pub fn mark(mw: MiddlewareFn) void
Tells Spider that mw is a middleware that authenticates requests (spider.markAuthMiddleware). An app with its own session or token middleware marks it before listen(); Spider's providers mark theirs. With auth present, the route listing and spider routes --check flag the routes that declare no access. Marking twice is harmless. Panics past 16 marked middlewares.
spider.ErrorHandlerOptions
pub const Options = struct
Options of spider.errorHandler. Every field has a default.
Fields
unauthorized_redirect: ?[]const u8 = null: error.Unauthorized redirects here for every kind of request (null: answered like any other error, 401).json_key: []const u8 = "error": Key of the message in JSON error bodies.always_json: bool = false: Answer JSON to every request, whatever its Accept header says (for API-only apps: a client that sends no Accept still gets JSON).toast_event: []const u8 = "spider:toast": htmx event name raised through HX-Trigger.forbidden_view: ?[]const u8 = null: View rendered for a full-page 403, with.user_name(the auth name claim). null: plain text.template_not_found_is_404: bool = false: error.TemplateNotFound answers 404 instead of 500.messages: Messages = .{}: The message shown for each status.
spider.dbgRequest
pub fn middleware(c: *Ctx, next: NextFn) anyerror!Response
Prints the incoming HTTP request shaped like the Web standard Request interface (method, url, headers, body) -- a teaching aid for introducing the Fetch API request/response model. Pairs with dbgResponse, which prints the matching Response side after the handler runs.
spider.dbgResponse
pub fn middleware(c: *Ctx, next: NextFn) anyerror!Response
Prints the outgoing HTTP response shaped like the Web standard Response interface (status, statusText, ok, headers, body) -- pairs with dbgRequest for teaching the Fetch API request/response model.
Related
What the declarations above take or give back, without a name of its own in spider.
Options
pub const Options = struct
Options of spider.loggerWith.
Fields
quiet_paths: []const []const u8 = &.{}: Paths whose successful (< 400) requests are not logged — heartbeats, polling, health checks. Segment globs: "" matches one segment, a trailing "*" matches the rest ("/tickets//presence", "/api/access/intelbras/*"). Errors and 4xx/5xx are always logged.log_stream_open: bool = true: Also log SSE/WebSocket connections: one line when the stream ends, withstreamand how long it stayed open.
nowNs
pub fn nowNs() i128
Wall clock in nanoseconds since the Unix epoch (libc clock_gettime — usable from std.log, which has no Io at hand). 0 when the clock cannot be read.
utc
pub fn utc(buf: *[24]u8, ns: i128) []const u8
Formats ns (nanoseconds since the Unix epoch) as UTC with milliseconds, into buf: "2026-09-26T17:36:33.178Z". The result is a slice of buf. An instant before 1970 is shown as the start of 1970.
colorEnabled
pub fn colorEnabled() bool
ANSI colors only when stderr is a terminal; a container log (docker, kamal) gets plain text instead of "\x1b[32m" noise.
Messages
pub const Messages = struct
The texts spider.errorHandler shows, by status. Replace the ones the app words differently (or translates): .messages = .{ .not_found = "..." }. A status not listed here (401, 413, ...) shows the name of the error.
Fields
forbidden: []const u8 = "You don't have permission to do this.": 403.not_found: []const u8 = "Not found.": 404.conflict: []const u8 = "This record already exists or is still in use.": 409.unprocessable: []const u8 = "This operation isn't allowed.": 422.busy: []const u8 = "The system is busy right now. Try again in a moment.": 503.bad_request: []const u8 = "Invalid data. Check it and try again.": Used for 400 when the error carries no detail (c.setErrorDetail).internal: []const u8 = "Unexpected error. Try again (ref. {s}).": 5xx; must contain one{s}, replaced by the request id.