Middleware
Middleware оборачивает Handler в новый Handler — Go/chi’шный
func(http.Handler) http.Handler, один в один. Обобщённый Service из
Tower (poll_ready/backpressure) сознательно не портирован: у
fiber-модели Nova нет async-сигнала готовности как класса, а backpressure
уже живёт на уровне accept-loop (ServerPolicy.max_inflight — см.
serving.md).
Исходник: src/middleware.nv.
Содержание
- Канон-форма:
middleware(fn(req, next)) Router.@useи порядок@then: композиция двух промежуточных обработчиков- Router
@useи@nest - Что оборачивается
- Пишем свой — в стиле батареек
- Связанные документы
Канон-форма: middleware(fn(req, next))
export fn middleware(f fn(ServerRequest, Handler) -> ServerResponse) -> Middleware
Строим промежуточный обработчик из одного плоского замыкания — без вложенной
церемонии fn(next) -> Handler. next — обычное значение Handler:
вызываем напрямую (next(req)), выполняем код до/после него, либо
коротко замыкаем, вернув свой ответ без вызова вовсе.
fn add_tag(tag str, next Handler, req ServerRequest) -> ServerResponse {
mut resp = next(req)
ro prev = hdr(resp, "x-order")
// Prepend on the way back out: the OUTERMOST layer's post-work runs
// LAST (it called `next` first, so it unwinds last), so prepending
// makes left-to-right in the final header match request-time order.
resp.header("x-order", if prev == "" { tag } else { "${tag},${prev}" })
resp
}
fn tag_layer(tag str) -> Middleware {
middleware(fn(req ServerRequest, next Handler) -> ServerResponse => add_tag(tag, next, req))
}
Сам Middleware — голый newtype над fn(Handler) -> Handler
(Middleware.new(f) — низкоуровневая «силовая» форма для одноразовой
настройки, которая должна выполняться один раз за композицию, а не один раз
на запрос — вместо неё почти всегда стоит использовать middleware(...)
выше).
Router.@use и порядок
test "middleware: canon middleware(fn(req, next)) form; first .use() call is outermost" {
mut r = Router.new()
r.use(tag_layer("A"))
r.use(tag_layer("B"))
r.get("/x", fn(req ServerRequest) -> ServerResponse => ServerResponse.text(StatusCode.OK, "base"))!!
ro wire = serve_once(r, get_req("/x"))
// request order A -> B -> handler; unwinding tags the header A,B
assert(wire_str(wire).contains("x-order: A,B"))
}
Первый вызов .use() — самый внешний слой, и он же выполняется
первым во время запроса (r.use(a); r.use(b) → поток запроса
a → b → handler) — это настоящая семантика chi (chi’шный chain()
строит mws[0] самым внешним слоем), то же правило, которому следует
Express для app.use(a); app.use(b). Слои накапливаются на маршрутизаторе и
запекаются в обработчик каждого route’а в момент регистрации (@route/
@get/…/@nest — все сходятся в одной точке вставки) — одна обёртка
замыканием на route при setup’е, а не одна на запрос.
Оборачиваются только routes, зарегистрированные после .use() —
то же правило, что документирует chi: добавляйте промежуточные обработчики
до routes, которые они должны покрывать.
@then: композиция двух промежуточных обработчиков
test "middleware: @then composes two middlewares into one (same order as two .use() calls)" {
mut r = Router.new()
r.use(tag_layer("A").then(tag_layer("B")))
r.get("/x", fn(req ServerRequest) -> ServerResponse => ServerResponse.text(StatusCode.OK, "base"))!!
ro wire = serve_once(r, get_req("/x"))
assert(wire_str(wire).contains("x-order: A,B"))
}
a.then(b) компонует a снаружи, b внутри —
a.then(b).apply(h) == a.apply(b.apply(h)) — ровно эквивалентно
r.use(a); r.use(b). Полезно, чтобы собрать одно переиспользуемое
значение Middleware из нескольких меньших (общий «стек», который вы
передаёте нескольким маршрутизаторам), вместо повторения последовательности
.use() в каждом месте.
Router @use и @nest
r.nest(prefix, sub) перевставляет уже зарегистрированные routes из sub
в r через тот же путь регистрации, что использует @route — так что
текущие слои r оборачивают и routes из sub, снаружи тех слоёв,
что уже были у sub в момент его собственной регистрации. Вложение одного
и того же суб-маршрутизатора в двух разных родителей не приводит к
перекрёстному загрязнению — @nest никогда не мутирует sub (value-
семантика), так что каждый родитель получает свою независимо обёрнутую
копию.
Что оборачивается
| Зарегистрировано через | Оборачивается .use()? |
|---|---|
Обработчики методов route’а (@get/@post/…) | Да |
MethodRouter.@fallback (per-route переопределение 405) | Да — и если у route нет собственного fallback, но слои есть, дефолтный 405 + Allow материализуется и тоже оборачивается, так что, например, CORS-preflight OPTIONS на GET-only route всё равно видит промежуточный обработчик |
Router.@fallback (глобальный 404) | Нет — это не зарегистрированный «route» в дереве, поэтому у wrap-at-registration нет для него точки входа; ведёт себя как route_layer из Axum, а не как .layer() на весь Service |
Пишем свой — в стиле батареек
Четыре батарейки из batteries.md делятся на две формы — выбор по числу скалярных ручек вашего промежуточного обработчика:
- До трёх скалярных параметров → голая функция с параметрами по
умолчанию (образец:
compression/ratelimit). Никакого типа-конфига: верхнеуровневаяfn my_thing(a int, b bool = false) -> Middleware, замыкающаяся на своих параметрах и делегирующая верхнеуровневой_apply-функции (держим набор захвата замыкания ровно тем, что нужно, один уровень вложенности). - Сложнее — списки, набор независимых переключателей → тип-конфиг +
приватный
@middleware()+ публичная одноимённая свободная функция (образец:cors/logger). Тип остаётся fluent-билдером (повторяемые методы вида@allow_origin(...), несколько булевых переключателей); сам@middleware()неexportится, так чтоmy_thing(cfg)— единственная публичная точка входа — никогда двух путей к одному и тому же промежуточному обработчику.
Связанные документы
Полный пример: examples/04-middleware — свой промежуточный обработчик, @then, порядок layers, log+ratelimit, реально запущенные.
- routing.md — сами
Router.@route/@nest - batteries.md — cors/compress/log/ratelimit, все построены так же
- auth.md —
require_jwt/session, ещё два промежуточных обработчика src/middleware.nv,src/middleware_test.nv