Middleware
Middleware позволяет выполнять код перед завершением запроса. Затем, на основе входящего запроса, вы можете изменить ответ, переписав его, перенаправив, изменив заголовки запроса или ответа, либо ответив напрямую.
Middleware выполняется до сопоставления кэшированного контента и маршрутов. Подробнее см. в разделе Сопоставление путей.
Варианты использования
Некоторые распространённые сценарии, где Middleware эффективен:
- Быстрое перенаправление после чтения частей входящего запроса
- Перезапись на разные страницы на основе A/B-тестов или экспериментов
- Изменение заголовков для всех страниц или их подмножества
Middleware не подходит для:
- Медленного получения данных
- Управления сеансами
Конвенция
Используйте файл middleware.ts
(или .js
) в корне вашего проекта для определения Middleware. Например, на том же уровне, что pages
или app
, или внутри src
, если применимо.
Примечание: Хотя поддерживается только один файл
middleware.ts
на проект, вы всё равно можете организовать логику Middleware модульно. Разделяйте функциональность Middleware на отдельные файлы.ts
или.js
и импортируйте их в основной файлmiddleware.ts
. Это позволяет более чисто управлять Middleware для конкретных маршрутов, агрегируя их вmiddleware.ts
для централизованного контроля. Ограничение одним файлом Middleware упрощает конфигурацию, предотвращает потенциальные конфликты и оптимизирует производительность, избегая нескольких слоёв Middleware.
Пример
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// Эта функция может быть помечена как `async`, если внутри используется `await`
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}
// Подробнее см. в разделе "Сопоставление путей"
export const config = {
matcher: '/about/:path*',
}
import { NextResponse } from 'next/server'
// Эта функция может быть помечена как `async`, если внутри используется `await`
export function middleware(request) {
return NextResponse.redirect(new URL('/home', request.url))
}
// Подробнее см. в разделе "Сопоставление путей"
export const config = {
matcher: '/about/:path*',
}
Сопоставление путей
Middleware будет вызываться для каждого маршрута в вашем проекте. Учитывая это, крайне важно использовать matchers для точного таргетинга или исключения определённых маршрутов. Порядок выполнения следующий:
headers
изnext.config.js
redirects
изnext.config.js
- Middleware (
rewrites
,redirects
и т.д.) beforeFiles
(rewrites
) изnext.config.js
- Файловые маршруты (
public/
,_next/static/
,pages/
,app/
и т.д.) afterFiles
(rewrites
) изnext.config.js
- Динамические маршруты (
/blog/[slug]
) fallback
(rewrites
) изnext.config.js
Есть два способа определить, на каких путях будет выполняться Middleware:
Matcher
matcher
позволяет фильтровать Middleware для выполнения на определённых путях.
export const config = {
matcher: '/about/:path*',
}
Вы можете сопоставить один путь или несколько путей с помощью синтаксиса массива:
export const config = {
matcher: ['/about/:path*', '/dashboard/:path*'],
}
Конфигурация matcher
поддерживает полные регулярные выражения, включая негативные просмотры вперёд и сопоставление символов. Пример негативного просмотра вперёд для сопоставления всех путей, кроме определённых:
export const config = {
matcher: [
/*
* Сопоставить все пути запросов, кроме начинающихся с:
* - api (API-маршруты)
* - _next/static (статические файлы)
* - _next/image (файлы оптимизации изображений)
* - favicon.ico, sitemap.xml, robots.txt (метаданные)
*/
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
],
}
Вы также можете обойти Middleware для определённых запросов, используя массивы missing
или has
, или их комбинацию:
export const config = {
matcher: [
/*
* Сопоставить все пути запросов, кроме начинающихся с:
* - api (API-маршруты)
* - _next/static (статические файлы)
* - _next/image (файлы оптимизации изображений)
* - favicon.ico, sitemap.xml, robots.txt (метаданные)
*/
{
source:
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
{
source:
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
has: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
{
source:
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
has: [{ type: 'header', key: 'x-present' }],
missing: [{ type: 'header', key: 'x-missing', value: 'prefetch' }],
},
],
}
Полезно знать: Значения
matcher
должны быть константами, чтобы их можно было статически проанализировать во время сборки. Динамические значения, такие как переменные, будут проигнорированы.
Настроенные matchers:
- ДОЛЖНЫ начинаться с
/
- Могут включать именованные параметры:
/about/:path
соответствует/about/a
и/about/b
, но не/about/a/c
- Могут иметь модификаторы для именованных параметров (начинающихся с
:
):/about/:path*
соответствует/about/a/b/c
, потому что*
означает ноль или более.?
означает ноль или один, а+
— один или более - Могут использовать регулярные выражения, заключённые в скобки:
/about/(.*)
эквивалентно/about/:path*
Подробнее см. в документации path-to-regexp.
Полезно знать: Для обратной совместимости Next.js всегда рассматривает
/public
как/public/index
. Поэтому matcher/public/:path
будет соответствовать.
Условные операторы
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/about')) {
return NextResponse.rewrite(new URL('/about-2', request.url))
}
if (request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.rewrite(new URL('/dashboard/user', request.url))
}
}
import { NextResponse } from 'next/server'
export function middleware(request) {
if (request.nextUrl.pathname.startsWith('/about')) {
return NextResponse.rewrite(new URL('/about-2', request.url))
}
if (request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.rewrite(new URL('/dashboard/user', request.url))
}
}
NextResponse
API NextResponse
позволяет:
redirect
перенаправлять входящий запрос на другой URLrewrite
перезаписывать ответ, отображая заданный URL- Устанавливать заголовки запросов для API-маршрутов,
getServerSideProps
и направленийrewrite
- Устанавливать cookies ответа
- Устанавливать заголовки ответа
Для создания ответа из Middleware вы можете:
rewrite
на маршрут (Страницу или Обработчик маршрута), который создаёт ответ- Вернуть
NextResponse
напрямую. См. Создание ответа
Использование Cookies
Cookies — это обычные заголовки. В Request
они хранятся в заголовке Cookie
. В Response
они находятся в заголовке Set-Cookie
. Next.js предоставляет удобный способ доступа и управления этими cookies через расширение cookies
в NextRequest
и NextResponse
.
- Для входящих запросов
cookies
имеет следующие методы:get
,getAll
,set
иdelete
cookies. Вы можете проверить наличие cookie с помощьюhas
или удалить все cookies с помощьюclear
. - Для исходящих ответов
cookies
имеют следующие методы:get
,getAll
,set
иdelete
.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Предположим, что во входящем запросе присутствует заголовок "Cookie:nextjs=fast"
// Получение cookies из запроса с помощью API `RequestCookies`
let cookie = request.cookies.get('nextjs')
console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
const allCookies = request.cookies.getAll()
console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]
request.cookies.has('nextjs') // => true
request.cookies.delete('nextjs')
request.cookies.has('nextjs') // => false
// Установка cookies в ответе с помощью API `ResponseCookies`
const response = NextResponse.next()
response.cookies.set('vercel', 'fast')
response.cookies.set({
name: 'vercel',
value: 'fast',
path: '/',
})
cookie = response.cookies.get('vercel')
console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }
// Исходящий ответ будет иметь заголовок `Set-Cookie:vercel=fast;path=/`.
return response
}
import { NextResponse } from 'next/server'
export function middleware(request) {
// Предположим, что во входящем запросе присутствует заголовок "Cookie:nextjs=fast"
// Получение cookies из запроса с помощью API `RequestCookies`
let cookie = request.cookies.get('nextjs')
console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
const allCookies = request.cookies.getAll()
console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]
request.cookies.has('nextjs') // => true
request.cookies.delete('nextjs')
request.cookies.has('nextjs') // => false
// Установка cookies в ответе с помощью API `ResponseCookies`
const response = NextResponse.next()
response.cookies.set('vercel', 'fast')
response.cookies.set({
name: 'vercel',
value: 'fast',
path: '/',
})
cookie = response.cookies.get('vercel')
console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }
// Исходящий ответ будет иметь заголовок `Set-Cookie:vercel=fast;path=/test`.
return response
}
Установка заголовков
Вы можете устанавливать заголовки запросов и ответов с помощью API NextResponse
(установка заголовков запросов доступна начиная с Next.js v13.0.0).
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Клонируем заголовки запроса и устанавливаем новый заголовок `x-hello-from-middleware1`
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-hello-from-middleware1', 'hello')
// Вы также можете установить заголовки запроса в NextResponse.next
const response = NextResponse.next({
request: {
// Новые заголовки запроса
headers: requestHeaders,
},
})
// Устанавливаем новый заголовок ответа `x-hello-from-middleware2`
response.headers.set('x-hello-from-middleware2', 'hello')
return response
}
import { NextResponse } from 'next/server'
export function middleware(request) {
// Клонируем заголовки запроса и устанавливаем новый заголовок `x-hello-from-middleware1`
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-hello-from-middleware1', 'hello')
// Вы также можете установить заголовки запроса в NextResponse.next
const response = NextResponse.next({
request: {
// Новые заголовки запроса
headers: requestHeaders,
},
})
// Устанавливаем новый заголовок ответа `x-hello-from-middleware2`
response.headers.set('x-hello-from-middleware2', 'hello')
return response
}
Полезно знать: Избегайте установки больших заголовков, так как это может вызвать ошибку 431 Request Header Fields Too Large в зависимости от конфигурации вашего сервера.
CORS
Вы можете установить заголовки CORS в Middleware для разрешения кросс-доменных запросов, включая простые и предварительные запросы.
import { NextRequest, NextResponse } from 'next/server'
const allowedOrigins = ['https://acme.com', 'https://my-app.org']
const corsOptions = {
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
export function middleware(request: NextRequest) {
// Проверяем источник из запроса
const origin = request.headers.get('origin') ?? ''
const isAllowedOrigin = allowedOrigins.includes(origin)
// Обрабатываем предварительные запросы
const isPreflight = request.method === 'OPTIONS'
if (isPreflight) {
const preflightHeaders = {
...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
...corsOptions,
}
return NextResponse.json({}, { headers: preflightHeaders })
}
// Обрабатываем простые запросы
const response = NextResponse.next()
if (isAllowedOrigin) {
response.headers.set('Access-Control-Allow-Origin', origin)
}
Object.entries(corsOptions).forEach(([key, value]) => {
response.headers.set(key, value)
})
return response
}
export const config = {
matcher: '/api/:path*',
}
import { NextResponse } from 'next/server'
const allowedOrigins = ['https://acme.com', 'https://my-app.org']
const corsOptions = {
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
export function middleware(request) {
// Проверяем источник из запроса
const origin = request.headers.get('origin') ?? ''
const isAllowedOrigin = allowedOrigins.includes(origin)
// Обрабатываем предварительные запросы
const isPreflight = request.method === 'OPTIONS'
if (isPreflight) {
const preflightHeaders = {
...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
...corsOptions,
}
return NextResponse.json({}, { headers: preflightHeaders })
}
// Обрабатываем простые запросы
const response = NextResponse.next()
if (isAllowedOrigin) {
response.headers.set('Access-Control-Allow-Origin', origin)
}
Object.entries(corsOptions).forEach(([key, value]) => {
response.headers.set(key, value)
})
return response
}
export const config = {
matcher: '/api/:path*',
}
Полезно знать: Вы можете настроить заголовки CORS для отдельных маршрутов в Обработчиках маршрутов.
Формирование ответа
Вы можете напрямую отвечать из Middleware, возвращая экземпляр Response
или NextResponse
. (Доступно начиная с Next.js v13.1.0)
import type { NextRequest } from 'next/server'
import { isAuthenticated } from '@lib/auth'
// Ограничиваем middleware путями, начинающимися с `/api/`
export const config = {
matcher: '/api/:function*',
}
export function middleware(request: NextRequest) {
// Вызываем функцию аутентификации для проверки запроса
if (!isAuthenticated(request)) {
// Отвечаем JSON с сообщением об ошибке
return Response.json(
{ success: false, message: 'authentication failed' },
{ status: 401 }
)
}
}
import { isAuthenticated } from '@lib/auth'
// Ограничиваем middleware путями, начинающимися с `/api/`
export const config = {
matcher: '/api/:function*',
}
export function middleware(request) {
// Вызываем функцию аутентификации для проверки запроса
if (!isAuthenticated(request)) {
// Отвечаем JSON с сообщением об ошибке
return Response.json(
{ success: false, message: 'authentication failed' },
{ status: 401 }
)
}
}
waitUntil
и NextFetchEvent
Объект NextFetchEvent
расширяет нативный FetchEvent
и включает метод waitUntil()
.
Метод waitUntil()
принимает промис в качестве аргумента и продлевает время жизни Middleware до его выполнения. Это полезно для выполнения фоновых задач.
import { NextResponse } from 'next/server'
import type { NextFetchEvent, NextRequest } from 'next/server'
export function middleware(req: NextRequest, event: NextFetchEvent) {
event.waitUntil(
fetch('https://my-analytics-platform.com', {
method: 'POST',
body: JSON.stringify({ pathname: req.nextUrl.pathname }),
})
)
return NextResponse.next()
}
Расширенные флаги Middleware
В Next.js v13.1 были добавлены два дополнительных флага для middleware: skipMiddlewareUrlNormalize
и skipTrailingSlashRedirect
для обработки сложных сценариев.
skipTrailingSlashRedirect
отключает редиректы Next.js для добавления или удаления завершающих слешей. Это позволяет кастомно обрабатывать слеши внутри middleware, сохраняя их для одних путей и удаляя для других, что упрощает постепенный переход.
module.exports = {
skipTrailingSlashRedirect: true,
}
const legacyPrefixes = ['/docs', '/blog']
export default async function middleware(req) {
const { pathname } = req.nextUrl
if (legacyPrefixes.some((prefix) => pathname.startsWith(prefix))) {
return NextResponse.next()
}
// обработка завершающего слеша
if (
!pathname.endsWith('/') &&
!pathname.match(/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+)/)
) {
return NextResponse.redirect(
new URL(`${req.nextUrl.pathname}/`, req.nextUrl)
)
}
}
skipMiddlewareUrlNormalize
позволяет отключить нормализацию URL в Next.js для одинаковой обработки прямых переходов и клиентских навигаций. В сложных случаях это дает полный контроль, используя оригинальный URL.
module.exports = {
skipMiddlewareUrlNormalize: true,
}
export default async function middleware(req) {
const { pathname } = req.nextUrl
// GET /_next/data/build-id/hello.json
console.log(pathname)
// с флагом будет /_next/data/build-id/hello.json
// без флага нормализуется в /hello
}
Юнит-тестирование (экспериментальное)
Начиная с Next.js 15.1, пакет next/experimental/testing/server
содержит утилиты для юнит-тестирования middleware. Тестирование помогает убедиться, что middleware выполняется только для нужных путей, а кастомная логика маршрутизации работает корректно перед попаданием в продакшен.
Функция unstable_doesMiddlewareMatch
проверяет, будет ли middleware выполняться для данного URL, заголовков и кук.
import { unstable_doesMiddlewareMatch } from 'next/experimental/testing/server'
expect(
unstable_doesMiddlewareMatch({
config,
nextConfig,
url: '/test',
})
).toEqual(false)
Также можно тестировать всю функцию middleware.
import { isRewrite, getRewrittenUrl } from 'next/experimental/testing/server'
const request = new NextRequest('https://nextjs.org/docs')
const response = await middleware(request)
expect(isRewrite(response)).toEqual(true)
expect(getRewrittenUrl(response)).toEqual('https://other-domain.com/docs')
// getRedirectUrl также можно использовать, если ответ - редирект
Рантайм
По умолчанию Middleware использует Edge Runtime. Начиная с v15.2 (canary), доступна экспериментальная поддержка Node.js Runtime. Для активации добавьте флаг в next.config
:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
nodeMiddleware: true,
},
}
export default nextConfig
const nextConfig = {
experimental: {
nodeMiddleware: true,
},
}
export default nextConfig
Затем в файле middleware укажите runtime: 'nodejs'
в объекте config
:
export const config = {
runtime: 'nodejs',
}
export const config = {
runtime: 'nodejs',
}
Примечание: Эта функция пока не рекомендуется для продакшена. Next.js выдаст ошибку, если вы используете стабильную версию вместо next@canary.
Поддерживаемые платформы
Вариант развертывания | Поддержка |
---|---|
Сервер Node.js | Да |
Docker-контейнер | Да |
Статический экспорт | Нет |
Адаптеры | Зависит от платформы |
Узнайте, как настроить Middleware при самостоятельном хостинге Next.js.
История версий
Версия | Изменения |
---|---|
v15.2.0 | Добавлена возможность использовать Node.js Runtime для Middleware (экспериментально) |
v13.1.0 | Добавлены расширенные флаги Middleware |
v13.0.0 | Middleware может изменять заголовки запросов, заголовки ответов и отправлять ответы |
v12.2.0 | Middleware стабилизирован, см. руководство по обновлению |
v12.0.9 | Обязательное использование абсолютных URL в Edge Runtime (PR) |
v12.0.0 | Добавлен Middleware (бета-версия) |