OxL Reference

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 of comptime.embedDir, Site.reply(req), Site.count/has, cacheRule(pattern, control). Behaviour: only GET and HEAD (405 otherwise); / and /dir/ serve index.html, /dir gives 308 to /dir/; paths with .., //, \ give 404 (the site's 404.html page, if present); there is no listing; ETag and If-None-Match → 304; Cache-Control by the first matching rule (a pattern with one *, public, max-age=300 by 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: Router with get/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) in expose.

Routes and handlers#

oxl
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 :name and *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.caller and policies work as for rpc; .with(requireUser()) and @allow on the handler function.

Static files#

FeatureBehaviour
Sourcescomptime.embedDir(path) — files are embedded in the binary at build time; StaticFs.dir(path) — from disk at run time
Path safetyleaving the root (.., symlinks pointing outside) — 404; directory listing is off
Index and SPA/dir/ → dir/index.html; spa: true — unknown paths return index.html
CachingETag by content hash (computed at build time for embedded files), Last-Modified, 304 Not Modified; cache: ["*.css" => 1y.immutable, "*.html" => noCache]
Compressionready-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
RangeRange requests and 206 Partial Content (video, large PDFs)
MIMEby extension, the table is built in; override mime: {"oxlrec" => "application/octet-stream"}
MethodsGET 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#

oxl
cluster docs {
  role web { expose 443 tls(acme: DOCS_DOMAIN) routes(SITE); replicas 2.. }
}
  • routes(r) in expose adds routes to the external listener. gRPC, the HTTP/JSON gateway of the services and the routes coexist on one port: gRPC is recognised by content-type, the gateway by the prefix /api/, everything else goes to the Router.
  • Without a cluster: std.web.serve(SITE, addr: ":8080") in main.

Example: the documentation server#

oxl
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

OxL 1.0.0-beta · Documentation version 1.0.0-beta© OxwaldLink · generated from the Standard, the std sources and the guides