Edge Rules
Edge Rules — это правила окружения, которые ONREZA исполняет на edge до основного route приложения. Через них настраивают redirects, rewrites, headers, cache, deny/log, rate limit и привязку ONREZA Functions route pipeline.
Правила описываются в onreza.rules.toml рядом с onreza.toml или публикуются
через UI. Для каждого окружения активен один effective ruleset: последняя
успешная активация из репозитория или UI заменяет предыдущий набор правил.
Если логика требует runtime-состояния, внешнего fetch, KV, вычислений,
персонализации или изменения body, это не native Edge Rule. Такую логику
пишите как ONREZA Function route pipeline, а
в Edge Rules оставляйте условие маршрутизации.
Что где настраивается
Заголовок раздела «Что где настраивается»| Поверхность | Что описывает | Где применяется |
|---|---|---|
| Build Output Manifest | Static/compute routes приложения | После Edge Rules |
| ONREZA Functions | HTTP handlers из *.nrz-fn.* файлов | В route pipeline как step или terminal function |
onreza.rules.toml | Статические правила, cache policy и route pipeline wiring | До terminal route или вокруг него |
onreza.rules.toml не исполняет код при publish. Файл парсится как TOML,
валидируется как данные и нормализуется в runtime ruleset.
Формат файла
Заголовок раздела «Формат файла»Файл лежит в корне проекта:
my-app/├── onreza.toml├── onreza.rules.toml└── functions/Минимальный файл:
schema = "EDGE_RULE_SET_V1"source = { origin = "build" }
[[rule]]id = "redirect-old-docs"when.path = { prefix = "/old-docs" }action.redirect = { target = "/docs", status_code = 308, if_no_file = false }Основные поля:
| Поле | Описание |
|---|---|
schema или schemaVersion | Версия контракта. Сейчас EDGE_RULE_SET_V1. |
source.origin | Для файла в репозитории используйте build. |
[[rule]] или [[rules]] | Упорядоченный список правил. Порядок в файле является порядком исполнения. |
id | Стабильный уникальный ID правила. |
name | Необязательное человекочитаемое имя. |
enabled | Необязательный флаг. По умолчанию true. |
when или condition | Условия срабатывания. Без условий правило матчится на все запросы. |
action.<kind> или action = { type = "..." } | Ровно одно действие. |
position писать нельзя. Платформа выставляет его сама по порядку [[rule]].
TOML 1.1 authoring profile
Заголовок раздела «TOML 1.1 authoring profile»nrz принимает TOML 1.1 authoring profile. Это значит, что можно писать
читаемый TOML, а CLI перед отправкой нормализует его в canonical contract.
Поддерживается:
schemaвместоschemaVersion;[[rule]]вместо[[rules]];whenвместоcondition;action.redirect,action.cache,action.pipelineи другиеaction.<kind>вместоaction = { type = "..." };- snake_case для полей:
status_code,if_no_file,ttl_seconds,swr_seconds,window_seconds,inherit_gate,cache_position; - TOML 1.1 multi-line inline tables для крупных
pipelineи cache rules; - обычный canonical camelCase формат тоже остаётся валидным.
Ключи внутри headers, query и cookies считаются пользовательскими данными
и не меняются. Для HTTP headers с дефисами используйте кавычки:
action.set_headers = { headers = { "x-frame-options" = "DENY", "x-content-type-options" = "nosniff", },}JSON Schema доступна по адресу:
/schemas/onreza-rules-v1.schema.json.
Локальная проверка:
nrz functions checkКоманда валидирует функции и onreza.rules.toml. Для rules-only проекта она
тоже полезна: неверный TOML, неизвестные поля, дубли id, небезопасный cache
vary и некорректный pipeline будут отклонены до deploy.
Полный пример
Заголовок раздела «Полный пример»schema = "EDGE_RULE_SET_V1"source = { origin = "build" }
[actions.security_headers.set_headers]headers = { "x-frame-options" = "DENY", "x-content-type-options" = "nosniff", "referrer-policy" = "strict-origin-when-cross-origin",}
[[rule]]id = "redirect-legacy-docs"when.path = { prefix = "/old-docs" }action.redirect = { target = "/docs", status_code = 308, if_no_file = false }
[[rule]]id = "cache-public-assets"when.path = { glob = "/assets/*" }when.methods = ["GET", "HEAD"]action.cache = { ttl_seconds = 86400, swr_seconds = 600 }
[[rule]]id = "mobile-download-page"when.path = { exact = "/download" }when.device = "mobile"action.rewrite = { target = "/download/mobile.html", if_no_file = false }
[[rule]]id = "dashboard-auth"when.path = { prefix = "/dashboard" }action.pipeline = { inherit_gate = true, steps = [ { use = "require-session", mode = "request", failure = "closed" }, { handle = "@app" }, { use = "add-user-header", mode = "response", failure = "open" }, ],}
[[rule]]id = "api-rate-limit"when.path = { prefix = "/api/" }action.rate_limit = { limit = 120, window_seconds = 60, key = "ip_path" }
[[rule]]id = "deny-blocked-network"when.any = [ { geo = ["KP"] }, { asn = [13335] },]action.deny = { status_code = 403 }
[[rule]]id = "security-headers"action.use = "security_headers"[actions.<name>.<kind>] — необязательный способ переиспользовать одно и то же
действие. В примере security_headers подключается через action.use.
Условия
Заголовок раздела «Условия»Если в одном правиле указано несколько условий, они объединяются через И: правило сработает только когда совпали все заданные поля.
| Condition | Authoring пример | Что проверяет |
|---|---|---|
path | { exact = "/docs" } | Точный путь |
path | { prefix = "/admin" } | Префикс пути |
path | { glob = "/blog/*" } | Glob по пути. * может покрывать вложенные сегменты. Поддерживает захваты {name} (см. Захваты пути). |
methods | ["GET", "HEAD"] | HTTP methods |
host | "app.example.com" | Host запроса |
headers | { "x-plan" = "pro" } | Request headers |
query | { preview = "1" } | Query string |
cookies | { bucket = "b" } | Cookies |
geo | ["RU", "KZ"] | ISO country codes |
asn | [13335, 15169] | Autonomous System Number |
device | "mobile" | desktop, mobile, tablet, bot |
sourceIpCidrs | ["203.0.113.0/24"] | Client IP/CIDR |
Canonical форма path тоже валидна:
condition.path = { type = "prefix", value = "/docs" }Через onreza.rules.toml сейчас поддерживаются только exact, prefix и
glob. Regex path matchers не являются публичной authoring-поверхностью.
any и not
Заголовок раздела «any и not»Для ограниченной булевой логики есть два поля:
when.any— список альтернатив через ИЛИ;when.not— отрицание одного вложенного условия.
Внутри any и not доступны те же базовые matchers, но без вложенных any и
not.
[[rule]]id = "deny-datacenter-or-blocked-geo"when.any = [ { geo = ["KP"] }, { asn = [13335] },]action.deny = {}
[[rule]]id = "cache-everything-except-api"when.path = { prefix = "/" }when.not = { path = { prefix = "/api" } }action.cache = { ttl_seconds = 300 }Действия
Заголовок раздела «Действия»| Action | Терминальное | Основные поля | Что делает |
|---|---|---|---|
allow | Нет | - | Явно продолжает цепочку правил. |
log | Нет | - | Отмечает совпадение правила. |
deny | Да в enforce, нет в shadow | status_code, mode | Возвращает отказ. По умолчанию 403. |
redirect | Да | target, status_code, if_no_file | Возвращает 301, 302, 307 или 308. |
rewrite | External — да, internal — нет | target, external, if_no_file | Переписывает путь или проксирует external origin. |
set_headers | Нет | headers | Добавляет или заменяет response headers. |
remove_headers | Нет | headers | Удаляет response headers. |
cache | Нет | ttl_seconds, swr_seconds, vary | Задаёт cache policy downstream response. |
bypass_cache | Нет | - | Отключает cache для совпавшего downstream route. |
rate_limit | Да в enforce, нет в shadow | limit, window_seconds, key, mode | Ограничивает частоту запросов. |
pipeline | Function terminal — да, @app — нет | steps, override, inherit_gate | Запускает ONREZA Functions вокруг route. |
Терминальное действие завершает обработку ruleset. Нетерминальные действия накапливают эффекты и позволяют следующим правилам продолжить обработку.
[[rule]]id = "deny-admin-posts"when.path = { prefix = "/admin" }when.methods = ["POST"]action.deny = { status_code = 403 }mode = "shadow" позволяет проверить правило без блокировки запроса:
action.deny = { status_code = 451, mode = "shadow" }redirect и rewrite
Заголовок раздела «redirect и rewrite»[[rule]]id = "redirect-slug"when.path = { exact = "/pricing-old" }action.redirect = { target = "/pricing", status_code = 301 }
[[rule]]id = "rewrite-legacy"when.path = { prefix = "/legacy" }action.rewrite = { target = "/modern", if_no_file = false }
[[rule]]id = "proxy-docs"when.path = { prefix = "/external-docs" }action.rewrite = { target = "https://docs.example.com/", external = true, if_no_file = false,}if_no_file = true означает file-priority поведение: если matching static файл
существует, он имеет приоритет и правило не применяется. if_no_file = false
применяет правило сразу.
External rewrite принимает только absolute http или https URL. Запросы к
приватным/internal адресам блокируются.
headers
Заголовок раздела «headers»[[rule]]id = "security-headers"action.set_headers = { headers = { "x-frame-options" = "DENY", "x-content-type-options" = "nosniff", },}
[[rule]]id = "remove-debug"when.path = { prefix = "/public" }action.remove_headers = { headers = ["x-debug"] }Порядок правил важен: поздний set_headers может задать финальное значение, а
поздний remove_headers удаляет ранее выставленный header с тем же именем.
cache и bypass_cache
Заголовок раздела «cache и bypass_cache»[[rule]]id = "cache-public"when.path = { prefix = "/public" }action.cache = { ttl_seconds = 600, swr_seconds = 60 }
[[rule]]id = "never-cache-preview"when.query = { preview = "1" }action.bypass_cache = {}Если cache зависит от request-specific условий, обязательно укажите vary.
Валидатор не даст опубликовать правило, которое может смешать ответы разных
пользователей.
| Условие в cache rule | Нужный vary |
|---|---|
headers | "header" |
query | "query" |
cookies | "cookie" |
geo | "geo" |
asn | "asn" |
device | "device" |
Пример:
[[rule]]id = "cache-mobile-plan-page"when.path = { exact = "/landing" }when.headers = { "x-plan" = "pro" }when.device = "mobile"action.cache = { ttl_seconds = 300, vary = ["header", "device"],}rate_limit
Заголовок раздела «rate_limit»[[rule]]id = "api-rate-limit"when.path = { prefix = "/api/" }action.rate_limit = { limit = 120, window_seconds = 60, key = "ip_path",}Поддерживаемые ключи:
| Key | Scope |
|---|---|
ip | Один client IP |
ip_path | Client IP + path |
ip_host | Client IP + host |
host | Host, общий для всех клиентов этого host |
mode = "shadow" считает совпадения, но не блокирует запросы.
pipeline
Заголовок раздела «pipeline»Pipeline запускает ONREZA Functions вокруг downstream route.
[[rule]]id = "dashboard-auth"when.path = { prefix = "/dashboard" }action.pipeline = { inherit_gate = true, steps = [ { use = "require-session", mode = "request", failure = "closed" }, { handle = "@app" }, { use = "add-user-header", mode = "response", failure = "open" }, { use = "audit-access", mode = "observe", failure = "open" }, ],}Pipeline состоит из steps:
| Step | Где ставить | Что делает |
|---|---|---|
{ use = "fn", mode = "request" } | До handle | Может вернуть новый Request или короткозамкнуть Response. |
{ handle = "@app" } | Ровно один terminal handle | Продолжает в обычный static/compute/app route. |
{ handle = "fn" } | Ровно один terminal handle | Делает функцию terminal route. Нужен override = true. |
{ use = "fn", mode = "response" } | После handle | Может изменить downstream response. |
{ use = "fn", mode = "observe" } | После handle | Side-effect step после downstream response. |
Правила pipeline:
- должен быть ровно один
handle; requeststeps идут доhandle;responseиobservesteps идут послеhandle;handle = "@app"оборачивает обычный app route и не требуетoverride;handle = "function-name"делает функцию terminal route и требуетoverride = true;failure = "closed"останавливает pipeline при ошибке step;failure = "open"пропускает упавший step и продолжает;- если
failureне указан,requeststep по умолчаниюclosed, аresponse/observestep по умолчаниюopen; cache_position = "before"или"after"задаёт, по какую сторону cache boundary выполняется request step. По умолчанию используетсяbefore.
Function terminal пример:
[[rule]]id = "api-function"when.path = { prefix = "/api/" }action.pipeline = { override = true, steps = [ { handle = "api" }, ],}Захваты пути и интерполяция
Заголовок раздела «Захваты пути и интерполяция»Glob-условие умеет запоминать части пути и переносить их в target правила
redirect/rewrite или в значения set_headers. Это позволяет описывать
параметрические правила одной строкой, без отдельной функции.
В glob-пути доступны два вида захвата:
{name}— один сегмент пути (до/);{name...}— остаток пути целиком, включая/.
В target и в значениях заголовков захват подставляется через {name} — в обоих
случаях по имени, без ...:
# /blog/hello-world → /posts/hello-world.html[[rule]]id = "clean-urls"when.path = { glob = "/blog/{slug}" }action.rewrite = { target = "/posts/{slug}.html" }
# /docs/guide/intro → редирект на /guides/guide/intro[[rule]]id = "move-docs"when.path = { glob = "/docs/{rest...}" }action.redirect = { target = "/guides/{rest}", status_code = 308 }
# проброс части пути во внешний адрес[[rule]]id = "sitemap-proxy"when.path = { glob = "/sitemap-{id}.xml" }action.rewrite = { target = "https://origin.example.com/v1/sitemap?path=/sitemap-{id}.xml", external = true,}
# захват можно подставить и в заголовок[[rule]]id = "tag-tenant"when.path = { glob = "/t/{tenant}/{rest...}" }action.set_headers = { headers = { "x-tenant" = "{tenant}" } }Правила захватов:
- захваты работают только в
glob(вexactиprefixфигурные скобки — обычные символы); - захваты объявляются только в основном
when.path; glob-пути внутри ветокany/notобъявлять захваты не могут; - имена захватов уникальны в пределах правила, splat
{name...}— не более одного; - каждый
{name}вtarget/заголовке должен быть объявлен в пути этого же правила, splat подставляется по имени без...— иначе правило не пройдёт публикацию; - чтобы вывести литеральный
{name}без подстановки (например, URI-template в заголовке), удвойте скобки:{{name}}превращается в текст{name}.
Порядок запроса
Заголовок раздела «Порядок запроса»Упрощённый порядок обработки:
access protection-> ordered Edge Rules-> request pipeline steps-> cache lookup-> terminal route: static / function / compute-> response and observe pipeline steps-> response headersКлючевые правила:
- правила идут сверху вниз;
- терминальное действие завершает обработку ruleset;
cache,bypass_cache,set_headers,remove_headers,allow,logи@apppipeline продолжают цепочку;- если несколько non-terminal правил меняют один эффект, позднее правило задаёт финальное состояние;
- preview и production имеют независимые rulesets.
CLI и UI
Заголовок раздела «CLI и UI»Для каждого окружения активен один ruleset. Последняя успешная активация окружения выигрывает.
| Сценарий | Итог |
|---|---|
В HEAD есть onreza.rules.toml, вы делаете успешный nrz deploy | Ruleset из репозитория становится активным для target environment |
| После этого вы публикуете другой ruleset через UI | UI ruleset становится активным для этого environment |
| Потом снова проходит deploy из HEAD | Repo ruleset снова заменяет UI ruleset |
Functions-only publish без edgeRules | Текущий active ruleset сохраняется |
Publish с пустым rules = [] | Ruleset явно очищается |
Rollback переключает окружение назад вместе с app artifact, function revisions и Edge Rules.
Когда выбрать ONREZA Functions
Заголовок раздела «Когда выбрать ONREZA Functions»Не пытайтесь выразить в onreza.rules.toml то, что зависит от runtime-данных:
- lookup session/user в KV;
- запрос во внешний API;
- A/B с вычислением bucket на лету;
- per-user redirects;
- auth/RBAC;
- изменение response body;
- логика, требующая циклов, даты, случайности или состояния.
Для этого создайте ONREZA Function и привяжите её через pipeline. Path
condition правила всё равно применяется нативно, а JavaScript выполнится только
на совпавших маршрутах.
Частые ошибки
Заголовок раздела «Частые ошибки»| Ошибка | Как исправить |
|---|---|
Добавили position в правило | Удалите поле. Порядок берётся из файла. |
Использовали regex path | Используйте exact, prefix или glob. |
Cache rule зависит от headers, query, cookies, geo, asn или device, но нет vary | Добавьте соответствующие значения в action.cache.vary. |
Pipeline содержит два handle | Оставьте ровно один terminal handle. |
request step стоит после handle | Перенесите его до handle. |
Function terminal без override = true | Добавьте override = true или используйте handle = "@app". |
| Header с дефисом написан без кавычек | Пишите "x-frame-options" = "DENY". |