Web API Fetch et bonnes pratiques

Découvrez les meilleures pratiques pour utiliser l'API Fetch en JavaScript afin d'effectuer des requêtes HTTP de manière efficace et sécurisée, tout en gérant correctement les erreurs et les réponses.

Web API Fetch et bonnes pratiques
Photo by Eric Ward / Unsplash

Si vous avez déjà utilisé l'API Web Fetch, vous savez que cette API Web est très pratique lorsque nous souhaitons exécuter des requêtes HTTP de manière asynchrone afin de récupérer des données depuis un serveur et les traiter dynamiquement dans notre document HTML.

Néanmoins, cette API est tellement facile à utiliser qu'on en oublie très vite les bonnes pratiques de code et c'est pourquoi je vous propose une liste non exhaustive des bonnes pratiques à adopter lorsque l'on utilise cette API Web.

Code d'exemple

Ce point peut paraître évident, mais nous pouvons très vite tomber dans la facilité lors de l'écriture de notre code JavaScript en omettant la gestion des erreurs, sous prétexte que notre serveur est démarré localement, qu'il nous répond sans erreurs, etc.

On peut alors être tenté d'écrire ce genre de code.

fetch("https://ipapi.co/json").then(function(response) {
  return response.json();
}).then(json => {
  console.log(json.ip);
});

Ici, nous exécutons une requête asynchrone à l'aide de l'API Web Fetch vers un serveur nous permettant de récupérer des informations sur la connexion active d'un client.

Lorsque le serveur nous répond, nous récupérons alors la réponse au format JSON, puis nous affichons la réponse au format JSON du serveur.

Ce code n'a pas l'air très choquant, car c'est un code que nous retrouvons assez souvent en exemple, néanmoins il peut se passer beaucoup de choses et notre objectif va être, au cours des différents chapitres, de voir comment rendre ce morceau de code plus robuste.

Code de statut HTTP

L'API Web Fetch nous permet d'exécuter des requêtes HTTP. C'est sur ce protocole que repose l'une des erreurs que nous avons faites, d'assumer que la réponse du serveur est toujours bonne, c'est-à-dire qu'elle a toujours un statut entre 200 et 299.

C'est ce que fait la propriété response.status qui nous permet de nous assurer que la réponse du serveur est correcte.

fetch("https://ipapi.co/json").then(function(response) {
  if (response.ok) {
    return response.json();
  }

  return Promise.reject(new Error(`Bad response: ${response.status}`));
}).then(json => {
  console.log(json.ip);
});

Ici, nous avons rajouté une condition, nous permettant de nous assurer que la réponse du serveur est correcte avant de pouvoir retourner les données au format JSON.

