Timeout des appels HTTP : prévention du blocage des sockets et résilience réseau
Comprendre pourquoi l'absence de timeout sur les appels fetch ou axios paralyse les serveurs Node.js, et comment configurer des limites réseau et métier avec AbortController.
Sur cette page (11 sections)▼
Dans l'écosystème JavaScript moderne, fetch est devenu l'interface universelle pour interagir avec des services tiers, des microservices et des bases de données distantes. Cependant, une caractéristique de l'API standard fetch surprend de nombreux développeurs : la spécification ne fournit pas une option standard universelle timeout.
Les comportements réseau effectifs (délais d'expiration TCP, seuils de coupure socket ou garde-fous de proxy) dépendent du runtime (Node.js, Bun, navigateurs), du client HTTP sous-jacent et de l'infrastructure d'hébergement. Sans borne explicite, une connexion réseau bloquée ou un serveur distant défaillant peut maintenir des ressources ouvertes bien plus longtemps que souhaité.
L'utilisation d'AbortSignal ou d'AbortSignal.timeout() permet précisément d'imposer une borne applicative contrôlée lorsque ces mécanismes sont supportés.
Pourquoi borner l'attente est vital en environnement serveur
Sur un serveur applicatif (comme une route d'API Next.js ou un serveur Express), chaque requête entrante consomme des ressources système : un descripteur de socket réseau, un enregistrement dans la table de connexion TCP du système d'exploitation, et de la mémoire vive associée au contexte de la promesse JavaScript.
sequenceDiagram
participant Client as Navigateur Client
participant Server as Serveur Next.js
participant API as API Externe Défaillante
Client->>Server: Requête HTTP (Consommation de thread)
Server->>API: fetch() sans borne applicative
Note over API: Panne réseau / Blocage TCP
Note over Server: Socket mobilisé anormalement longtemps
Note over Client: En attente (Gateway Timeout 504)
Note over Server: Épuisement des descripteurs de fichiers
Server-->>Client: Dégradation ou indisponibilité du service
Lorsqu'un service externe subit une panne de connectivité ou un ralentissement sévère :
- Les requêtes
fetchinitiées sans borne applicative restent ouvertes et accumulent les contextes d'exécution. - Le pool de connexions HTTP du serveur local s'engorge progressivement.
- Les nouvelles requêtes de vos propres utilisateurs légitimes sont ralenties ou mises en file d'attente jusqu'à l'émission d'erreurs
504 Gateway Timeoutpar votre proxy inverse (Cloudflare, Vercel ou NGINX).
Timeout vs Retry : deux piliers complémentaires
Il est primordial de ne pas confondre Timeout et Retry :
- Le Timeout définit la durée maximale pendant laquelle votre application accepte d'attendre une réponse avant de considérer l'opération comme échouée et de libérer immédiatement les ressources allouées.
- Le Retry (voir Politiques de retry non borné) définit l'action corrective à mener une fois que l'échec (éventuellement causé par un timeout) est constaté.
Un retry sans timeout est totalement inopérant si la requête initiale reste suspendue pendant 15 minutes dans le vide.
L'implémentation moderne : AbortController et AbortSignal.timeout
Depuis Node.js 18+ et dans tous les runtimes modernes compatibles avec les standards Web, la gestion des timeouts s'effectue au moyen de l'objet AbortController ou de son raccourci moderne AbortSignal.timeout().
1. La méthode concise moderne : AbortSignal.timeout()
Idéale pour les appels simples sans logique d'annulation externe :
export async function fetchCatalog(category: string) {
try {
const response = await fetch(`https://api.fournisseur.com/items?cat=${category}`, {
// Interrompt automatiquement la connexion si aucun octet n'est reçu après 4000ms
signal: AbortSignal.timeout(4000),
});
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error instanceof Error && error.name === "TimeoutError") {
console.error("L'API fournisseur a dépassé le seuil de tolérance de 4s.");
// Déclencher un fallback gracieux ou retourner des données en cache
return getCachedCatalog(category);
}
throw error;
}
}
2. La méthode historique polyvalente : AbortController explicite
Nécessaire si vous devez combiner un timeout temporel avec une interruption manuelle (par exemple lors de l'arrêt gracieux du serveur) :
export async function queryExternalService(payload: unknown, timeoutMs = 5000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch("https://api.tiers.com/v1/process", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
signal: controller.signal,
});
return await res.json();
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
throw new Error(`La requête vers le service tiers a été interrompue après ${timeoutMs}ms`);
}
throw error;
} finally {
// Nettoyer impérativement le timer pour ne pas maintenir le process Node actif
clearTimeout(timer);
}
}
Timeout réseau vs Timeout métier
Une distinction technique essentielle doit guider votre architecture :
Le timeout réseau (Transport)
Il mesure le temps nécessaire pour établir la connexion TCP/TLS et commencer à recevoir les paquets de données. Il protège l'infrastructure contre les pannes d'infrastructure brute et les routes réseau coupées.
Le timeout métier (End-to-End)
Il englobe l'intégralité du traitement : acheminement, exécution d'une requête SQL complexe sur le serveur tiers, génération de la réponse et téléchargement de la charge utile.
- Un appel à un webhook de notification doit avoir un timeout court (ex. 2 à 3 secondes).
- Un appel de génération par un modèle d'IA ou une conversion de document PDF peut nécessiter un timeout de 30 à 60 secondes. Dans ce cas, l'appel doit impérativement être traité de manière asynchrone (via une file de messages ou un job en arrière-plan) plutôt que de bloquer une requête HTTP synchrone de l'utilisateur final.
Attention aux effets de bord critiques lors d'un timeout
Un timeout interrompt l'attente du client, mais n'interrompt pas nécessairement l'exécution du travail sur le serveur distant :
- Si vous envoyez une requête de virement bancaire ou de débit Stripe et que votre client subit un timeout après 5 secondes, le serveur distant a peut-être déjà débité la carte avant que le réseau ne coupe.
- Ne rejouez jamais un appel non idempotent ayant subi un timeout sans disposer d'une clé d'idempotence stricte (consultez le guide sur la sécurité des webhooks Stripe).
Ce que GVO observe lors de l'audit
L'analyse de code GVO vérifie systématiquement la configuration des appels réseau sortants dans les API routes, actions de serveurs et modules d'intégration :
GVO-REL-HTTP-TIMEOUT-V1: Détecte les invocations defetch,axios,gotou d'instances HTTP natives ne transmettant aucun paramètre de timeout ni signal d'interruption (AbortSignalousignal).- Le moteur signale le risque d'épuisement du pool de connexions et recommande l'injection systématique de
AbortSignal.timeout(ms)ajusté selon la criticité du flux.
Sources et références
- MDN Web Docs : Fetch API & AbortSignal: timeout() static method.
- Node.js Documentation : Globals - AbortSignal & Resource Management.
- Cloudflare Workers Docs : Subrequests limits, connection pooling, and cancellation.
- W3C Fetch Specification : Fetching with abort signals and controller semantics.
À poursuivre
Vérifier ce point sur votre projet
GVO confronte le code et les configurations de votre application aux règles de sécurité réelles.