II.4 Web: `std.web`
A module for sites and HTTP APIs on top of std.http: routes, static files, caching, compression, templates, middleware. Routes are attached to the cluster in the same way as services.
Implementation-defined. Contents of
std.web(D-8054; D-7884). In this implementation the module contains a subset sufficient for the documentation server:Site.embedded(files, rules)over the result ofcomptime.embedDir,Site.reply(req),Site.count/has,cacheRule(pattern, control). Behaviour: onlyGETandHEAD(405 otherwise);/and/dir/serveindex.html,/dirgives 308 to/dir/; paths with..,//,\give 404 (the site's404.htmlpage, if present); there is no listing;ETagandIf-None-Match→ 304;Cache-Controlby the first matching rule (a pattern with one*,public, max-age=300by default);Vary: Accept-Encoding; stored gzip data is served to clients that accept gzip, to the others it is served inflated;X-Content-Type-Options: nosniff,Referrer-Policy: no-referrer. The rest of this part consists of optional features not implemented in 1.0:Routerwithget/static/with,StaticFs.dir,Range/206, ready-made.br, MIME overrides, templates (html"…",comptime.template), middleware (compress,cors,logRequests,rateLimit,requireUser,csrf,securityHeaders),routes(r)inexpose.
Routes and handlers#
use std.web { Router, Request, Response, StaticFs, html }
fn hello(req: Request) -> Response pure =>
Response.html(html"<h1>Hello, {req.query(\"name\") ?? \"guest\"}</h1>") // interpolation is escaped
fn userPage(req: Request) -> Response | NotFound | NetError net {
let id = UserId(req.param("id").parse<u64>()?) // :id from the path
let u = Users.get(id)? // an ordinary rpc
Response.json(u) // serialisation to JSON per §9.3
}
pub static SITE: Router = Router.new()
.get("/", hello)
.get("/users/:id", userPage)
.static("/assets", comptime.embedDir("web/assets")) // files embedded in the binary
.static("/docs", StaticFs.dir(DOCS_DIR), spa: false) // files from disk
.with(compress(), logRequests())- A handler is a function
(Request) -> Response | E. Errors are mapped to HTTP codes:NotFound→ 404,AccessDenied→ 401/403,DecodeError→ 400,Timeout→ 504, other@errors → by@http_status(code)on the type, 500 by default. - Path parameters
:nameand*rest, query, headers, cookies:req.param,req.query,req.header,req.cookie. Body:req.json<T>(),req.form<T>()(with checking of refined types),req.bytes(),req.stream(). - Responses:
Response.html,json,text,bytes,file,redirect,stream(stream<bytes>),sse(stream<T>);.status(code),.header(k, v),.cookie(…). ctx.callerand policies work as for rpc;.with(requireUser())and@allowon the handler function.
Static files#
| Feature | Behaviour |
|---|---|
| Sources | comptime.embedDir(path) — files are embedded in the binary at build time; StaticFs.dir(path) — from disk at run time |
| Path safety | leaving the root (.., symlinks pointing outside) — 404; directory listing is off |
| Index and SPA | /dir/ → dir/index.html; spa: true — unknown paths return index.html |
| Caching | ETag by content hash (computed at build time for embedded files), Last-Modified, 304 Not Modified; cache: ["*.css" => 1y.immutable, "*.html" => noCache] |
| Compression | ready-made .br/.gz next to the file are served by Accept-Encoding; otherwise on-the-fly compression with a cache; for embedded files compression is done at build time |
| Range | Range requests and 206 Partial Content (video, large PDFs) |
| MIME | by extension, the table is built in; override mime: {"oxlrec" => "application/octet-stream"} |
| Methods | GET and HEAD; others — 405 |
Templates#
- The prefixed literal
html"…"checks markup at compile time and escapes interpolations;raw(s)— insertion without escaping (the analyzer marks it with a warning). comptime.template("web/page.html")compiles a template file into a function with typed parameters: errors in the template are compile errors.
Middleware#
compress(), cors(origins), logRequests(), rateLimit(rps, by), requireUser(), csrf(), securityHeaders() (HSTS, CSP, X-Content-Type-Options); custom ones are functions (Request, Next) -> Response.
Connecting to the cluster#
cluster docs {
role web { expose 443 tls(acme: DOCS_DOMAIN) routes(SITE); replicas 2.. }
}routes(r)inexposeadds routes to the external listener. gRPC, the HTTP/JSON gateway of the services and the routes coexist on one port: gRPC is recognised bycontent-type, the gateway by the prefix/api/, everything else goes to theRouter.- Without a
cluster:std.web.serve(SITE, addr: ":8080")inmain.
Example: the documentation server#
use std.web { Router, StaticFs, securityHeaders, compress }
const DOCS_DOMAIN = "docs.oxl.dev"
// The site is built by the command oxl doc --site into the directory build/site and embedded in the binary:
// one file with no external dependencies, ETag and compression computed at build time
pub static SITE: Router = Router.new()
.static("/", comptime.embedDir("build/site"),
cache: ["*.html" => noCache, "assets/*" => 1y.immutable])
.get("/pdf", _ => Response.file("build/oxl-reference.pdf")) // a dump from oxl doc --pdf
.with(compress(), securityHeaders())
cluster docs { role web { expose 443 tls(acme: DOCS_DOMAIN) routes(SITE); replicas 2.. } }Source: docs/spec/oxl-standard.en.md