Express est apprécié pour sa légèreté et sa flexibilité. Cette souplesse implique qu'une petite mauvaise configuration ou un nouveau chemin de code peut produire des comportements déroutants sous charge ou après un changement en apparence anodin. Ce guide offre un chemin pratique pour isoler vite les problèmes, valider les correctifs et rétablir le service en sécurité. Il s'agit de Express API troubleshooting appliqué au quotidien, avec des repères concrets (Express API errors, Express API logs, Express API commands, Express API recovery).
Il se concentre sur ce que vous pouvez observer et modifier à faible risque : versions, variables d'environnement, occupation de port, logs, parsing JSON, CORS, gestion d'erreurs, et pannes courantes de la couche données (par exemple, MongoDB). Chaque étape n'introduit des commandes ou de la configuration que lorsqu'elles s'appliquent directement. Les exemples construits sont signalés comme tels. Remplacez les ports, environnements et chemins d'exemple par les vôtres.
Introduction
Le dépannage fonctionne mieux lorsqu'il est visible et limité. Commencez par inventorier exactement ce qui tourne, puis adoptez une configuration minimale et sûre pour supprimer les angles morts (journalisation, gestion centralisée des erreurs, endpoints de santé). Vérifiez d'abord localement avant d'élargir. Gardez des étapes de reprise réversibles et faciles à valider.
Inventaire des versions et de l'environnement
Avant de modifier quoi que ce soit, relevez précisément ce qui s'exécute. Cela évite de « chasser des fantômes » causés par des versions ou variables d'environnement décalées.
- Relever les versions du runtime
- Version Node.js :
node -v
- Version npm :
npm -v
- Version locale d'Express utilisée par le projet :
npm list express
Dans un monorepo, notez la version sous le chemin du service API.
- Capturer l'OS et l'usage du port
- Infos de base OS et utilisateur courant :
uname -a # macOS/Linux
ver # Windows Command Prompt
- Occupation d'un port typique d'Express (remplacez 3000) :
lsof -i :3000 -P -n # macOS/Linux
netstat -anp | grep :3000 # Alternative Linux
netstat -ano | findstr :3000 # Windows
Si un autre processus occupe le port, vous verrez son PID et déciderez de l'arrêter ou d'utiliser un autre port pour l'API.
- Snapshot des variables d'environnement
Variables typiques : PORT, NODE_ENV et réglages de connexion base de données.
# macOS/Linux
printenv | grep -E '^(PORT|NODE_ENV|MONGODB_|DB_|JWT_|API_)'
# Windows PowerShell
Get-ChildItem Env: | Where-Object { $_.Name -match '^(PORT|NODE_ENV|MONGODB_|DB_|JWT_|API_)' }
Enregistrez la sortie dans vos notes d'incident. De petits écarts (PORT manquant, NODE_ENV différent) expliquent beaucoup de surprises.
- Inspecter package.json et la commande de démarrage
Ouvrez package.json et relevez :
- scripts.start et scripts.dev
- engines (si présent)
- express, body-parser (le cas échéant), cors, bibliothèques de logs (morgan, pino, winston)
Ces éléments déterminent comment le service démarre et quels middlewares s'exécutent.
Chemin de configuration sûre
Adoptez de petits changements réversibles qui améliorent l'observabilité et la sécurité sans toucher au métier.
- Journalisation minimale et gestion d'erreurs
Ajoutez la journalisation des requêtes avec morgan (adapté au dev) et un gestionnaire centralisé des erreurs. Exemple construit :
// app.js (exemple construit)
const express = require('express');
const morgan = require('morgan');
const cors = require('cors');
const app = express();
// Parser JSON avec limite raisonnable; exposer les erreurs de syntaxe en 400
app.use(express.json({ limit: '1mb' }));
app.use((err, req, res, next) => {
if (err && err.type === 'entity.parse.failed') {
return res.status(400).json({ error: 'Invalid JSON body' });
}
return next(err);
});
// Logs des requêtes
app.use(morgan('combined'));
// CORS: commencer étroit et explicite
app.use(cors({ origin: ['https://your-frontend.example.com'], methods: ['GET','POST','PUT','DELETE','OPTIONS'], credentials: true }));
// Endpoints de santé et d'info
app.get('/healthz', (req, res) => res.status(200).json({ status: 'ok' }));
app.get('/info', (req, res) => res.json({ name: 'example-api', version: process.env.BUILD_SHA || 'dev' }));
// Route d'exemple
app.get('/api/v1/ping', (req, res) => res.json({ pong: true }));
// Gestionnaire d'erreurs centralisé
app.use((err, req, res, next) => {
console.error('Unhandled error', { message: err.message, stack: err.stack });
if (res.headersSent) return next(err);
res.status(500).json({ error: 'Internal Server Error' });
});
module.exports = app;
- Entrée serveur claire avec timeouts
Réglez des timeouts au niveau serveur pour éviter les blocages et logguez le démarrage.
// server.js (exemple construit)
const http = require('http');
const app = require('./app');
const PORT = Number(process.env.PORT || 3000);
const server = http.createServer(app);
// Éviter des requêtes pendantes indéfiniment
server.requestTimeout = 60_000; // 60s
server.headersTimeout = 65_000; // 65s
server.listen(PORT, () => {
console.log(`Server listening on port ${PORT} env=${process.env.NODE_ENV || 'development'}`);
});
- Gestionnaires de processus
Ne masquez pas les problèmes : logguez et quittez sur signaux fatals pour permettre au process manager de redémarrer.
// index.js (exemple construit)
require('dotenv').config();
require('./server');
process.on('unhandledRejection', (reason) => {
console.error('Unhandled Rejection', reason);
process.exit(1);
});
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception', err);
process.exit(1);
});
- Garder un CORS explicite
Évitez les wildcards pendant le dépannage. Si plusieurs origines sont nécessaires, listez-les explicitement.
- Endpoints de santé
Retournez un JSON simple et un 200 uniquement quand les dépendances cœur sont joignables. Sinon, 503 clair.
// health.js (exemple construit)
app.get('/healthz', async (req, res) => {
const checks = { db: 'down' };
try {
// Pseudo: ping de votre DB ici
// await db.admin().command({ ping: 1 });
checks.db = 'up';
} catch (e) {
return res.status(503).json({ status: 'degraded', checks });
}
res.json({ status: 'ok', checks });
});
Vérifications et diagnostics
Avec ces garde-fous, prouvez les basiques et collectez des signaux utiles pour la suite.
- Démarrer le service et confirmer le port
node index.js
# Attendu: Server listening on port 3000 env=development
- Contrôles élémentaires
- Santé :
curl -i http://localhost:3000/healthz
# Attendu: HTTP/1.1 200 OK et un petit JSON
- Info :
curl -i http://localhost:3000/info
# Attendu: 200 avec name et version
- Route d'exemple :
curl -i http://localhost:3000/api/v1/ping
# Attendu: 200 avec {"pong":true}
- Vérification du parsing JSON
- JSON valide :
curl -i -X POST http://localhost:3000/api/v1/echo \
-H 'Content-Type: application/json' \
-d '{"hello":"world"}'
# Attendu: 200 avec l'écho JSON (route d'exemple construite)
- JSON invalide (exemple construit) :
curl -i -X POST http://localhost:3000/api/v1/echo \
-H 'Content-Type: application/json' \
-d '{"hello":"world"' # accolade fermante manquante
# Attendu: 400 avec {"error":"Invalid JSON body"}
- Vérification CORS (en ligne de commande)
curl -I http://localhost:3000/api/v1/ping \
-H 'Origin: https://your-frontend.example.com'
# Attendu: Access-Control-Allow-Origin: https://your-frontend.example.com
- Ports et processus
En cas d'EADDRINUSE, identifiez le processus en conflit et choisissez d'arrêter ou de changer PORT.
lsof -i :3000 -P -n # macOS/Linux
netstat -ano | findstr :3000 # Windows
- Connectivité base de données
Exposez l'état via /healthz. Pour un contrôle manuel (exemple construit de type Mongo) :
# Vérification hypothétique, remplacez par votre outil DB
mongo --eval 'db.adminCommand({ ping: 1 })'
# Attendu: ok: 1
Modes de panne et récupération
| Symptôme ou log | Cause probable | Premiers contrôles | Correction sûre |
|---|---|---|---|
| EADDRINUSE :3000 au démarrage | Port déjà utilisé | lsof/netstat sur 3000 | Changer PORT ou arrêter le processus en conflit |
| ECONNREFUSED vers l'hôte DB | DB down ou hôte/port erronés | Ping hôte/port DB | Corriger config ou redémarrer DB; 503 dans healthz |
| Unexpected token in JSON | JSON client malformé | Corps et Content-Type | Intercepter l'erreur, renvoyer 400 |
| Cannot set headers after they are sent | Double envoi de réponse | Rechercher res.send/json/end multiples | Une seule réponse; ajouter des return |
| socket hang up / ETIMEDOUT | Timeout amont ou serveur | requestTimeout; latence dans logs | Ajuster timeouts; améliorer perfs |
| 404 sur route connue | Montée de route/prefixe faux | app.use('/api', router) vs requêtes | Aligner route et client |
| Erreur CORS navigateur | Origin non autorisée | En-tête Allow-Origin | Ajouter l'origine explicite |
Scénario A : Conflit de port (EADDRINUSE)
- Confirmer le conflit
lsof -i :3000 -P -n
- Contenir
- Si l'autre processus est à vous, arrêtez-le proprement.
- Sinon, ne le tuez pas à l'aveugle ; changez PORT et poursuivez le dépannage.
- Correction sûre
PORT=3001 node index.js
- Vérification
- http://localhost:3001/healthz retourne 200.
- Rollback
- Revenez au port d'origine quand le conflit disparaît.
Scénario B : Crashs sur promesses non gérées
Symptômes : sorties de processus, logs UnhandledPromiseRejectionWarning.
- Contenir
- Ajoutez un handler unhandledRejection qui loggue et quitte (vu plus haut).
- Cause racine
- Handlers async qui lèvent sans try/catch ni next(err).
// Avant
app.get('/api/v1/user', async (req, res) => {
const user = await loadUser(req.query.id); // throws -> crash
res.json(user);
});
// Après
app.get('/api/v1/user', async (req, res, next) => {
try {
const user = await loadUser(req.query.id);
res.json(user);
} catch (err) {
next(err);
}
});
- Vérification
- La requête en échec retourne 500 avec la réponse d'erreur centralisée, sans crash.
- Rollback
- Rétablir la dernière version saine si un changement récent a introduit le souci.
Scénario C : Échecs de parsing JSON
Symptômes : 500 et traces pour corps JSON invalides.
- Contenir
- Assurez-vous que express.json est chargé avant les routes et que le handler d'erreur de parsing est en place.
- Cause racine
- Clients envoyant du JSON mal formé ou mauvais Content-Type.
- Correction sûre
- Renvoyer 400 avec un court message. Optionnel : resserrer le Content-Type.
app.use((req, res, next) => {
if (['POST','PUT','PATCH'].includes(req.method)) {
if (!req.is('application/json')) {
return res.status(415).json({ error: 'Expected application/json' });
}
}
next();
});
- Vérification
- Envoyez des requêtes valides et invalides : 200 pour valides, 400/415 pour invalides.
- Rollback
- Si des clients dépendent d'un comportement plus souple, retirez temporairement le contrôle Content-Type mais conservez la gestion d'erreur de parsing.
Scénario D : Panne ou mauvaise configuration de la base
Symptômes : 500 sur routes dépendantes; /healthz dégradé.
- Contenir
- Court-circuitez les requêtes quand la DB est down pour éviter les longs timeouts. Renvoyez 503 avec Retry-After.
let dbIsUp = false;
setInterval(async () => {
try {
// await db.admin().command({ ping: 1 });
dbIsUp = true;
} catch {
dbIsUp = false;
}
}, 5000);
app.use((req, res, next) => {
if (!dbIsUp && req.path.startsWith('/api/v1/orders')) {
res.set('Retry-After', '30');
return res.status(503).json({ error: 'DB unavailable' });
}
next();
});
- Cause racine
- Hôte/port erronés, identifiants modifiés, ACL réseau, crash du process DB.
- Correction sûre
- Corrigez la chaîne de connexion et les identifiants. Ajoutez un connectTimeout et une politique de retry limitée pour échouer vite.
// Pseudo lors de la création du client DB
const client = new DBClient({ connectTimeoutMS: 5000 });
- Vérification
- /healthz passe de 503 à 200 peu après le retour de la DB. Les routes métier retrouvent une latence normale.
- Rollback
- Si une mise à jour de driver ou de schéma est en cause, revenez à la dernière version saine.
Scénario E : Hausse mémoire ou CPU
Symptômes : latence en hausse, redémarrages, OOM.
- Contenir
- Réduire le trafic, ou temporairement scaler horizontalement (plusieurs instances derrière un équilibreur). À défaut, redémarrer en fenêtre de maintenance pour restaurer le service.
- Cause racine
- Fuites : tableaux globaux, setInterval sans clearInterval, listeners ajoutés en boucle.
// Fuite: empile dans un tableau global à chaque requête
const cache = [];
app.get('/api/v1/items', (req, res) => {
cache.push(Date.now());
res.json({ ok: true });
});
// Correctif: cache borné
const cache2 = [];
app.get('/api/v1/items', (req, res) => {
if (cache2.length > 1000) cache2.shift();
cache2.push(Date.now());
res.json({ ok: true });
});
- Diagnostics sûrs
- Logguez la mémoire à intervalle régulier pendant l'enquête :
setInterval(() => {
const m = process.memoryUsage();
console.log(`rss=${m.rss} heapUsed=${m.heapUsed}`);
}, 60_000);
- Vérification
- Après correctif, heapUsed se stabilise sous charge stable.
- Rollback
- Restaurez la fonctionnalité suspecte ; confirmez le retour aux valeurs de base.
Liste d'exploitation (Checklist)
| Tâche | Quand | Vérification |
|---|---|---|
| Relever versions Node, npm, Express | Début ou incident | node -v, npm -v, npm list express |
| Confirmer l'écoute du port | Au démarrage/EADDRINUSE | lsof/netstat montre un seul listener |
| Vérifier /healthz et /info | Après chaque déploiement/fix | /healthz en 200 si dépendances OK |
| Tester erreurs JSON | Après changement parser | JSON invalide -> 400, pas 500 |
| Valider CORS | Changement d'origine front | En-tête Access-Control-Allow-Origin correct |
| Observer les timeouts | Ajout de logique lente | Requêtes longues finissent dans requestTimeout |
| Tester connectivité DB | Avant routes DB-intensives | /healthz rapporte db: up |
| Surveiller mémoire/CPU | En enquête | rss et heapUsed stables dans le temps |
Résultats attendus et « bon » comportement
- Démarrage avec une ligne claire (port + environnement), stabilité sous faible charge.
- /healthz renvoie 200 seulement si les dépendances sont disponibles ; sinon 503 explicite.
- Corps JSON invalides : 400 avec message court ; corps valides : succès.
- Réponses CORS incluent l'origine exacte.
- Timeouts finis, visibles dans les logs, alignés sur vos limites amont.
Workflow de vérification et de reprise
- Identifier
- Capturez le message d'erreur exact et l'horodatage dans les logs.
- Notez la route, la méthode, et tout ID de corrélation.
- Contenir
- Préférez des bascules de configuration et clauses garde (503) aux réécritures pendant l'enquête.
- Si une route isole le problème, cachez-la derrière un feature flag ou un garde de route.
- Diagnostiquer
- Reproduisez localement avec curl (même méthode, en-têtes, corps).
- Comparez les environnements (NODE_ENV, PORT) pour révéler les écarts.
- Corriger
- Appliquez le plus petit changement qui traite la cause racine (try/catch, ajustement CORS, hôte DB correct, timeouts).
- Vérifier
- Confirmez un cas de succès et un cas d'échec représentatif.
- Surveillez les logs 5-10 minutes pour écarter les rechutes.
- Revenir en arrière si nécessaire
- Si l'erreur s'aggrave, revenez au dernier fichier ou réglage connu comme sain.
Conclusion
Un dépannage Express robuste commence par la visibilité : connaître vos versions, ports et variables d'environnement, puis ajouter une journalisation minimale et une gestion d'erreurs fiable. Vérifiez d'abord les routes basiques, le parsing JSON, CORS et les checks de santé avant d'engager des changements plus profonds. Les pannes courantes (conflits de port, rejets non gérés, erreurs de parsing, indisponibilité de la base) ont toutes des premiers gestes sûrs et des signaux de validation nets. Démarrez étroit, mesurez, élargissez prudemment, et servez-vous de la checklist pour garder chaque incident focalisé et reproductible. Avec ces pratiques, vous passerez moins de temps à deviner et plus à livrer une API stable.