Відлагодження Server Actions у Next.js: помилки розгортання, CORS та обмеження навантаження
Практичний посібник з усунення проблем, який пояснює, чому Server Actions та API-шляхи Next.js працюють без помилок або видають неzрозумілі повідомлення про помилки, та пропонує конкретні рішення для кожного випадку.
Джерела: Common Server Action and API Route Errors in Next.js (and How to Fix Them)
Server Actions значно зменшили складність роботи з логікою бекенду в Next.js — тепер вам більше не потрібен окремий API-шлях лише для обробки надсилання форм. Але ця зручність супроводжується власними проблемами, які зовсім не схожі на баги, з якими доводилося стикатися у старому Pages Router. Ось огляд того, що зазвичай йде не так, та способи вирішення цих проблем.
1. „Помилка: Не вдалося знайти Server Action“ після розгортання
Усе працює нормально на вашому комп’ютері, але як тільки виходить нова версія — все ламається, і користувачі під час сеансу раптово стикаються з цією помилкою.
Причина: Server Actions компілюються з ID, пов’язаними з конкретною версією. Якщо хтось має відкриту вкладку браузера під час розгортання, його клієнт продовжує використовувати ID дій з старої версії, а сервер їх більше не впізнає.
Виправлення: ви не можете повністю запобігти цьому для користувачів, які вже працюють під час розгортання, але можете краще впоратися з цим, замість того щоб дозволити програмі беззвучно зламатися:
'use client';
import { useTransition } from 'react';
function SubmitButton({ action }) {
const [isPending, startTransition] = useTransition(); async function handleClick() {
try {
await action();
} catch (err) {
if (err.message?.includes('Failed to find Server Action')) {
window.location.reload(); // force a fresh client with current build IDs
}
}
} return <button onClick={() => startTransition(handleClick)}>Submit</button>;
}
Захоплення цієї конкретної помилки та запрошення користувача оновити сторінку забезпечує набагато кращий досвід, ніж простий, незрозумілий збій.
2. Дія сервера мовчки не виконується
Ви викликаєте дію, нічого не відбувається, і жодна помилка не виводиться ніде.
Поширена причина: відсутність директиви 'use server' у верхній частині функції або запис дії всередину клієнтського компонента без належної позначки.
// Wrong — no directive
async function saveUser(formData) {
await db.user.update(/* ... */);
}
Виправлення:
'use server';
export async function saveUser(formData) {
await db.user.update(/* ... */);
}
Якщо дія визначена у тому самому файлі, що й клієнтський компонент, директива має розташовуватися на початку самої функції, а не лише на початку файлу:
'use client';
export default function Form() {
async function saveUser(formData) {
'use server'; // required here specifically
await db.user.update(/* ... */);
}
return <form action={saveUser}>{/* ... */}</form>;
}
3. „Помилка: до клієнтських компонентів можна передавати лише звичайні об’єкти“
Ця помилка виникає, коли значення, що повертається серверною дією, не піддається серіалізації — наприклад, екземпляр класу, об’єкт Date, розташований у незвичайній структурі, або модель бази даних із власними методами.
'use server';
export async function getUser(id) {
return await db.user.findUnique({ where: { id } }); // ORM might return a class instance
}
Виправлення — вручну перетворіть результат у звичайний об’єкт:
'use server';
export async function getUser(id) {
const user = await db.user.findUnique({ where: { id } });
return {
id: user.id,
name: user.name,
email: user.email,
createdAt: user.createdAt.toISOString(), // Dates need explicit serialization too
};
}
Не передавайте результат ORM безпосередньо до клієнтського компонента. Спочатку перетворіть його у звичайний, серіалізований об’єкт.
4. Серверна дія працює, але дані форми повертаються як undefined
<form action={createPost}>
<input name="title" />
</form>
'use server';
export async function createPost(formData) {
console.log(formData.title); // undefined
}
аргумент formData, який ви отримуєте, є нативним об’єктом FormData, а не звичайним об’єктом JavaScript, тому вам потрібно викликати метод .get(), щоб отримати з нього значення.
Рішення:
'use server';
export async function createPost(formData) {
const title = formData.get('title');
console.log(title); // correct
}
5. „Об’єм тіла запиту перевищив ліміт у 1 МБ“ у Server Actions
Server Actions встановлюють стандартний ліміт розміру тіла запиту, що стає проблемою при роботі з завантаженням файлів чи великими об’єктами форм.
Рішення — явно збільшити ліміт у файлі next.config.js:
// next.config.js
module.exports = {
experimental: {
serverActions: {
bodySizeLimit: '10mb',
},
},
};
У випадках, схожих на справжнє завантаження файлу, зазвичай краще взагалі не проходити через Server Action під час передачі файлу. Завантажуйте його безпосередньо з клієнта у систему зберігання об’єктів — S3, R2 чи подібну — за допомогою підписаної URL. Це дозволяє уникнути обмежень щодо розміру та зменшує навантаження на ваш сервер.
6. Помилки CORS на шляхах API, які викликаються з іншого джерела
Ця проблема зазвичай виникає, коли мобільний додаток, розширення браузера чи окремий фронтенд безпосередньо звертається до шляхів API Next.js, а не через власні сторінки.
Access to fetch at 'https://yourapp.com/api/data' from origin 'https://otherapp.com' has been blocked by CORS policy
Виправлення — явно встановіть заголовки всередині обробника маршруту:
// app/api/data/route.js
export async function GET(request) {
const data = await getData();
return Response.json(data, {
headers: {
'Access-Control-Allow-Origin': 'https://otherapp.com', // never '*' for authenticated routes
'Access-Control-Allow-Methods': 'GET, POST',
},
});
}
export async function OPTIONS() {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': 'https://otherapp.com',
'Access-Control-Allow-Methods': 'GET, POST',
},
});
}
Уникайте встановлення Access-Control-Allow-Origin: '*' на будь-якому кінцевому пункті, який використовує куки автентифікації. Це фактично передає дані користувача будь-якому джерелу, яке звернеться за ними.
7. Обробник маршруту повертає HTML замість JSON у разі помилки
Коли обробник маршруту викидає некеровану виняток, клієнт не отримує охайного JSON-повідомлення про помилку — замість цього він отримує вбудовану сторінку помилки Next.js у форматі HTML. Це є проблемою для будь-якого клієнтського коду, який припускає, що може безпечно використовувати response.json().
Рішення — обгорніть логіку обробника у try/catch, який завжди надсилатиме JSON:
export async function POST(request) {
try {
const body = await request.json();
const result = await processOrder(body);
return Response.json(result);
} catch (error) {
return Response.json(
{ error: error.message ?? 'Something went wrong' },
{ status: 500 }
);
}
}
Завдяки цьому клієнт завжди отримуватиме дані, які можна обробити як JSON, незалежно від того, успішна чи невдала запит, замість HTML-документа, який призводить до помилок у виклику .json() пізніше.
Чому ці помилки здаються гіршими, ніж баги у старомодних API-маршрутах
Дії сервера об’єднують логіку клієнта та сервера в один файл, що дивовижно полегшує перетин меж без будь-яких поміток. Значення, яке повертається, може виглядати абсолютно нормальним у вашому редакторі, проте все одно спричинити проблеми під час виконання, оскільки його неможливо серіалізувати, або відсутня директива може призвести до тихого збою замість того, щоб виникнути у місці, яке ви очікуєте. Звичайні методи дебаггінгу для API у стилі REST — відкриття вкладки мережі, перевірка відповіді — не завжди допомагають у цьому випадку, адже багато з цих проблем виникають ще до того, як буде надісланий будь-який мережевий запит.
Для команд, які створюють панелі керування з великою кількістю форм, завантаження файлів та операцій, розкиданих по кількох сторінках, варто єдиний раз у всьому кодбазі вирішити такі проблеми, як серіалізація даних, обробка помилок та обмеження розміру тіла запиту, замість того щоб постійно виправляти одні й ті самі проблеми у кожній новій формі. Деякі готові шаблони адмін-панелей для SaaS-продуктів на основі штучного інтелекту вже постачаються з правильно налаштованими Server Actions та API-шляхами, включаючи верифікацію даних за допомогою Zod та належну обробку помилок з самого початку, тож ці сім проблем вирішуються лише один раз, а не знову трапляються у кожній створеній формі.
Висновок
Дії сервера скорочують велику кількість стандартних елементів, які раніше були необхідні для API-шляхів, але вони також приносять із собою свої проблеми: ідентифікатори проектів стають застарілими після розгортання, відсутність необхідних директив, значення, які неможливо серіалізувати, неправильне використання FormData, обмеження за розміром тіла запиту, проблеми CORS між різними доменами та відповіді на помилки, які насправді не є JSON. Усі ці проблеми трапляються часто, усі їх можна виправити за кілька рядків коду, і усі вони потребують швидкої перевірки, перш ніж припускати, що виникає щось більш складне.