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

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 ManifestStatic/compute routes приложенияПосле Edge Rules
ONREZA FunctionsHTTP 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]].

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.

Если в одном правиле указано несколько условий, они объединяются через И: правило сработает только когда совпали все заданные поля.

ConditionAuthoring примерЧто проверяет
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-поверхностью.

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

  • 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, нет в shadowstatus_code, modeВозвращает отказ. По умолчанию 403.
redirectДаtarget, status_code, if_no_fileВозвращает 301, 302, 307 или 308.
rewriteExternal — да, 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, нет в shadowlimit, window_seconds, key, modeОграничивает частоту запросов.
pipelineFunction 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 = "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 адресам блокируются.

[[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",
}

Поддерживаемые ключи:

KeyScope
ipОдин client IP
ip_pathClient IP + path
ip_hostClient IP + host
hostHost, общий для всех клиентов этого 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" }После handleSide-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 = "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 deployRuleset из репозитория становится активным для target environment
После этого вы публикуете другой ruleset через UIUI ruleset становится активным для этого environment
Потом снова проходит deploy из HEADRepo ruleset снова заменяет UI ruleset
Functions-only publish без edgeRulesТекущий active ruleset сохраняется
Publish с пустым rules = []Ruleset явно очищается

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".