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

Middleware

Middleware позволяет выполнять код до и после обработки запроса. Используйте middleware для аутентификации, редиректов, URL rewriting, geo-routing, A/B тестирования и модификации ответов.

Middleware выполняется в Edge Runtime в контексте SSR/static маршрутизации:

Запрос → Middleware Pipeline → SSR / Static → Ответ

Каждый middleware может:

  • Продолжить запрос с модифицированными заголовками (NextResponse.next())
  • Перенаправить на другой URL (NextResponse.redirect())
  • Переписать URL (NextResponse.rewrite())
  • Вернуть ответ напрямую (new Response(...))
  • Модифицировать ответ — вызвать await next(request), получить ответ от SSR/static, изменить и вернуть
middleware/auth.js
// NextResponse доступен глобально (globalThis.NextResponse)
export default async function middleware(request) {
const token = request.headers.get('Authorization');
if (!token) {
return NextResponse.redirect(new URL('/login', request.url), 307);
}
// Продолжить с инжектированным заголовком
return NextResponse.next({
headers: { 'X-Authenticated': 'true' },
});
}
{
"middleware": [
{
"name": "auth",
"bundlePath": "middleware/auth.js",
"codeHash": "sha256-...",
"matchers": ["^/(?:dashboard|api)/.*$"],
"priority": 100
}
]
}

Продолжить обработку запроса. Опционально инжектирует дополнительные заголовки.

// Без модификаций
return NextResponse.next();
// С дополнительными заголовками
return NextResponse.next({
headers: { 'X-Custom': 'value' },
});

Перенаправить на другой URL. По умолчанию статус 307 (Temporary Redirect).

return NextResponse.redirect(new URL('/login', request.url));
return NextResponse.redirect(new URL('/new-page', request.url), 301);

Переписать URL — клиент видит исходный URL, но сервер обрабатывает другой путь.

// /old-page будет обслуживаться как /new-page
return NextResponse.rewrite(new URL('/new-page', request.url));

Вернуть любой Response напрямую:

return new Response('Forbidden', { status: 403 });
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
});

Вызовите await next(request) чтобы выполнить оставшиеся middleware + SSR/static маршрутизацию, получить Response и модифицировать его перед отправкой клиенту.

export default async function middleware(request) {
// Выполнить downstream (SSR/static), получить Response
const response = await next(request);
// Модифицировать ответ
response.headers.set('X-Custom-Header', 'value');
response.headers.set('X-Request-Id', crypto.randomUUID());
return response;
}
СценарийИспользуйте
Auth check (redirect/block)NextResponse.next() или new Response(...)
Добавить headers к requestNextResponse.next({ headers })
Модифицировать response headersawait next(request)
Замерить время ответаawait next(request)
Добавить security headersawait next(request)
Инжектировать скрипт в HTMLawait next(request) + модификация body
export default async function middleware(request) {
const response = await next(request);
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
return response;
}
export default async function middleware(request) {
const start = performance.now();
const response = await next(request);
const duration = performance.now() - start;
response.headers.set('Server-Timing', `total;dur=${duration.toFixed(1)}`);
return response;
}
export default async function middleware(request) {
const response = await next(request);
// Проверяем Content-Type
const contentType = response.headers.get('Content-Type') || '';
if (contentType.includes('text/html')) {
const body = await response.text();
const modifiedBody = body.replace(
'</head>',
'<script src="/analytics.js"></script></head>'
);
return new Response(modifiedBody, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
}
return response;
}

При использовании await next(request) действуют следующие ограничения:

// Неправильно — next() можно вызвать только один раз
export default async function middleware(request) {
const res1 = await next(request);
const res2 = await next(request); // Ошибка!
return res1;
}

Response body полностью буферизуется. Streaming через next() не поддерживается.

// Response будет полностью загружен в память
const response = await next(request);
const body = await response.text(); // Полный body в памяти

Максимальный размер request/response body при использовании next()5 MB.

// Если body запроса > 5MB — ошибка
export default async function middleware(request) {
const response = await next(request);
// Если response body > 5MB — ошибка
return response;
}

Если нескольким middleware нужно модифицировать response — объедините логику в один middleware:

middleware/security.js
// Неправильно — два middleware вызывают next()
export default async function middleware(request) {
const response = await next(request);
response.headers.set('X-Frame-Options', 'DENY');
return response;
}
// middleware/timing.js
export default async function middleware(request) {
const response = await next(request); // Ошибка! next() уже вызван
return response;
}
// Правильно — объедините в один middleware
export default async function middleware(request) {
const start = performance.now();
const response = await next(request);
const duration = performance.now() - start;
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('Server-Timing', `mw;dur=${duration.toFixed(1)}`);
return response;
}

Поле priority определяет порядок выполнения middleware: чем выше значение, тем раньше выполняется (сортировка по убыванию, DESC).

Значение по умолчанию: 0.

{
"features": {
"middleware": [
{ "name": "rate-limit", "priority": 200 },
{ "name": "auth", "priority": 100 },
{ "name": "logging", "priority": 0 }
]
}
}

Порядок выполнения: rate-limit (200) → auth (100) → logging (0).

Поле matchers определяет, для каких URL путей запускается middleware. Используется regex patterns в синтаксисе Rust regex (без lookbehinds и backreferences).

{
"matchers": [
"/dashboard/.*",
"/api/.*",
"/admin/.*"
]
}
{
"matchers": [
"^/api/(?!public/).*",
"^/(en|ru|de)/.*",
"/blog/[0-9]+-.*"
]
}
{
"matchers": [
"/api/(?!health|public).*"
]
}

Исключает /api/health и /api/public/*, но включает /api/users, /api/admin, и т.д.

Если matchers пуст или не указан, middleware выполняется для всех путей.

Middleware имеет доступ к тем же Edge Runtime APIs, что и Edge Functions:

APIОписание
next(request)Выполнить downstream routing, получить Response для модификации
@onreza/runtime/contextКонтекст запроса (region, deployment info)
@onreza/runtime/kvKV Store для сессий и кэша
@onreza/runtime/dbD1 Database — SQL база данных
fetch()HTTP запросы
crypto.subtleWeb Crypto API
import { kv } from '@onreza/runtime/kv';
export default async function middleware(request) {
const session = request.headers.get('Cookie')
?.match(/session=([^;]+)/)?.[1];
if (!session) {
return NextResponse.redirect(new URL('/login', request.url));
}
const user = await kv.get(`session:${session}`, { type: 'json' });
if (!user) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next({
headers: { 'X-User-Id': user.id },
});
}
  • Timeout: наследуется от ssrTimeoutMs деплоймента (default: 30s)
  • Memory: наследуется от ssrMemoryMb деплоймента
  • Middleware выполняется последовательно по priority
  • next() можно вызвать только один раз внутри одного middleware
  • Только один middleware в цепочке может использовать await next() для модификации response. Остальные middleware могут использовать NextResponse.next(), NextResponse.redirect(), NextResponse.rewrite() или new Response(...) без ограничений
  • Request body в next() не может превышать 5 MB
  • Response body при next() буферизуется полностью (максимум ~5 MB), streaming не поддерживается