← Документация · nv-lang/nova-polaris

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))

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, реально запущенные.