24 августа 2026 г. · 3 мин чтения
revalidatePath во время отрисовки роняет всю страницу
Страница переписки открывалась ошибкой сервера. Разговор в базе создавался, сообщения отправлялись, но открыть переписку было нельзя — маршрут отдавал 500 с одним лишь кодом ошибки в теле.
Код выглядел безобидно. Страница получала список сообщений и заодно ставила отметку «прочитано»:
Функция markConversationRead — обычное серверное действие. Внутри неё, помимо записи в базу, стоял revalidatePath: сбросить кэш списка переписок, чтобы счётчик непрочитанных обновился.
В этом и была ошибка. revalidatePath нельзя вызывать во время отрисовки страницы. Next относится к этому не как к предупреждению: он роняет весь маршрут целиком. Причём падает не то место, где сброс, а страница, поэтому в первый момент кажется, что дело в запросах к базе.
Ошибка неочевидна ровно потому, что вызов выглядит как обычная функция. Слово «действие» в названии ничего не подсказывает — с виду это просто await с записью в базу. А то, что внутри спрятан сброс кэша, вспоминаешь, только когда откроешь файл.
Лечится тем, что отметка ставится прямым запросом, без обёртки в действие. Сбрасывать кэш здесь и не нужно: страница помечена как динамическая и собирается на каждый запрос.
Общее правило: серверные действия предназначены для того, чтобы их вызывали из формы или обработчика нажатия — то есть в ответ на действие человека, а не во время сборки страницы. Если хочется дёрнуть действие из отрисовки, почти всегда нужен просто запрос к базе.
И вторая половина урока. Ошибка прожила неделю, потому что никто не смотрел в журнал: 500 на странице, куда заходит один человек в день, ничем себя не проявляет. После этого случая на сайте появилось слежение за ошибками — оно сообщило бы в ту же минуту.
/* Отметка о прочтении ставится здесь напрямую, а не вызовом серверного действия markConversationRead. Действие внутри дёргает revalidatePath, а он во время отрисовки страницы запрещён — Next роняет весь маршрут с серверной ошибкой. Снаружи это выглядело как «переписка не открывается»: страница отдавала 500, и написать было физически некуда. Сбрасывать кэш здесь и не нужно: страница и так собирается на каждый запрос.*/if (!user.impersonatedBy) { await prisma.conversationMember.update({ where: { conversationId_userId: { conversationId: id, userId: user.id } }, data: { lastReadAt: new Date() }, });}