njs в NGINX: где JavaScript действительно работает, а где — нет

njs добавляет JavaScript в NGINX, но не превращает его в Node.js. Код выполняется только в строго заданных точках обработки запроса и должен укладываться в миллисекунды. Два движка — устаревший встроенный njs и современный QuickJS — различаются по скорости и возможностям.

njs в NGINX: где JavaScript действительно работает, а где — нет

njs: где JavaScript в NGINX действительно работает, а где — нет

njs не превращает NGINX в Node.js. Он позволяет написать несколько строк JavaScript, которые выполнятся в строго определённый момент: когда NGINX решает, пустить ли запрос дальше, какой ответ вернуть или какие заголовки добавить. Контекст живёт только во время обработки запроса или таймера, а затем исчезает. Никакого постоянного процесса, никакого event loop — только микро-вспышки кода, которые должны уложиться в миллисекунды. Если скрипт зависнет хоть на секунду, замрёт весь воркер. Это не гибкость, а жёсткое ограничение.


Два движка: QuickJS против устаревшего встроенного njs

njs поддерживает два JavaScript-движка. Встроенный njs работает по ES5.1 и объявлен deprecated с версии 1.0.0 — он лишён современных фич, но быстрее создаёт контексты. QuickJS — это современный движок с поддержкой ES2023, модулей, асинхронных генераторов и BigInt. Переключение между ними делается одной строчкой в конфигурации NGINX:

js_engine qjs;

QuickJS медленнее создаёт контексты — это критично для систем с короткими запросами и высокой нагрузкой. В документации нет бенчмарков, зато есть предупреждение: если гонять скрипты с QuickJS на сотнях запросов в секунду, задержки вырастут.

Ещё один нюанс — переиспользование контекстов. При js_context_reuse on; QuickJS может брать готовые контексты из пула, но глобальные переменные при этом остаются между запросами. Если забыть их сбросить, один запрос испортит следующий. Это не баг, а следствие устройства движка: контекст живёт дольше одного запроса, но не гарантирует чистоту.


Пять операционных деталей, которые решают исход

  1. Объект r — единственный интерфейс к NGINX. Он даёт доступ к переменным (r.variables), заголовкам (r.headersIn, r.headersOut), а также позволяет запускать подзапросы (r.subrequest()) и выполнять внутренние редиректы (r.internalRedirect()).

  2. Точки интеграции жёстко заданы. Например, js_header_filter и js_body_filter не поддерживают асинхронность — они должны вернуть результат сразу. А js_access обязан завершиться мгновенно, иначе блокирует обработку запроса.

  3. Shared-словарь (js_shared_dict_zone) — единственный способ делиться состоянием между воркерами NGINX. Без него данные между запросами не сохраняются.

  4. Нет доступа к файловой системе за пределами встроенных модулей. Хотите прочитать конфиг? Придётся делать это через переменные NGINX. Работа с большими файлами невозможна без предварительной загрузки в память.

  5. Нет поддержки npm-пакетов и потоков. Долгий синхронный код блокирует весь воркер, даже если обёрнут в async/await.


Три примера, которые можно копировать и адаптировать

1. Динамическая маршрутизация для A/B-тестирования или canary-релизов

function choose_upstream(r) {
  const mark = (r.headersIn['X-Exp'] || getCookie(r, 'exp') || '').toLowerCase();
  if (mark === 'v2') return 'app_v2';
  const bucket = Math.abs(hashCode(r.variables.request_id)) % 10;
  return bucket === 0 ? 'app_v2' : 'app_v1';
}

Ограничение: если r.variables.request_id нестабилен, распределение по бакетам сломается. Для продакшн-кода стоит использовать более надёжный идентификатор, например, хэш от r.remoteAddress + r.uri.


2. Канонизация URL: убираем www, переводим на HTTPS, чистим параметры

function canonical_redirect(r) {
  const host = (r.headersIn['Host'] || '').toLowerCase();
  if (host.startsWith('www.')) {
    r.return(301, `https://${host.slice(4)}${r.uri}${r.args ? '?' + r.args : ''}`);
    return;
  }
  if (r.variables.scheme !== 'https') {
    r.return(301, `https://${host}${r.uri}${r.args ? '?' + r.args : ''}`);
    return;
  }
  r.internalRedirect('@upstream');
}

Edge case: если заголовок Host отсутствует или искажён, скрипт упадёт. В продакшн-коде стоит добавить проверку и fallback.


3. Безопасность на границе сети: добавляем заголовки CSP и HSTS

function add_security_headers(r) {
  r.headersOut['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains';
  r.headersOut['Content-Security-Policy'] = "default-src 'self'";
}

Ограничение: если r.headersOut уже содержит заголовки, они будут перезаписаны. Для сохранения существующих заголовков нужно сначала их прочитать и объединить.


Что njs не потянет: три причины не строить на нём бизнес-логику

  1. Однопоточность. Долгий синхронный код блокирует весь воркер. Даже с async/await это не спасёт, если операция реально долгая. njs не Node.js: здесь нет отдельного event loop.

  2. Нет нормального доступа к данным. Нет работы с файловой системой, нет npm-пакетов, нет потоков. Хотите обработать большой JSON-файл? Придётся тащить его в память через переменные NGINX — что не всегда возможно.

  3. Нет изоляции между воркерами. Shared-словарь — единственный способ делиться состоянием, но он не атомарен и не транзакционен. Race condition между воркерами — типичная проблема.


Практическое правило: используйте njs для микро-обработки, а не для приложений

njs — это инструмент для тонкой настройки на границе сети. Он удобен для редиректов, фильтрации заголовков, простой маршрутизации или добавления безопасности. Но если ваша задача требует сложной бизнес-логики, долгих вычислений или работы с большими данными, лучше вынести её за NGINX.

QuickJS удобнее для современного кода, но медленнее в создании контекстов. Встроенный njs быстрее, но deprecated. Выбор движка — это trade-off между удобством и производительностью.

Если решитесь использовать njs, начните с малого: возьмите простой скрипт для редиректов или добавления заголовков. Убедитесь, что он не блокирует воркер. И только потом расширяйте логику. Иначе рискуете обнаружить, что ваш "микро-скрипт" стал узким местом.

Read more

Россияне не покупают у брендов напрямую: главные барьеры и что делать бизнесу

Россияне не покупают у брендов напрямую: главные барьеры и что делать бизнесу

Опрос 1229 жителей 17 городов России показал: 45% откажутся от покупки, если товар нельзя доставить в их населённый пункт. Проблемы с оплатой на сайте и долгие сроки доставки отсекают ещё до 39% клиентов. Без инфраструктуры даже сильный бренд теряет половину аудитории.

DeepSeek V4 Pro 0813: где модель выигрывает по цене, а где проигрывает по возможностям

DeepSeek V4 Pro 0813: где модель выигрывает по цене, а где проигрывает по возможностям

DeepSeek V4 Pro 0813 дешевле конкурентов в 11–34 раза, но не поддерживает заполнение пропусков кода в режиме рассуждений по умолчанию. Модель подходит для задач с длинным контекстом, но требует ручного переключения режимов и не бьёт рекорды по качеству.

Linux на десктопе: где ломается совместимость приложений

Linux на десктопе: где ломается совместимость приложений

Ядро Linux сохраняет совместимость десятилетиями, но приложения на десктопе часто перестают работать из-за фрагментации окружений и библиотек. Разработчики обвиняют GNOME и дистрибутивы в создании искусственных барьеров, а пользователи платят за это нестабильностью.