Middleware
Middleware позволяет выполнять код перед завершением запроса. Затем, на основе входящего запроса, вы можете изменить ответ, переписав его, перенаправив, изменив заголовки запроса или ответа, либо ответив напрямую.
Middleware выполняется до сопоставления кэшированного контента и маршрутов. Подробнее см. в разделе Сопоставление путей.
Варианты использования
Интеграция Middleware в ваше приложение может значительно улучшить производительность, безопасность и пользовательский опыт. Вот некоторые распространённые сценарии, где Middleware особенно эффективен:
- Аутентификация и авторизация: проверка личности пользователя и сессионных куки перед предоставлением доступа к определённым страницам или API-маршрутам.
- Перенаправления на стороне сервера (Server-Side Redirects): перенаправление пользователей на уровне сервера на основе определённых условий (например, локали или роли пользователя).
- Перезапись путей (Path Rewriting): поддержка A/B-тестирования, постепенного внедрения функций или устаревших путей путём динамической перезаписи путей к API-маршрутам или страницам на основе свойств запроса.
- Обнаружение ботов (Bot Detection): защита ресурсов путём обнаружения и блокировки бот-трафика.
- Логирование и аналитика: сбор и анализ данных запроса для получения аналитических данных перед обработкой страницей или API.
- Функциональные флаги (Feature Flagging): динамическое включение или отключение функций для плавного внедрения или тестирования.
Также важно понимать ситуации, когда Middleware может быть не оптимальным решением. Вот некоторые сценарии, на которые стоит обратить внимание:
- Сложная выборка и обработка данных: Middleware не предназначен для прямой выборки или обработки данных — это следует делать в обработчиках маршрутов (Route Handlers) или серверных утилитах.
- Ресурсоёмкие вычислительные задачи: Middleware должен быть лёгким и быстро отвечать, иначе это может вызвать задержки при загрузке страницы. Ресурсоёмкие задачи следует выполнять в выделенных обработчиках маршрутов.
- Расширенное управление сессиями: хотя Middleware может выполнять базовые задачи управления сессиями, расширенное управление должно осуществляться специализированными сервисами аутентификации или в обработчиках маршрутов.
- Прямые операции с базой данных: выполнение прямых операций с базой данных в 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 будет вызываться для каждого маршрута в вашем проекте. Учитывая это, важно использовать матчеры для точного таргетинга или исключения определённых маршрутов. Порядок выполнения следующий:
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
поддерживает полные регулярные выражения, включая негативный просмотр вперёд (negative lookaheads) или сопоставление символов. Пример негативного просмотра вперёд для сопоставления всех путей, кроме определённых:
export const config = {
matcher: [
/*
* Сопоставить все пути запросов, кроме начинающихся с:
* - api (API-маршруты)
* - _next/static (статические файлы)
* - _next/image (файлы оптимизации изображений)
* - favicon.ico (файл фавикона)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
Вы также можете обойти Middleware для определённых запросов, используя массивы missing
или has
, или их комбинацию:
export const config = {
matcher: [
/*
* Сопоставить все пути запросов, кроме начинающихся с:
* - api (API-маршруты)
* - _next/static (статические файлы)
* - _next/image (файлы оптимизации изображений)
* - favicon.ico (файл фавикона)
*/
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
has: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
has: [{ type: 'header', key: 'x-present' }],
missing: [{ type: 'header', key: 'x-missing', value: 'prefetch' }],
},
],
}
Полезно знать: Значения
matcher
должны быть константами, чтобы их можно было статически проанализировать во время сборки. Динамические значения, такие как переменные, будут проигнорированы.
Настроенные матчеры:
- ДОЛЖНЫ начинаться с
/
- Могут включать именованные параметры:
/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
. Поэтому матчер/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
и направлений перезаписи - Устанавливать куки ответа
- Устанавливать заголовки ответа
Для формирования ответа из Middleware вы можете:
- Использовать
rewrite
для маршрута (Страница или Edge API Route), который формирует ответ - Вернуть
NextResponse
напрямую. См. Формирование ответа
Использование куки
Куки — это обычные заголовки. В Request
они хранятся в заголовке Cookie
. В Response
они находятся в заголовке Set-Cookie
. Next.js предоставляет удобный способ доступа и управления этими куки через расширение cookies
в NextRequest
и NextResponse
.
- Для входящих запросов
cookies
предоставляет следующие методы:get
,getAll
,set
иdelete
. Вы можете проверить наличие куки с помощьюhas
или удалить все куки с помощью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"
// Получение куки из запроса с помощью 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
// Установка куки в ответе с помощью 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"
// Получение куки из запроса с помощью 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
// Установка куки в ответе с помощью 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.rewrite
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.rewrite
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*',
}
Формирование ответа
Вы можете напрямую отвечать из Middleware, возвращая экземпляр Response
или NextResponse
. (Доступно начиная с Next.js v13.1.0)
import { 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: 'Ошибка аутентификации' },
{ 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: 'Ошибка аутентификации' },
{ 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
В версии v13.1
Next.js были добавлены два дополнительных флага для 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+)/)
) {
req.nextUrl.pathname += '/'
return NextResponse.redirect(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
}
Среда выполнения (Runtime)
Middleware в настоящее время поддерживает только Edge runtime. Среда выполнения Node.js не поддерживается.
История версий
Версия | Изменения |
---|---|
v13.1.0 | Добавлены расширенные флаги Middleware |
v13.0.0 | Middleware может изменять заголовки запросов, заголовки ответов и отправлять ответы |
v12.2.0 | Middleware стал стабильным, см. руководство по обновлению |
v12.0.9 | Принудительное использование абсолютных URL в Edge Runtime (PR) |
v12.0.0 | Добавлен Middleware (бета-версия) |