frontend / javascript
Fetch et API réseau
Explication
Ce que vous allez apprendre
- Effectuer une requête GET et POST avec
fetch() - Expliquer pourquoi
fetch()ne rejette pas sur une erreur HTTP (404, 500) - Vérifier systématiquement
response.okavant de traiter une réponse comme valide - Annuler une requête en vol avec
AbortController - Paralléliser plusieurs requêtes indépendantes avec
Promise.all
Dans quel contexte ?
Une application affiche "Utilisateur : undefined undefined" au lieu d'un message d'erreur clair quand l'identifiant demandé n'existe pas côté serveur. Le code fait const data = await (await fetch(\/api/users/${id}`)).json();sans jamais vérifier le statut HTTP : le serveur répond 404 avec un corps JSON{ "error": "not found" }, que le code interprète comme si c'était un utilisateur valide. Ce bug très courant vient d'un malentendu sur fetch()` : une réponse 404 n'est pas un rejet de Promise, contrairement à ce qu'on pourrait intuitivement attendre.
1. Récupérer des données depuis un serveur
Une application web moderne n'affiche presque jamais des données figées : elle les récupère depuis un serveur distant, au chargement de la page ou en réaction à une action de l'utilisateur. fetch() est l'API standard pour ça, et elle repose entièrement sur les Promises vues dans les leçons précédentes.
Prérequis
Cette leçon suppose une bonne maîtrise des Promises et de async/await (leçons 11 et 12) : fetch() retourne une Promise, et tous les exemples de cette leçon s'appuient sur ces deux mécanismes.
2. Le piège numéro un à connaître absolument
fetch() ne considère PAS un statut d'erreur HTTP (404, 500...) comme un échec. La Promise se résout normalement même si le serveur répond "Not Found" — seule une vraie erreur réseau (pas de connexion, DNS injoignable) provoque un rejet.
| Situation | fetch() rejette-t-il la Promise ? | response.ok |
|---|---|---|
| Réponse 200 | Non | true |
| Réponse 404 ou 500 | Non | false |
| Pas de connexion réseau / DNS injoignable | Oui | Non applicable |
3. La vérification à ne jamais oublier
Il faut donc systématiquement vérifier la propriété response.ok soi-même avant de considérer la requête comme réussie, sous peine de traiter une page d'erreur comme si elle contenait des données valides.
Piège fréquent
if (!response.ok) throw new Error(\HTTP ${response.status}`);doit être ajouté juste après chaquefetch()`, avant de lire le corps de la réponse. Sans cette ligne, un 404 ou un 500 sera silencieusement traité comme un succès.
4. Annuler une requête devenue inutile
Une requête peut aussi rester bloquée trop longtemps, ou devenir obsolète avant même d'avoir abouti — par exemple quand l'utilisateur retape dans un champ de recherche avant que la requête précédente soit revenue. AbortController permet de l'annuler en vol.
5. Un dernier réflexe de performance
Lancer plusieurs requêtes indépendantes séquentiellement avec await gaspille du temps. Promise.all permet de les lancer toutes en même temps et d'attendre leur résultat combiné — le même réflexe déjà vu dans la leçon sur async/await.
Commandes & code
Fetch et API réseau
Communiquer avec un serveur depuis le navigateur ou Node.js.
// --- GET basique ---
async function obtenirUtilisateurs() {
const reponse = await fetch("https://api.example.com/utilisateurs");
if (!reponse.ok) {
// fetch NE REJETTE PAS sur un statut 4xx/5xx -- il faut verifier .ok manuellement !
throw new Error(`Erreur HTTP ${reponse.status} : ${reponse.statusText}`);
}
return reponse.json(); // parse le corps en JSON (retourne une Promise)
}
// --- POST avec corps JSON ---
async function creerUtilisateur(donnees) {
const reponse = await fetch("https://api.example.com/utilisateurs", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${localStorage.getItem("token")}`,
},
body: JSON.stringify(donnees),
});
if (!reponse.ok) throw new Error(`Erreur ${reponse.status}`);
return reponse.json();
}
// --- Gestion des erreurs reseau (vs erreurs HTTP) ---
async function requeteRobuste(url) {
try {
const reponse = await fetch(url);
if (!reponse.ok) {
throw new Error(`Erreur serveur : ${reponse.status}`);
}
return await reponse.json();
} catch (erreur) {
if (erreur instanceof TypeError) {
// fetch rejette avec TypeError pour les erreurs RESEAU (offline, CORS, DNS...)
console.error("Probleme reseau :", erreur.message);
} else {
console.error("Erreur applicative :", erreur.message);
}
throw erreur;
}
}
// --- AbortController : annuler une requete (timeout, changement de page, nouvelle recherche) ---
async function requeteAvecTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const reponse = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
return await reponse.json();
} catch (erreur) {
if (erreur.name === "AbortError") {
throw new Error("Requete annulee : timeout depasse");
}
throw erreur;
}
}
// Annuler une recherche precedente quand l'utilisateur retape (pattern autocomplete)
let controllerActuel = null;
async function rechercher(terme) {
if (controllerActuel) controllerActuel.abort(); // annule la recherche precedente
controllerActuel = new AbortController();
const reponse = await fetch(`/api/recherche?q=${encodeURIComponent(terme)}`, {
signal: controllerActuel.signal,
});
return reponse.json();
}
// --- Requetes en parallele avec Promise.all ---
async function chargerTableauDeBord() {
const [utilisateurs, commandes, produits] = await Promise.all([
fetch("/api/utilisateurs").then((r) => r.json()),
fetch("/api/commandes").then((r) => r.json()),
fetch("/api/produits").then((r) => r.json()),
]);
return { utilisateurs, commandes, produits };
}
// --- Retry avec backoff exponentiel ---
async function fetchAvecRetry(url, tentatives = 3) {
for (let i = 0; i < tentatives; i++) {
try {
const reponse = await fetch(url);
if (reponse.ok) return reponse.json();
} catch (erreur) {
if (i === tentatives - 1) throw erreur;
}
await new Promise((r) => setTimeout(r, 2 ** i * 500)); // 500ms, 1000ms, 2000ms...
}
}
// --- Autres formats de reponse ---
async function telechargerFichier(url) {
const reponse = await fetch(url);
const blob = await reponse.blob(); // fichier binaire (image, PDF...)
const texte = await fetch(url).then((r) => r.text()); // texte brut
const buffer = await fetch(url).then((r) => r.arrayBuffer()); // donnees binaires brutes
}
// --- Streaming de reponse (gros volumes) ---
async function lireEnStreaming(url) {
const reponse = await fetch(url);
const reader = reponse.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(decoder.decode(value)); // traite le chunk au fur et a mesure
}
}Résumé
fetch()ne rejette JAMAIS sur un statut HTTP d'erreur : toujours vérifierresponse.okmanuellement.TypeErrorsignale une erreur réseau (offline, CORS) ; le reste est une erreur applicative à gérer via.status.AbortControllerannule une requête en vol : essentiel pour les timeouts et l'autocomplete.- Le streaming (
response.body.getReader()) traite une réponse volumineuse sans tout charger en mémoire.
Exercices pratiques
Mission : la page qui affiche des utilisateurs même sur une erreur 500
Objectif : Corriger l'absence de vérification response.ok, puis implémenter une annulation de recherche avec AbortController.
Contexte
Sur le tableau de bord de Technologik, une erreur serveur 500 sur /api/utilisateurs affiche quand même une page vide sans message d'erreur visible, comme si tout s'était bien passé. Le code :
async function obtenirUtilisateurs() {
const reponse = await fetch("/api/utilisateurs");
return reponse.json();
}Un champ de recherche autocomplete pose aussi problème : quand l'utilisateur tape vite, plusieurs requêtes partent, et une ancienne réponse (plus lente) arrive parfois après une réponse plus récente, affichant des résultats obsolètes.