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, изменить и вернуть
Быстрый старт
Заголовок раздела «Быстрый старт»1. Создайте файл middleware
Заголовок раздела «1. Создайте файл middleware»// 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' }, });}2. Объявите в manifest.json
Заголовок раздела «2. Объявите в manifest.json»{ "middleware": [ { "name": "auth", "bundlePath": "middleware/auth.js", "codeHash": "sha256-...", "matchers": ["^/(?:dashboard|api)/.*$"], "priority": 100 } ]}NextResponse API
Заголовок раздела «NextResponse API»NextResponse.next(init?)
Заголовок раздела «NextResponse.next(init?)»Продолжить обработку запроса. Опционально инжектирует дополнительные заголовки.
// Без модификацийreturn NextResponse.next();
// С дополнительными заголовкамиreturn NextResponse.next({ headers: { 'X-Custom': 'value' },});NextResponse.redirect(url, status?)
Заголовок раздела «NextResponse.redirect(url, status?)»Перенаправить на другой URL. По умолчанию статус 307 (Temporary Redirect).
return NextResponse.redirect(new URL('/login', request.url));return NextResponse.redirect(new URL('/new-page', request.url), 301);NextResponse.rewrite(url)
Заголовок раздела «NextResponse.rewrite(url)»Переписать URL — клиент видит исходный URL, но сервер обрабатывает другой путь.
// /old-page будет обслуживаться как /new-pagereturn NextResponse.rewrite(new URL('/new-page', request.url));Early Response
Заголовок раздела «Early Response»Вернуть любой Response напрямую:
return new Response('Forbidden', { status: 403 });return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401, headers: { 'Content-Type': 'application/json' },});Bidirectional Middleware (next())
Заголовок раздела «Bidirectional Middleware (next())»Вызовите 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;}Когда использовать next()
Заголовок раздела «Когда использовать next()»| Сценарий | Используйте |
|---|---|
| Auth check (redirect/block) | NextResponse.next() или new Response(...) |
| Добавить headers к request | NextResponse.next({ headers }) |
| Модифицировать response headers | await next(request) |
| Замерить время ответа | await next(request) |
| Добавить security headers | await next(request) |
| Инжектировать скрипт в HTML | await next(request) + модификация body |
Пример: Security Headers
Заголовок раздела «Пример: Security Headers»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;}Пример: Timing Header
Заголовок раздела «Пример: Timing Header»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;}Пример: Response Body Modification
Заголовок раздела «Пример: Response Body Modification»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;}Ограничения Bidirectional Middleware
Заголовок раздела «Ограничения Bidirectional Middleware»При использовании await next(request) действуют следующие ограничения:
Один вызов next()
Заголовок раздела «Один вызов next()»// Неправильно — next() можно вызвать только один разexport default async function middleware(request) { const res1 = await next(request); const res2 = await next(request); // Ошибка! return res1;}Без Streaming
Заголовок раздела «Без Streaming»Response body полностью буферизуется. Streaming через next() не поддерживается.
// Response будет полностью загружен в памятьconst response = await next(request);const body = await response.text(); // Полный body в памятиЛимит размера body
Заголовок раздела «Лимит размера 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;}Только один next() в цепочке middleware
Заголовок раздела «Только один next() в цепочке middleware»Если нескольким middleware нужно модифицировать response — объедините логику в один middleware:
// Неправильно — два middleware вызывают next()export default async function middleware(request) { const response = await next(request); response.headers.set('X-Frame-Options', 'DENY'); return response;}
// middleware/timing.jsexport default async function middleware(request) { const response = await next(request); // Ошибка! next() уже вызван return response;}
// Правильно — объедините в один middlewareexport 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
Заголовок раздела «Priority»Поле 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).
Match Patterns
Заголовок раздела «Match Patterns»Поле matchers определяет, для каких URL путей запускается middleware. Используется regex patterns в синтаксисе Rust regex (без lookbehinds и backreferences).
Простые паттерны
Заголовок раздела «Простые паттерны»{ "matchers": [ "/dashboard/.*", "/api/.*", "/admin/.*" ]}Regex паттерны
Заголовок раздела «Regex паттерны»{ "matchers": [ "^/api/(?!public/).*", "^/(en|ru|de)/.*", "/blog/[0-9]+-.*" ]}Исключения (negative lookahead)
Заголовок раздела «Исключения (negative lookahead)»{ "matchers": [ "/api/(?!health|public).*" ]}Исключает /api/health и /api/public/*, но включает /api/users, /api/admin, и т.д.
Пустой список matchers
Заголовок раздела «Пустой список matchers»Если matchers пуст или не указан, middleware выполняется для всех путей.
Доступные API
Заголовок раздела «Доступные API»Middleware имеет доступ к тем же Edge Runtime APIs, что и Edge Functions:
| API | Описание |
|---|---|
next(request) | Выполнить downstream routing, получить Response для модификации |
@onreza/runtime/context | Контекст запроса (region, deployment info) |
@onreza/runtime/kv | KV Store для сессий и кэша |
@onreza/runtime/db | D1 Database — SQL база данных |
fetch() | HTTP запросы |
crypto.subtle | Web 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 }, });}import { getGeo } from '@onreza/runtime/context';
export default async function middleware(request) { const geo = getGeo(); const country = geo?.country || 'US';
if (country === 'RU') { return NextResponse.rewrite(new URL('/ru' + new URL(request.url).pathname, request.url)); }
return NextResponse.next();}// NextResponse доступен глобально (globalThis.NextResponse)
export default async function middleware(request) { const url = new URL(request.url);
if (url.pathname === '/pricing') { const bucket = request.headers.get('Cookie')?.match(/ab_bucket=([^;]+)/)?.[1] || (Math.random() > 0.5 ? 'A' : 'B');
return NextResponse.rewrite( new URL(`/pricing-${bucket.toLowerCase()}`, request.url) ); }
return NextResponse.next();}// NextResponse доступен глобально (globalThis.NextResponse)
const redirects = { '/old-blog': '/blog', '/docs/v1': '/docs', '/about-us': '/about',};
export default async function middleware(request) { const path = new URL(request.url).pathname; const target = redirects[path];
if (target) { return NextResponse.redirect(new URL(target, request.url), 301); }
return NextResponse.next();}// next() — глобальная функция для bidirectional middleware
export default async function middleware(request) { const start = performance.now(); const response = await next(request); const duration = performance.now() - start;
// Добавить security headers + timing response.headers.set('X-Content-Type-Options', 'nosniff'); response.headers.set('X-Frame-Options', 'DENY'); response.headers.set('Server-Timing', `mw;dur=${duration.toFixed(1)}`);
return response;}import { kv } from '@onreza/runtime/kv';
// Маппинг ролей на разрешённые путиconst ROLE_ROUTES = { admin: ['/admin/.*', '/api/admin/.*'], editor: ['/dashboard/.*', '/api/content/.*'], viewer: ['/dashboard/.*'],};
export default async function middleware(request) { const token = request.headers.get('Authorization')?.replace('Bearer ', ''); if (!token) { return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401, headers: { 'Content-Type': 'application/json' }, }); }
// Верификация JWT через Web Crypto API const payload = await verifyJWT(token); if (!payload) { return new Response(JSON.stringify({ error: 'Invalid token' }), { status: 401, headers: { 'Content-Type': 'application/json' }, }); }
// Проверка роли в KV (кэшированные permissions) const userRole = await kv.get(`role:${payload.sub}`, { type: 'json' }); const role = userRole?.role || 'viewer'; const allowedPatterns = ROLE_ROUTES[role] || [];
const path = new URL(request.url).pathname; const isAllowed = allowedPatterns.some(pattern => new RegExp(`^${pattern}$`).test(path) );
if (!isAllowed) { return new Response(JSON.stringify({ error: 'Forbidden' }), { status: 403, headers: { 'Content-Type': 'application/json' }, }); }
return NextResponse.next({ headers: { 'X-User-Id': payload.sub, 'X-User-Role': role }, });}
// JWT верификация через crypto.subtle (HMAC-SHA256)async function verifyJWT(token) { try { const [header, payload, signature] = token.split('.'); const key = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(ONREZA.env.get('JWT_SECRET')), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify'] ); const valid = await crypto.subtle.verify( 'HMAC', key, Uint8Array.from(atob(signature.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0)), new TextEncoder().encode(`${header}.${payload}`) ); if (!valid) return null; return JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/'))); } catch { return null; }}// next() — глобальная функция для bidirectional middleware
export default async function middleware(request) { const response = await next(request);
// Инжектируем аналитику только в HTML const contentType = response.headers.get('Content-Type') || ''; if (!contentType.includes('text/html')) { return response; }
const body = await response.text(); const script = '<script src="/analytics.js" defer></script>'; const modifiedBody = body.replace('</head>', `${script}</head>`);
return new Response(modifiedBody, { status: response.status, statusText: response.statusText, headers: response.headers, });}Общие ограничения
Заголовок раздела «Общие ограничения»- 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 не поддерживается