C'est important, surtout si le serveur que nous contactons respecte, lui, le protocole HTTP, et donc nous renvoie des codes de statut HTTP correspondant aux différents événements qui peuvent arriver lors d'une requête (la route est introuvable, le serveur n'a pas réussi à contacter la base de données, etc.).

Nous avons utilisé ici Promise.reject, mais nous aurions tout à fait pu utiliser aussi throw afin de renvoyer une erreur. Il n'y a aucune différence et c'est essentiellement une histoire de goût.

Gestion des erreurs

L'API Web Fetch peut potentiellement renvoyer des erreurs.

Par exemple, si nous ne passons pas une chaîne de caractères pour l'URL, si l'URL n'est pas accessible ou ne résout aucune adresse IP connue par un serveur DNS, si le navigateur est coupé d'internet, etc.

C'est autant d'erreur qui peuvent nous empêcher de récupérer notre réponse correctement, et que nous devons gérer.

En plus de ces erreurs, il faut aussi gérer les autres erreurs que nous avons générées plus haut avec la méthode Promise.reject.

fetch("https://ipapi.co/json").then(function(response) {
  if (response.ok) {
    return response.json();
  }

  return Promise.reject(new Error(`Bad response: ${response.status}`));
}).then(json => {
  console.log(json.ip);
}).catch(error => {
  console.error(error.message);
});

Ici, nous avons simplement rajouté un appel à la méthode catch sur la promesse qui est retournée par le deuxième appel à la méthode then.

Nous pourrions nous arrêter ici, mais en réalité une autre erreur se glisse dans ce code. Avez-vous remarqué laquelle ?

L'objet error que nous recevons n'est pas forcément une instance de la classe Error. Il est tout à fait possible (malheureusement) de lever des exceptions autres que des objets de la classe Error en JavaScript. Modifions notre exemple pour illustrer cet argument.

fetch("https://ipapi.co/json").then(function(response) {
  if (response.ok) {
    return response.json();
  }

  throw `Bad response: ${response.status}`;
}).then(json => {
  console.log(json.ip);
}).catch(error => {
  console.error(error.message);
});

Ici, nous avons modifié le retour d'erreur pour utiliser plutôt le mot-clé throw mais nous avons levé une chaîne de caractères.

Si nous continuons notre code tel-quel, nous remarquerons que nous aurons la valeur undefined puisqu'il n'existe pas de propriété message sur l'objet String en cas d'erreur.

C'est aussi une erreur qu'il faut gérer, et pour cela, nous allons vérifier ce que nous récupérons dans l'objet error.

fetch("https://ipapi.co/json").then(function(response) {
  if (response.ok) {
    return response.json();
  }

  return Promise.reject(new Error(`Bad response: ${response.status}`));
}).then(json => {
  console.log(json.ip);
}).catch(error => {
  if (error instanceof Error) {
    console.error(error.message);
  } else {
    console.error(String(error));
  }
});

Désormais, notre méthode catch gère correctement l'erreur levée précédemment, quel que soit son type. Il est d'ailleurs possible de lever n'importe quel type JavaScript, y compris des nombres, bien que cela puisse paraître contre-intuitif. C'est pour cela qu'il est important d'ajouter cette vérification afin de s'assurer de gérer le plus de cas possible dans notre code.

Validation de données

Dans le code précédent, en cas de succès lors de la récupération des données, nous partons du principe que nous récupérons toujours un objet (première erreur), et que dans cet objet, nous aurons toujours une propriété ip (deuxième erreur).

Penser de cette façon peut nous amener à des erreurs, car il n'est pas impossible que le serveur nous renvoie un objet qui ressemble à ceci aujourd'hui.

{
  "ip": "1.2.3.4"
}

Et, après une mise-à-jour de l'API, que nous recevons désormais des données dans ce format-là.

{
  "result": {
    "ip": "1.2.3.4"
  }
}

Le souci ici est que notre code, qui fonctionnait pendant un certain temps, risque de ne plus fonctionner après une mise-à-jour, et si nous ne consultons pas régulièrement les changements liés à cette API, nous risquons d'avoir des erreurs qui dégraderont l'expérience utilisateur.

Pour pouvoir résoudre ce problème, il est nécessaire de faire un travail préliminaire de validation des données.

fetch("https://ipapi.co/json").then(function(response) {
  if (response.ok) {
    return response.json();
  }

  return Promise.reject(new Error(`Bad response: ${response.status}`));
}).then(json => {
  if (typeof json !== "object") {
    return Promise.reject(new Error("Not an object"));
  }

  if (json === null) {
    return Promise.reject(new Error("Null provided"));
  }

  if (!("ip") in json) {
    return Promise.reject(new Error("No property ip"));
  }

  if (typeof json.ip !== "string") {
    return Promise.reject(new Error("ip property type mismatch"));
  }

  console.log(json.ip);
}).catch(error => {
  if (error instanceof Error) {
    console.error(error.message);
  } else {
    console.error(String(error));
  }
});

Ici, nous avons rajouté des vérifications pour s'assurer dans un premier temps que l'objet JSON est bel et bien un objet.

Puis, nous nous sommes assurés qu'il n'est pas null. Malheureusement, en JavaScript, due à une erreur qui traîne depuis les débuts du langage, null est un objet dans la définition du langage, donc nous devons vérifier que nous n'arrivons pas dans ce cas.

Enfin, nous pouvons vérifier que nous avons, dans un premier temps, la propriété ip présente dans l'objet json, car encore une fois rien n'est garanti, surtout pour une API qui ne nous appartient pas, et enfin, on peut vérifier par mesure de sécurité que cette propriété est bien une chaîne de caractères, dans le cas où nous souhaitons par exemple appeler des méthodes du prototype String sur cette propriété.

Conditions réseau dégradées

Bien que cela ne soit pas directement notre travail en tant que développeur, il est quand même important de bien penser à gérer les moments où notre utilisateur pourrait consulter notre application Web dans des conditions de réseau dégradées, par exemple dans le métro dans lequel la connectivité n'est pas idéal pour bénéficier d'une expérience utilisateur optimale.

Imaginons que nous nous rendons sur une page d'accueil où nous chargeons des articles. Si nous sommes dans le métro, il y a de fortes chances que cela mettent un certain temps. Si pendant ce temps l'utilisateur s'impatiente et se rend sur une page de produit, une page de profil, de nouveau sur une page de produit, et de nouveau sur une page d'articles, il risque d'y avoir non seulement plusieurs requêtes envoyées en même temps, du fait de la nature asynchrone des API Web utilisées, mais aussi une surcharge inutile sur le réseau puisque sur la page des articles, l'utilisateur n'a plus besoin des produits.

Pour éviter de surcharger inutilement la connexion, nous pouvons ajouter un contrôleur nous permettant, selon les besoins et les cas d'utilisation, annuler la requête HTTP afin de sauvegarder de précieuses ressources pour les requêtes suivantes.

Dans l'exemple qui suit, nous partons du principe que nous annulons la requête après un certain temps (10 secondes). Mais l'idéal serait d'exécuter ce code une fois qu'une page est changée par exemple (nous ne ferons pas cet exemple pour rester simple).

let abortController;

setTimeout(function() {
  if (abortController) {
    abortController.abort();
  }
}, 10000);

function sendRequest() {
  abortController = new AbortController();

  fetch("https://ipapi.co/json", {
    signal: abortController.signal
  }).then(function(response) {
    if (response.ok) {
      return response.json();
    }

    return Promise.reject(new Error(`Bad response: ${response.status}`));
  }).then(json => {
    if (typeof json !== "object") {
      return Promise.reject(new Error("Not an object"));
    }

    if (json === null) {
      return Promise.reject(new Error("Null provided"));
    }

    if (!("ip") in json) {
      return Promise.reject(new Error("No property ip"));
    }

    if (typeof json.ip !== "string") {
      return Promise.reject(new Error("ip property type mismatch"));
    }

    console.log(json.ip);
  }).catch(error => {
    if (error instanceof Error) {
      console.error(error.message);
    } else {
      console.error(String(error));
    }
  });
}

sendRequest();

Quelques explications s'imposent.

Tout d'abord, nous avons utilisé une fonction pour envoyer la requête. Cela nous permettrait, par exemple, si l'utilisateur revient sur la page, de rafraîchir les données.

Ensuite, nous avons utilisé un objet AbortController. Cet objet spécial nous permet, lorsqu'il est lié à une requête HTTP, de pouvoir annuler cette dernière. Attention, ici, on parle d'annuler le traitement à partir du moment où la requête est annulée.

Donc si la requête HTTP est en état pending nous ne continuerons pas plus loin. Mais il n'est pas possible que dans le même temps le serveur ait quand même eu le temps de nous renvoyer une réponse dans le flux HTTP qui est établie entre le client et le serveur, que nous ne traiterons jamais.

Nous pouvons donc attacher un objet AbortSignal qui vient de la propriété abortController.signal à notre requête HTTP via la propriété signal dans les options de l'API Web Fetch.

Nous avons enfin ajouté un intervalle, nous permettant d'appeler la méthode abortController.abort lorsque nous en avons envie, ici après 10 secondes pour l'exemple, mais il serait tout à fait possible d'appeler la méthode après un changement de page, ou lorsque l'utilisateur clique sur un bouton d'annulation par exemple.

Conclusion

Afin de nous assurer d'exécuter des requêtes HTTP et de gérer la plupart des cas d'erreurs, il est nécessaire :

  • Gérer les différents codes de statut du serveur
  • Gérer les erreurs à l'exécution de nos promesses
  • Valider nos données
  • S'assurer d'annuler nos requêtes au bon moment

En appliquant ces principes, il sera possible d'améliorer significativement l'expérience utilisateur sur nos applications Web.

Néanmoins, comme vous avez pu vous en douter, le code nécessaire pour pouvoir faire tout cela peut être long et pénible à écrire, raison principale pour laquelle on oublie très vite de s'en charger, c'est pour ça qu'il existe heureusement des librairies pour pouvoir le faire.

Notamment, si vous utilisez un framework JavaScript, vous pouvez utiliser Tanstack Query, qui est un module agnostique permettant de pouvoir exécuter des requêtes HTTP avec des aides à la programmation de ces différentes parties en simplifiant le code écrit, permettant de donner une meilleure expérience aux développeurs qui souhaitent renforcer la fiabilité de leurs requêtes HTTP.

Références