OxL Справочник OxL
РусскийEnglish

II.4 Веб: `std.web`

Модуль для сайтов и HTTP-API поверх std.http: маршруты, статика, кэширование, сжатие, шаблоны, middleware. Маршруты подключаются к кластеру так же, как сервисы.

Определяется реализацией. Состав std.web (D-8054; D-7884). В этой реализации модуль содержит подмножество, достаточное для сервера документации: Site.embedded(files, rules) над результатом comptime.embedDir, Site.reply(req), Site.count/has, cacheRule(pattern, control). Поведение: только GET и HEAD (иначе 405); / и /dir/ отдают index.html, /dir — 308 на /dir/; пути с .., //, \ — 404 (страница 404.html сайта, если есть); листинга нет; ETag и If-None-Match → 304; Cache-Control по первому подходящему правилу (шаблон с одной *, по умолчанию public, max-age=300); Vary: Accept-Encoding; сжатые gzip данные отдаются клиентам, принимающим gzip, остальным — распакованными; X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer. Остальное в этой части — необязательные возможности, не реализованные в 1.0: Router с get/static/with, StaticFs.dir, Range/206, готовые .br, переопределение MIME, шаблоны (html"…", comptime.template), middleware (compress, cors, logRequests, rateLimit, requireUser, csrf, securityHeaders), routes(r) в expose.

Маршруты и обработчики#

oxl
use std.web { Router, Request, Response, StaticFs, html }

fn hello(req: Request) -> Response pure =>
  Response.html(html"<h1>Привет, {req.query(\"name\") ?? \"гость\"}</h1>")   // интерполяция экранируется

fn userPage(req: Request) -> Response | NotFound | NetError  net {
  let id = UserId(req.param("id").parse<u64>()?)        // :id из пути
  let u = Users.get(id)?                                  // обычный rpc
  Response.json(u)                                        // сериализация в JSON по §9.3
}

pub static SITE: Router = Router.new()
  .get("/", hello)
  .get("/users/:id", userPage)
  .static("/assets", comptime.embedDir("web/assets"))   // файлы встроены в бинарник
  .static("/docs", StaticFs.dir(DOCS_DIR), spa: false)   // файлы с диска
  .with(compress(), logRequests())
  • Обработчик — функция (Request) -> Response | E. Ошибки отображаются в коды HTTP: NotFound → 404, AccessDenied → 401/403, DecodeError → 400, Timeout → 504, остальные @error → по @http_status(code) на типе, по умолчанию 500.
  • Параметры пути :name и *rest, query, заголовки, cookie: req.param, req.query, req.header, req.cookie. Тело: req.json<T>(), req.form<T>() (с проверкой уточнённых типов), req.bytes(), req.stream().
  • Ответы: Response.html, json, text, bytes, file, redirect, stream(stream<bytes>), sse(stream<T>); .status(code), .header(k, v), .cookie(…).
  • ctx.caller и политики работают так же, как для rpc; .with(requireUser()) и @allow на функции-обработчике.

Статика#

ВозможностьПоведение
Источникиcomptime.embedDir(path) — файлы встраиваются в бинарник при сборке; StaticFs.dir(path) — с диска во время работы
Безопасность путейвыход за корень (.., симлинки наружу) — 404; листинг каталогов выключен
Index и SPA/dir/ → dir/index.html; spa: true — неизвестные пути отдают index.html
КэшированиеETag по хешу содержимого (для встроенных — вычислен при сборке), Last-Modified, 304 Not Modified; cache: ["*.css" => 1y.immutable, "*.html" => noCache]
Сжатиеготовые .br/.gz рядом с файлом отдаются по Accept-Encoding; иначе сжатие на лету с кэшем; для встроенных файлов сжатие делается при сборке
RangeRange-запросы и 206 Partial Content (видео, большие PDF)
MIMEпо расширению, таблица встроена; переопределение mime: {"oxlrec" => "application/octet-stream"}
МетодыGET и HEAD; остальные — 405

Шаблоны#

  • Префиксный литерал html"…" проверяет разметку при компиляции и экранирует интерполяции; raw(s) — вставка без экранирования (анализатор помечает предупреждением).
  • comptime.template("web/page.html") компилирует файл-шаблон в функцию с типизированными параметрами: ошибки в шаблоне — ошибки компиляции.

Middleware#

compress(), cors(origins), logRequests(), rateLimit(rps, by), requireUser(), csrf(), securityHeaders() (HSTS, CSP, X-Content-Type-Options), свои — функции (Request, Next) -> Response.

Подключение к кластеру#

oxl
cluster docs {
  role web { expose 443 tls(acme: DOCS_DOMAIN) routes(SITE); replicas 2.. }
}
  • routes(r) в expose добавляет маршруты к внешнему слушателю. gRPC, HTTP/JSON-шлюз сервисов и маршруты сосуществуют на одном порту: gRPC определяется по content-type, шлюз — по префиксу /api/, остальное идёт в Router.
  • Без cluster: std.web.serve(SITE, addr: ":8080") в main.

Пример: сервер документации#

oxl
use std.web { Router, StaticFs, securityHeaders, compress }

const DOCS_DOMAIN = "docs.oxl.dev"

// Сайт собран командой oxl doc --site в каталог build/site и встроен в бинарник:
// один файл без внешних зависимостей, ETag и сжатие посчитаны при сборке
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"))   // дамп из oxl doc --pdf
  .with(compress(), securityHeaders())

cluster docs { role web { expose 443 tls(acme: DOCS_DOMAIN) routes(SITE); replicas 2.. } }

Исходник: docs/spec/oxl-standard.md

OxL 1.0.0-beta · Версия документации 1.0.0-beta© OxwaldLink · сгенерировано из Стандарта, исходников std и руководства