Перейти к содержимому

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: платформа собирает его из пользовательских правил и автоматических правил адаптера, если адаптеру нужно отразить routing-фичи фреймворка.

Если логика требует 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"
[[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.
[[rule]] или [[rules]] Упорядоченный список правил. Порядок в файле является порядком исполнения.
id Стабильный уникальный ID правила.
name Необязательное человекочитаемое имя.
enabled Необязательный флаг. По умолчанию true.
when или condition Условия срабатывания. Без условий правило матчится на все запросы.
action.<kind> или action = { type = "..." } Ровно одно действие.

position писать нельзя. Платформа выставляет его сама по порядку [[rule]].

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"
[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" или "пример.рф" Hostname запроса без scheme, port, path и userinfo. IDN-домены можно писать Unicode-строкой; платформа нормализует их в punycode.
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-поверхностью.

Для ограниченной булевой логики есть два поля:

  • 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" }
[[rule]]
id = "redirect-slug"
when.path = { exact = "/pricing-old" }
action.redirect = { target = "/pricing", status_code = 301 }
[[rule]]
id = "redirect-cyrillic-domain"
when.path = { glob = "/{rest...}" }
when.any = [
{ host = "пример.рф" },
{ host = "www.пример.рф" },
]
action.redirect = {
target = "https://example.com/{rest}",
status_code = 301,
if_no_file = false,
}
[[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 применяет правило сразу.

Redirect принимает относительный путь или absolute http/https URL. Это обычный HTTP redirect: edge вернёт Location, но не будет проксировать внешний origin. IDN-домены в host и absolute URL можно писать кириллицей — при publish они нормализуются в punycode.

Internal rewrite принимает относительный путь. External rewrite принимает только absolute https URL и проксирует запрос к внешнему origin. Запросы к приватным/internal адресам блокируются.

[[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 с тем же именем.

[[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"],
}
[[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 запускает 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;
  • request steps идут до handle;
  • response и observe steps идут после handle;
  • handle = "@app" оборачивает обычный app route и не требует override;
  • handle = "function-name" делает функцию terminal route и требует override = true;
  • failure = "closed" останавливает pipeline при ошибке step;
  • failure = "open" пропускает упавший step и продолжает;
  • если failure не указан, request step по умолчанию closed, а response/observe step по умолчанию 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 = "move-domain"
when.path = { glob = "/{rest...}" }
action.redirect = { target = "https://example.com/{rest}", status_code = 301 }
# проброс части пути во внешний адрес
[[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}" } }

Правила захватов:

  • захваты работают только в globexact и 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 и @app pipeline продолжают цепочку;
  • если несколько non-terminal правил меняют один эффект, позднее правило задаёт финальное состояние;
  • preview и production имеют независимые rulesets.

Для каждого окружения активен один ruleset. Последняя успешная активация окружения выигрывает.

Сценарий Итог
В HEAD есть onreza.rules.toml, вы делаете успешный nrz deploy Пользовательские rules из репозитория становятся активными для target environment
В HEAD нет onreza.rules.toml, вы делаете успешный nrz deploy Текущие пользовательские rules сохраняются; адаптер может обновить только свои автоматические rules
После этого вы публикуете rules через UI UI обновляет пользовательские rules для этого environment
Потом снова проходит deploy с onreza.rules.toml Repo rules снова обновляют пользовательскую часть
Standalone functions publish без onreza.rules.toml Текущие пользовательские rules сохраняются
Publish с пустым rules = [] Пользовательские rules явно очищаются; автоматические adapter rules могут остаться

Rollback переключает окружение назад вместе с app artifact, function revisions и Edge Rules.

Не пытайтесь выразить в 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".