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.
Маршруты и обработчики#
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; иначе сжатие на лету с кэшем; для встроенных файлов сжатие делается при сборке |
| Range | Range-запросы и 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.
Подключение к кластеру#
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.
Пример: сервер документации#
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