Надпись «Не найдено» и HTTP-статус не всегда совпадают. В Next.js 15 с App Router состояние notFound() может возникнуть после того, как сервер уже начал отдавать документ. Заголовки к этому моменту отправлены, поэтому позднее состояние отсутствия не превращает начатый ответ в новый ответ с другим статусом.
Это отдельный случай, который важно отличать от самодельного шаблона ошибки: приложение просто вернуло текст «Товар не найден» внутри обычной успешной страницы. Посмотрим оба сценария на собственном учебном примере. Здесь нет результатов проверки настоящего магазина; код нужен для воспроизведения поведения на стенде.
Что означает потоковая отдача для статуса
При потоковой отдаче браузер получает документ частями. Например, сначала появляется общий интерфейс и экран загрузки, а затем результат обращения к данным товара. Если отсутствие подтвердилось во второй части, изменить уже отправленные HTTP-заголовки нельзя.
Next.js прямо описывает разницу: not-found возвращает 200 для потоковых ответов и 404 для непотоковых. Поэтому обнаруженный 200 не доказывает, что функция отсутствия не сработала. Документация not-found для версии 15.
Но и обратный вывод неверен. Сам по себе 200 рядом с текстом «Не найдено» не доказывает корректную обработку Next.js. Нужно найти, какой код создал сообщение, началась ли отдача заранее и какие поисковые директивы присутствуют в завершённом ответе.
Отличите notFound от простого сообщения
В первом сценарии страница выполняет return <p>Товар не найден</p>. Такой возврат сам по себе не задаёт 404 и не означает вызова механизма notFound. Во втором сценарии она вызывает импортированную из next/navigation функцию notFound(), после чего используется соответствующий файл интерфейса отсутствия.
Функция также добавляет noindex. Это важная часть проверки, особенно когда начальный статус документа уже равен 200. Поведение функции описано в справке notFound. Уведомление об отсутствии и директива поиска проверяются вместе, а не угадываются по виду экрана.
Найдите точную ветку для нужного URL. Если notFound() есть в проекте, но вызывается только на другой странице, это ничего не объясняет. Попросите показать путь данных: получение записи, решение о её наличии, вызов функции и компонент not-found.tsx для данного сегмента.
Воспроизведите позднее отсутствие на стенде
Учебная страница app/catalog/[slug]/page.tsx ниже возвращает один существующий товар. Для любого другого slug она искусственно ждёт полторы секунды и вызывает notFound. Задержка нужна только для эксперимента и не должна переходить в боевой каталог.
import { notFound } from 'next/navigation'
export default async function ProductPage({ params }: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
if (slug === 'red-mug') return <h1>Красная кружка</h1>
await new Promise((resolve) => setTimeout(resolve, 1500))
notFound()
}Добавьте app/catalog/[slug]/loading.tsx и not-found.tsx. Первый файл создаёт предусмотренный экран загрузки для сегмента, второй — сообщение об отсутствии. На своём проекте проверьте, какой именно layout оборачивает эти файлы.
// loading.tsx
export default function LoadingProduct() {
return <p>Загружаем сведения о товаре.</p>
}// not-found.tsx
import Link from 'next/link'
export default function MissingProduct() {
return <section>
<h1>Товар не найден</h1>
<p>Проверьте адрес или выберите товар в каталоге.</p>
<Link href="/catalog">Открыть каталог</Link>
</section>
}Выполните production-сборку и запустите её на стенде. Откройте существующий /catalog/red-mug и заведомо отсутствующий /catalog/missing-mug. Отдельно повторите эксперимент без loading.tsx. Результат зависит и от других границ ожидания в вашем дереве, поэтому удаление одного файла не является универсальным обещанием смены статуса.
Сохраните документ и проверьте noindex
В панели Network выберите запрос основного документа. Не путайте его с запросом API, изображения, prefetch или служебного ответа React Server Components. Для дополнительной проверки можно сохранить заголовки и полный HTML обычным GET-запросом к стенду.
curl -sS -D /tmp/missing-product.headers \
-o /tmp/missing-product.html \
https://staging.example.ru/catalog/missing-mugАдрес в команде условный. Используйте свой разрешённый стенд, а не случайный чужой домен. В файле заголовков найдите статус основного ответа. В завершённом HTML найдите поисковую директиву, затем сравните её с итоговым DOM браузера. Ранний фрагмент документа может ещё не содержать позднее добавленную разметку.
Если перед приложением стоит proxy или CDN, сравните поведение через него и напрямую в своей тестовой среде. Промежуточный сервер может буферизовать поток или заменить ответ собственной страницей ошибки. Сохраните используемый User-Agent и настройки кеша, чтобы повторный тест проверял тот же сценарий.
Запишите результаты отдельно для существующего и отсутствующего товара. У настоящей карточки не должно оказаться noindex, оставшегося из общего шаблона. Если такая директива есть и на обычных страницах, разберите наследование metadata, а не только состояние отсутствия.
Решите, нужен ли вашему маршруту строгий 404
Если внешний потребитель требует именно 404, проверку существования нужно завершать до отправки заголовков соответствующего ответа. Разработчик должен предложить устройство этого маршрута и подтвердить его тестом. Замена надписи в not-found.tsx ничего не меняет в моменте принятия решения.
Не отключайте потоковую отдачу всего сайта ради одной карточки без оценки последствий. Сначала выясните, откуда приходит позднее решение, можно ли раньше проверить существование и какие части действительно нуждаются в ожидании. Подходящий вариант зависит от источника данных, кеширования и структуры маршрутов.
В Next.js 15 есть экспериментальный global-not-found для несуществующих маршрутов приложения. Он не является автоматическим решением отсутствующей записи внутри существующего динамического маршрута /catalog/[slug]. Любой slug уже может подходить под этот маршрут, а наличие товара выясняется отдельно.
Также проверьте, почему запись объявлена отсутствующей. Если общий catch превращает тайм-аут API в notFound, сначала исправляйте различие причин. Нужный статус для действительно удалённого товара не оправдывает сообщение об удалении при обычном сбое сервиса.
Критерии приёмки исправления
Согласуйте ожидаемый результат для трёх случаев: существующий товар, подтверждённое отсутствие и временный сбой источника. Для каждого сохраните статус документа, конечный URL, интерфейс и поисковые директивы. Если потоковый 200 допустим по вашему контракту, это должно быть явным решением, а не случайно пропущенной проверкой.
Повторите прямой запрос после публикации исправления и сравните его с переходом из каталога. При наличии кеша проверьте свежий запрос и следующую обычную загрузку. Исчезновение ошибки только в dev-режиме не подтверждает результат production-маршрута.
Технический аудит страницы поможет зафиксировать полученный ответ и доступные замечания на дату проверки. Он не заменяет эксперимент с границами загрузки и всеми ответами API. Общая структура маршрутов изложена в руководстве по Next.js 15.
Правильно выполненная работа даёт команде воспроизводимое объяснение ответа и понятную обработку каждого состояния. Она не гарантирует решение поисковой системы по индексации: это отдельно наблюдают после повторного обхода страницы.