Middleware
middleware.ts: перенаправления, перезапись адресов, заголовки и cookie перед каждым запросом, с config.matcher.
Middleware выполняет код до завершения запроса. Он может перенаправлять запросы, переписывать пути, задавать заголовки и cookie или самостоятельно возвращать ответ.
Соглашение
Создайте middleware.ts в корне проекта (или в src/), рядом с app/. Экспортируйте функцию middleware (или используйте экспорт по умолчанию) и необязательный config.
import { NextResponse, type NextRequest } from "@textjs/app/server";export function middleware(request: NextRequest) { if (!request.cookies.has("session")) { return NextResponse.redirect(new URL("/login", request.url)); } return NextResponse.next();}export const config = { matcher: ["/dashboard/:path*", "/api/:path*"],};Middleware выполняется перед обработчиками маршрутов, выдачей статических файлов и страниц консоли при запуске text dev, text start и с любым серверным адаптером развёртывания. Он использует веб-API Request и Response, поэтому один и тот же файл работает на Node, Bun, Vercel Edge и Cloudflare Workers.
Сопоставление путей
Без config.matcher middleware выполняется для каждого запроса. Шаблоны сопоставления используют тот же синтаксис, что и Next.js:
| Шаблон | Совпадения |
|---|---|
/about | /about и /about/ |
/blog/:slug | /blog/hello |
/docs/:path* | /docs, /docs/a/b |
/a/:rest+ | /a/b, но не /a |
/user/:id(\\d+) | /user/42, но не /user/ada |
/((?!api/).*) | Всё, кроме /api/* |
Добавьте условия с помощью has и missing (заголовок, cookie, параметр запроса или хост):
export const config = { matcher: [{ source: "/beta/:path*", has: [{ type: "cookie", key: "beta", value: "1" }] }],};Шаблоны применяются к пути без basePath. В text dev собственные запросы модулей Vite (/@vite/*, исходные файлы) никогда не запускают middleware.
Каждый хост определяет единый канонический путь до запуска middleware и использует его для сопоставления, request.nextUrl и поиска маршрута, обработчика и файла. Процентные последовательности декодируются, а путь кодируется заново так, как его записывает new URL(); сегменты . и .. разрешаются, а повторяющиеся и завершающие косые черты удаляются. /ad%6din//users/ поступает в middleware как /admin/users, а /café остаётся /caf%C3%A9. Шаблон с завершающей косой чертой, например /admin/, соответствует /admin. Запросы, в пути которых есть закодированная прямая или обратная косая черта (%2F, %5C), управляющий символ или некорректное процентное кодирование, получают 400. Маршруты чувствительны к регистру: /Admin не соответствует /admin, а статические файлы выдаются только по их точному имени.
Сопоставляйте URL, по которым выдаётся файл, а не только его имя. Cloudflare Pages выдаёт report.html и по адресу /report, поэтому шаблон для /report.html не защищает /report. Укажите оба адреса, matcher: ["/report", "/report.html"], или храните защищённые файлы в каталоге и сопоставляйте его, например /reports/:path* для файлов в public/reports/.
NextResponse
| API | Действие |
|---|---|
NextResponse.next() | Переход к маршруту |
NextResponse.redirect(url, status?) | 307 по умолчанию; url должен быть абсолютным |
NextResponse.rewrite(url) | Выдача другого пути; внешние источники проксируются |
NextResponse.json(body, init?) | Ответ в формате JSON |
new Response(...) | Ответ с любым содержимым |
Не возвращайте ничего, чтобы продолжить без изменений. next() и rewrite() принимают { status } для изменения статуса итогового ответа. Только ответы, созданные через NextResponse.next() / NextResponse.rewrite(), управляют маршрутизацией: любой другой Response, включая возвращённый из fetch(), отправляется клиенту без изменений, но с удалёнными заголовками x-middleware-*.
request.nextUrl исключает basePath из pathname, а его href и toString() включают его. Чтобы остаться внутри приложения, клонируйте его: const url = request.nextUrl.clone(); url.pathname = "/login"; return NextResponse.redirect(url);.
export function middleware(request: NextRequest) { if (request.nextUrl.pathname === "/status") { return NextResponse.rewrite(new URL("/api/health", request.url)); }}Заголовки
Передайте request.headers, чтобы изменить заголовки, которые получат последующие обработчики, и headers, чтобы добавить заголовки ответа:
export function middleware(request: NextRequest) { const headers = new Headers(request.headers); headers.set("x-user", "ada"); const response = NextResponse.next({ request: { headers } }); response.headers.set("x-frame", "▼"); return response;}Заголовки ответа из middleware добавляются к итоговому ответу, если обработчик маршрута не задаёт тот же заголовок.
Cookie
request.cookies читает заголовок Cookie; response.cookies записывает Set-Cookie.
export function middleware(request: NextRequest) { const theme = request.cookies.get("theme")?.value ?? "geistDark"; const response = NextResponse.next(); response.cookies.set("theme", theme, { httpOnly: true, sameSite: "lax", maxAge: 60 * 60 * 24 }); return response;}Cookie, заданные в ответе next() или rewrite(), доступны и обработчику маршрута в том же запросе.
Фоновая работа
У второго аргумента есть waitUntil(), который сохраняет работу бессерверной среды выполнения после отправки ответа:
import type { NextFetchEvent, NextRequest } from "@textjs/app/server";export function middleware(request: NextRequest, event: NextFetchEvent) { event.waitUntil(fetch("https://analytics.example.com", { method: "POST", body: request.url }));}Примечания
- Страницы с клиентским рендерингом рендерятся в браузере, поэтому переписывание пути на другую страницу меняет выдаваемый файл, а не маршрут, который рендерит консольное приложение. Переписывание путей на обработчики маршрутов, статические файлы и внешние источники работает ожидаемым образом.
- Middleware может читать тело запроса; обработчики маршрутов по-прежнему получают его. Тело запросов, исключённых шаблоном сопоставления, никогда не затрагивается.
request.urlвсегда содержит источник сервера: адреса запросов вроде//other.host/xнормализуются до одной начальной косой черты.- Статический адаптер не может выполнять middleware.