Débogage des actions serveur Next.js : erreurs de déploiement, CORS et limites de charge utile
Un guide pratique de dépannage expliquant pourquoi les actions serveur et les routes API de Next.js échouent en silence ou génèrent des erreurs obscures, avec des solutions concrètes pour chaque cas.
Sources: Common Server Action and API Route Errors in Next.js (and How to Fix Them)
Les actions serveur ont considérablement réduit les complexités liées à la logique backend dans Next.js — il n’est plus nécessaire d’utiliser une route API dédiée simplement pour gérer la soumission d’un formulaire. Cependant, cette commodité s’accompagne de ses propres problèmes de fonctionnement, qui ne ressemblent en rien aux bugs rencontrés dans l’ancien système Pages Router. Voici un aperçu des problèmes fréquents et des moyens de les résoudre.
1. "Erreur : Impossible de trouver l’action serveur" après un déploiement
Tout fonctionne correctement sur votre machine, mais cela cesse de fonctionner dès que une nouvelle version est déployée — les utilisateurs en cours de session se heurtent soudainement à cette erreur.
Cause : Les actions serveur sont compilées avec des identifiants liés à une version spécifique. Si quelqu’un a une fenêtre du navigateur ouverte pendant le déploiement, son client continue de faire référence aux identifiants des actions de la vieille version, que le serveur ne reconnaît plus.
Réparation : il n’est pas possible d’empêcher complètement ce phénomène pour les utilisateurs déjà actifs pendant un déploiement, mais on peut gérer la situation de manière plus souple au lieu de laisser l’application planter en silence :
'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>;
}
Attraper cette erreur spécifique et inciter l’utilisateur à actualiser la page offre une expérience bien meilleure qu’une panne brutale et inexplicable.
2. L’action serveur ne s’exécute pas en silence
Cause fréquente : omission de la directive 'use server' en haut de la fonction, ou écriture de l’action à l’intérieur d’un composant client sans la marquer correctement.
// Wrong — no directive
async function saveUser(formData) {
await db.user.update(/* ... */);
}
Réparation :
'use server';
export async function saveUser(formData) {
await db.user.update(/* ... */);
}
Si l’action est définie dans le même fichier qu’un composant client, la directive doit se trouver en haut de la fonction elle-même, et non simplement en haut du fichier :
'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. « Erreur : Seuls les objets simples peuvent être transmis aux composants clients »
Cette erreur apparaît lorsque la valeur de retour d’une action serveur n’est pas serialisable — par exemple une instance de classe, un Date inséré au sein d’une structure inhabituelle, ou un modèle de base de données contenant ses propres méthodes.
'use server';
export async function getUser(id) {
return await db.user.findUnique({ where: { id } }); // ORM might return a class instance
}
Réparation — reformater manuellement le résultat en un objet simple :
'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
};
}
Ne passez pas directement le résultat d’un ORM à un composant client. Convertissez-le d’abord en un objet simple et serialisable.
4. L’action serveur fonctionne, mais les données du formulaire reviennent undefined
<form action={createPost}>
<input name="title" />
</form>
'use server';
export async function createPost(formData) {
console.log(formData.title); // undefined
}
Cause : l’argument formData que vous recevez est une instance native de FormData, et non un objet JavaScript ordinaire ; il vous faut donc appeler la méthode .get() pour en extraire les valeurs.
Réparation :
'use server';
export async function createPost(formData) {
const title = formData.get('title');
console.log(title); // correct
}
5. « Le corps de la requête dépasse la limite de 1 Mo » dans les Server Actions
Les Server Actions imposent une limite par défaut à la taille du corps de la requête, ce qui pose problème dès que l’on traite des téléchargements de fichiers ou des en-têtes de formulaire volumineux.
Réparation — augmentez explicitement la limite dans next.config.js :
// next.config.js
module.exports = {
experimental: {
serverActions: {
bodySizeLimit: '10mb',
},
},
};
Pour tout ce qui ressemble à un véritable téléchargement de fichier, il est généralement préférable d’éviter complètement l’utilisation des Server Actions pour acheminer le fichier. Téléchargez directement depuis le client vers un système de stockage d’objets — S3, R2 ou similaire — en utilisant une URL signée. Cela permet de contourner la limite de taille et de réduire la charge sur votre serveur.
6. Erreurs CORS sur les routes API appelées depuis une origine différente
Ces erreurs apparaissent généralement lorsque une application mobile, une extension de navigateur ou un frontend indépendant accède directement aux routes API de Next.js au lieu d’utiliser ses propres pages.
Access to fetch at 'https://yourapp.com/api/data' from origin 'https://otherapp.com' has been blocked by CORS policy
Résolution — définir explicitement les en-têtes à l’intérieur du gestionnaire de route :
// 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',
},
});
}
Évitez de définir Access-Control-Allow-Origin: '*' sur tout point d’entrée qui dépend de cookies d’authentification. Cela revient en fait à transmettre les données de l’utilisateur à n’importe quelle origine qui le demande.
7. Le gestionnaire de route renvoie du HTML plutôt que du JSON en cas d’erreur
Lorsqu’un gestionnaire de route génère une exception non gérée, le client ne reçoit pas d’erreur JSON bien structurée — il obtient plutôt la page d’erreur HTML intégrée à Next.js. Cela pose un problème pour tout code client qui suppose pouvoir appeler en toute sécurité response.json().
Réparation — enveloppez la logique du gestionnaire dans un try/catch qui renvoie toujours du 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 }
);
}
}
Avec cette solution en place, le client reçoit toujours du contenu pouvant être analysé comme JSON, que la requête réussisse ou échoue, au lieu d’un document HTML qui provoquerait des erreurs dans l’appel .json() ultérieur.
Pourquoi ces erreurs semblent pires que les bugs des routes API de l’ancienne génération
Les actions serveur regroupent la logique client et serveur dans le même fichier, ce qui rend étonnamment facile de franchir une frontière sans s’en rendre compte. Une valeur de retour peut paraître parfaite dans votre éditeur et provoquer des problèmes en temps de exécution parce qu’elle n’est pas serialisable, ou une directive manquante peut entraîner des échecs silencieux au lieu de générer l’erreur attendue. Les habitudes habituelles de débogage utilisées avec les API de type REST — ouvrir l’onglet réseau, examiner la réponse — ne sont pas toujours utiles ici, car plusieurs de ces échecs se produisent avant même que toute requête réseau ne soit envoyée.
Pour les équipes qui créent des tableaux de bord remplis de formulaires, de téléchargements de fichiers et de mutations réparties sur de nombreuses pages, il est avantageux de résoudre de manière cohérente ces problématiques — sérialisation, gestion des erreurs, limites de taille du corps des requêtes — une seule fois dans l’ensemble du code, plutôt que de devoir déboguer les mêmes problèmes à chaque nouveau formulaire. Certains modèles administratifs prédéfinis pour des produits SaaS basés sur l’IA sont déjà fournis avec des actions serveur et des routes API correctement configurées, y compris une validation via Zod ainsi qu’une gestion appropriée des erreurs intégrée dès le départ, ce qui permet de résoudre ces sept problèmes une seule fois au lieu qu’ils réapparaissent dans chaque formulaire que vous créez.
En résumé
Les actions serveur réduisent une grande partie du code générique que les routes API devaient auparavant inclure, mais elles apportent avec elles leur propre ensemble de modes d’échec : des IDs de construction qui deviennent obsolètes après un déploiement, l’absence de directives, des valeurs de retour qui ne peuvent pas être sérialisées, une utilisation incorrecte de FormData, des limites de taille du corps, des échecs CORS entre origines différentes, ainsi que des réponses d’erreur qui ne sont pas vraiment en JSON. Tous ces problèmes se produisent fréquemment, tous peuvent être résolus en quelques lignes seulement, et tous méritent une vérification rapide avant de supposer qu’un problème plus complexe est à l’origine du souci.