Les Requêtes HTTP en Javascript

Présentation

En développement web, accéder à des ressources distantes, comme des bases de données ou des services web, passe souvent par des API (Interfaces de Programmation d'Applications). Une API est un ensemble de règles permettant à des applications ou systèmes de communiquer entre eux. Par exemple, une application peut utiliser une API pour afficher la météo d'une ville, récupérer les derniers articles d'un blog ou encore soumettre un formulaire à un serveur distant.

Les API peuvent être classées en deux grandes catégories :

  • API privées : Elles sont utilisées au sein d'une même application ou organisation et ne sont pas destinées à être consommées par des tiers. Par exemple, une API privée peut permettre à différentes parties d'une application (comme le front-end et le back-end) d'échanger des données de manière structurée. Un cas courant est l'envoi d'un formulaire vers le serveur sans recharger la page, améliorant ainsi l'expérience utilisateur. Bien qu'elles reposent sur HTTP comme toute API web, elles ne sont accessibles qu'aux composants autorisés de l'application ou du réseau.
  • API publiques : Elles sont exposées à des utilisateurs ou systèmes tiers, en dehors de l'organisation qui les fournit. Par exemple, une API publique peut permettre de récupérer des données météo pour les afficher sur une application, d'obtenir des informations sur des films pour enrichir un catalogue, ou encore de gérer des paiements en ligne via un service externe.

API REST

Dans le développement moderne, les API REST (Representational State Transfer) sont très répandues. REST est un style architectural qui repose sur plusieurs principes :

  • Les ressources (comme "météo", "articles" ou "utilisateurs") sont identifiées par des URLs claires et cohérentes.
  • Les méthodes HTTP permettent d'indiquer l'action souhaitée sur la ressource :
    • GET : Récupérer une ressource. Des paramètres peuvent être ajoutés à l'URL pour filtrer les résultats ou préciser la requête.
    • POST : Créer une nouvelle ressource.
    • PUT : Remplacer entièrement une ressource existante par une nouvelle version.
    • PATCH : Modifier partiellement une ressource existante.
    • DELETE : Supprimer une ressource existante.
  • Les réponses peuvent adopter plusieurs formats selon l'API. Elles sont toutefois très souvent structurées en JSON, un format léger et facile à manipuler en JavaScript.
  • REST suit un principe dit stateless, ce qui signifie que chaque requête doit contenir les informations nécessaires pour être comprise et traitée, sans exiger que le serveur se base sur un état applicatif mémorisé d'une requête précédente.
  • Les API REST sont conçues pour être simples, évolutives et interopérables (elles permettent à des applications écrites dans des technologies différentes de communiquer entre elles), facilitant leur utilisation par différents types d'applications (sites web, applications mobiles ou services tiers).

Par exemple, une API REST pourrait fournir les fonctionnalités suivantes :

  • GET https://api.exemple.com/articles : Récupérer tous les articles.
  • POST https://api.exemple.com/articles : Ajouter un nouvel article à un blog.
  • PUT https://api.exemple.com/articles/1 : Remplacer entièrement l'article ayant l'ID 1.
  • PATCH https://api.exemple.com/articles/1 : Modifier partiellement l'article ayant l'ID 1 (ex. uniquement son titre).
  • DELETE https://api.exemple.com/articles/1 : Supprimer l'article avec l'ID 1.

Pour interagir avec ces API en JavaScript dans le navigateur, on utilise généralement l'une des deux approches suivantes, à savoir XMLHttpRequest (XHR) et Fetch.

XMLHttpRequest

XMLHttpRequest est une interface introduite dans les premières implémentations d'AJAX (Asynchronous JavaScript and XML). Elle permet d'envoyer des requêtes HTTP et de recevoir des réponses sans recharger la page, rendant possibles les pages web dynamiques.

Malgré son importance historique, XHR présente plusieurs inconvénients :

  • Une syntaxe relativement verbeuse.
  • Une gestion basée sur des fonctions de rappel (callbacks), qui peut rendre le code difficile à lire dans des cas complexes.
  • Une manipulation parfois délicate des réponses et des erreurs.

Fetch

L'API Fetch est une interface moderne introduite dans les standards récents du web. Elle simplifie l'envoi de requêtes HTTP et la réception des réponses.

  • Une syntaxe plus concise et lisible, basée sur les Promesses.
  • Une intégration naturelle avec async / await, facilitant la gestion de l'asynchronisme.
  • Une manipulation plus claire des réponses, notamment pour les données JSON.

Fetch est aujourd'hui la méthode la plus couramment utilisée dans les applications web modernes pour interagir avec des API REST. Elle permet par exemple de récupérer des données, d'envoyer des formulaires ou d'actualiser une interface sans recharger la page.

Requêtes de Base

Il est possible d'effectuer des requêtes sans configuration spécifique. Dans ce cas, une configuration par défaut sera utilisée. Pour effectuer une requête de base avec fetch, seules trois étapes principales sont nécessaires.

  • L'URL : il s'agit de l'adresse du serveur ou de l'API où vous souhaitez envoyer la requête ou récupérer des données.
  • Le bloc .then() : il s'exécute si la requête réussit. Ce bloc vous permet de traiter la réponse, par exemple en la transformant en données exploitables (comme du JSON).
  • Le bloc .catch() : il s'exécute en cas d'échec de la requête (erreur réseau, problème d'URL, etc.), mais également lorsqu'une exception est levée dans l'un des blocs .then() précédents. Il permet de gérer les erreurs de manière centralisée.

Dans l'exemple suivant, une requête HTTP est envoyée à l'URL https://jsonplaceholder.typicode.com/users à l'aide de fetch. Comme aucune méthode spécifique n'est configurée, la méthode GET est utilisée par défaut pour récupérer des données. Si la requête réussit, le bloc .then() s'exécute, affichant dans la console l'objet response, qui contient des informations sur la réponse du serveur, telles que le statut HTTP, les en-têtes et le type de réponse. En cas d'erreur (par exemple, une URL incorrecte ou un problème réseau), le bloc .catch() s'exécute pour afficher un message d'erreur dans la console, permettant de diagnostiquer le problème.


                // Définition de l'URL de l'API d'où les données seront récupérées.
                const url = "https://jsonplaceholder.typicode.com/users";

                // Utilisation de fetch pour envoyer une requête HTTP "GET" à l'URL.
                fetch(url)
                    .then(response => 
                    {
                        // Le bloc ".then" s'exécute lorsque la requête a réussi.
                        // L'objet "response" contient des informations sur la réponse du serveur, notamment :
                        // - Le statut HTTP de la requête (ex. 200 pour "OK").
                        // - Un indicateur de succès ou d'échec ("true" si le statut est 200-299).
                        // - Les en-têtes HTTP renvoyés par le serveur.
                        // - Le type de réponse (ex. "cors", "basic").
                        // - L'URL à laquelle la requête a été envoyée.

                        // Affiche l'objet "response" dans la console pour en examiner les détails.
                        console.log(response);
                    })
                    .catch(error => 
                    {
                        // En cas d'échec de la requête (problème réseau, URL invalide, etc.),
                        // le bloc ".catch" s'exécute pour gérer l'erreur.
                        // Affiche un message d'erreur dans la console pour le diagnostic.
                        console.error("Erreur lors de la requête :", error.message);
                    });

                    /*
                        Affiche :
                            Response { type: "cors", url: "https://jsonplaceholder.typicode.com/users", redirected: false, status: 200, ok: true, statusText: "", headers: Headers(4), body: ReadableStream, bodyUsed: false }
                                body: ReadableStream { locked: false }
                                bodyUsed: false
                                headers: Headers(4) { "cache-control" → "max-age=43200", "content-type" → "application/json; charset=utf-8", expires → "-1", … }
                                ok: true
                                redirected: false
                                status: 200
                                statusText: ""
                                type: "cors"
                                url: "https://jsonplaceholder.typicode.com/users"
                    */
                

La réponse

Lorsqu'une requête avec fetch aboutit, elle retourne un objet Response, qui est passé en paramètre au bloc .then() lors de la résolution de la promesse. Cet objet représente la réponse du serveur et contient des informations sur le résultat de la requête. Il fournit des métadonnées (comme le statut HTTP et les en-têtes) ainsi que le corps de la réponse, qui peut être lu et interprété avec des méthodes spécifiques. Voici quelques-unes de ses propriétés principales :

  • body : Un flux lisible (ReadableStream) représentant le corps de la réponse. Un flux lisible est un flux de données qui arrive progressivement. Pour accéder aux données, utilisez des méthodes comme text(), json(), ou blob().
  • headers : Un objet Headers contenant les en-têtes HTTP associés à la réponse.
  • ok : Un booléen qui indique si le statut HTTP de la réponse est compris entre 200 et 299, ce qui correspond à une requête réussie. Il est important de noter que fetch ne rejette la promesse qu'en cas d'erreur réseau (DNS introuvable, connexion refusée, blocage CORS, etc.). Un code HTTP comme 404 ou 500 n'entraîne pas de rejet, la promesse est résolue normalement, mais avec ok à false. C'est pourquoi il est essentiel de vérifier cette propriété pour distinguer une réponse valide d'une réponse erronée.
  • status : Le code de statut HTTP de la réponse (par exemple, 200 pour une réussite ou 404 pour une ressource non trouvée).
  • statusText : Le message descriptif associé au code de statut HTTP (par exemple, "OK" pour le code 200). Notez cependant qu'en HTTP/2 et HTTP/3, cette propriété est toujours une chaîne vide, car ces versions du protocole ne transmettent plus de reason phrase. Il est donc préférable de se baser sur status plutôt que sur statusText pour le diagnostic.
  • url : L'URL qui a été utilisée pour générer la réponse.
  • ...

Lire le corps de la réponse

L'objet Response ne contient pas directement les données demandées. Celles-ci se trouvent dans le corps de la réponse (body), sous forme de flux lisible. Pour les extraire, il faut utiliser l'une des méthodes de lecture fournies par l'objet Response. La plus courante est .json(), qui interprète le corps comme du JSON et retourne une promesse contenant les données exploitables. D'autres méthodes existent, comme .text() pour du texte brut ou .blob() pour des données binaires (images, fichiers, etc.).

Dans l'exemple suivant, on récupère la liste des utilisateurs depuis l'API, puis on extrait les données JSON du corps de la réponse. La méthode .json() retournant elle-même une promesse, un second bloc .then() est nécessaire pour accéder aux données :


                // Définition de l'URL de l'API d'où les données seront récupérées.
                const url = "https://jsonplaceholder.typicode.com/users";

                // Utilisation de fetch pour envoyer une requête HTTP "GET" à l'URL.
                fetch(url)
                    .then(response => 
                    {
                        // Vérifie si la réponse est considérée comme "ok" (statut HTTP entre 200 et 299).
                        // Rappel : fetch ne rejette la promesse qu'en cas d'erreur réseau.
                        // Un code 404 ou 500 résout la promesse normalement, mais avec "ok" à "false".
                        if (!response.ok) 
                        {
                            // Lance une exception pour signaler l'erreur.
                            // Celle-ci sera interceptée par le bloc ".catch" plus bas.
                            throw new Error(`Erreur HTTP : ${response.status}`);
                        }

                        // Interprète le corps de la réponse comme du JSON.
                        // La méthode ".json()" retourne une promesse, c'est pourquoi
                        // un second bloc ".then()" est nécessaire pour accéder aux données.
                        return response.json();
                    })
                    .then(data => 
                    {
                        // "data" contient désormais les données JSON exploitables.
                        // Ici, c'est un tableau d'objets représentant des utilisateurs.
                        console.log(data);
                    })
                    .catch(error => 
                    {
                        // Ce bloc intercepte :
                        // - Les erreurs réseau (rejet de la promesse fetch).
                        // - Les exceptions levées dans les blocs ".then()" (ex. le "throw" ci-dessus).
                        console.error("Erreur lors de la requête :", error.message);
                    });

                    /*
                        Affiche :
                            (10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
                                0: { id: 1, name: "Leanne Graham", username: "Bret", email: "Sincere@april.biz", ... }
                                1: { id: 2, name: "Ervin Howell", username: "Antonette", email: "Shanna@melissa.tv", ... }
                                ...
                    */
                

Passer des paramètres d'URL

Comme pour toute requête réalisée avec la méthode GET, il est possible de transmettre des informations via les paramètres d'URL. Ces paramètres sont ajoutés à la fin de l'URL après un ?, qui sert à séparer le chemin de la ressource des paramètres. Les paramètres eux-mêmes prennent la forme de paires clé-valeur, séparées par un = et reliées entre eux par un &. Cela est particulièrement utile pour filtrer ou personnaliser les données retournées par l'API sans avoir à utiliser une méthode plus complexe comme POST.

Dans l'exemple suivant, on souhaite récupérer les informations des utilisateurs ayant pour ID les valeurs 1 et 5 en ajoutant ?id=1&id=5 à l'URL. Notez que la possibilité de répéter un même paramètre (id apparaît deux fois) dépend de l'implémentation côté serveur (toutes les API ne gèrent pas cette syntaxe de la même manière).


                // Définition de l'URL de l'API contenant les données à récupérer.
                // Cette URL inclut des paramètres d'URL (id=1&id=5), qui permettent au serveur
                // de filtrer les résultats et de ne retourner que les utilisateurs avec l'ID 1 ou 5.
                // Note : la répétition d'un même paramètre est supportée par cette API,
                // mais ce comportement n'est pas universel.
                const url = "https://jsonplaceholder.typicode.com/users?id=1&id=5";

                // Utilisation de fetch pour envoyer une requête HTTP "GET" à l'URL.
                fetch(url)
                    .then(response => 
                    {
                        if (!response.ok) 
                        {
                            throw new Error(`Erreur HTTP : ${response.status}`);
                        }
                        return response.json();
                    })
                    .then(data => 
                    {
                        console.log(data);
                    })
                    .catch(error => 
                    {
                        console.error("Erreur lors de la requête :", error.message);
                    });

                    /*
                        Affiche :
                            (2) [{…}, {…}]
                                0: { id: 1, name: "Leanne Graham", username: "Bret", ... }
                                1: { id: 5, name: "Chelsey Dietrich", username: "Kamren", ... }
                    */
                

Une pratique courante consiste à utiliser les paramètres d'URL pour transmettre des clés d'API ou des critères de recherche. Ces clés permettent au fournisseur d'authentifier les requêtes et de gérer l'accès aux données (quotas, niveaux d'accès, etc.). Elles s'obtiennent généralement en créant un compte sur le site du fournisseur de l'API.

Voici un exemple concret utilisant une API météo qui nécessite une clé d'API, ainsi que des critères comme la ville et les unités de mesure :


                // Exemple : Utilisation de paramètres d'URL pour interroger une API météo
                const cleApi = "votre_cle_api";
                const ville = "Brussels";
                const unite = "metric"; // 'metric' pour Celsius, 'imperial' pour Fahrenheit

                // Construction de l'URL avec les paramètres
                const url = `https://api.openweathermap.org/data/2.5/weather?appid=${cleApi}&q=${ville}&units=${unite}`;

                // Utilisation de fetch pour envoyer une requête HTTP "GET" à l'URL avec les paramètres d'URL...
                

Dans cet exemple, l'URL transmet la clé d'API appid, le nom de la ville q, et les unités units. Cette approche est courante dans les services en ligne, car elle rend les requêtes simples et faciles à lire.

Notez que cette méthode d'intégration des clés dans l'URL convient principalement aux clés d'API publiques ou non critiques. Pour les clés sensibles ou les données privées, une approche plus sécurisée consiste à utiliser un jeton Bearer transmis dans les en-têtes HTTP. Nous reviendrons sur cette méthode dans la section dédiée aux en-têtes.

Exercices - Partie 01

Utilisation du protocole HTTPS

Pour tous les exercices de ce chapitre et afin d'obtenir une réponse du serveur, assurez-vous d'utiliser le protocole HTTPS pour vos requêtes.

Activation de HTTPS sous Laragon (Windows)

Avec Laragon, l'activation du SSL est simple :

  1. Cliquez sur la roue crantée.
  2. Accédez à Services & Ports.
  3. Cochez la case SSL sur la ligne correspondant à Apache.

Activation de HTTPS sous MAMP (macOS) avec mkcert

Pour les utilisateurs de Mac utilisant la version gratuite de MAMP, voici une méthode moderne pour activer HTTPS en utilisant mkcert :

  1. Installer et configurer mkcert
    • Ouvrez le Terminal et installez mkcert via Homebrew (si ce n'est pas déjà fait) :
      
                                          brew install mkcert
                                          mkcert -install
                                      
  2. Générer un certificat SSL avec mkcert
    • Créez un dossier dédié aux certificats SSL dans MAMP et naviguez-y :
      
                                          mkdir -p /Applications/MAMP/conf/ssl
                                          cd /Applications/MAMP/conf/ssl
                                      
    • Générez un certificat pour localhost :
      
                                          mkcert localhost
                                      
    • Les fichiers générés seront localhost.pem (certificat) et localhost-key.pem (clé privée). Pour simplifier la configuration, renommez-les :
      
                                          mv localhost.pem localhost.crt
                                          mv localhost-key.pem localhost.key
                                      
  3. Activer le module SSL dans Apache
    • Ouvrez le fichier de configuration d'Apache de MAMP :
      
                                          sudo nano /Applications/MAMP/conf/apache/httpd.conf
                                      
    • Dans l'éditeur, recherchez les lignes suivantes et décommentez-les (supprimez les # en début de ligne) ou créez les si elles n'existent pas :
      
                                          LoadModule ssl_module modules/mod_ssl.so
                                      
      
                                          Include conf/extra/httpd-ssl.conf
                                      
  4. Configurer le Virtual Host SSL
    • Ouvrez le fichier de configuration des hôtes virtuels SSL :
      
                                          sudo nano /Applications/MAMP/conf/apache/extra/httpd-ssl.conf
                                      
    • Recherchez et modifiez ces lignes pour pointer vers vos certificats générés :
      
                                          <VirtualHost _default_:443>
                                              DocumentRoot "/Applications/MAMP/htdocs"
                                              ServerName localhost
                                              SSLEngine on
                                              SSLCertificateFile "/Applications/MAMP/conf/apache/localhost.crt"
                                              SSLCertificateKeyFile "/Applications/MAMP/conf/apache/localhost.key"
                                          </VirtualHost>
                                      
    • Enregistrez et fermez (avec CTRL+X, puis Y, puis Entrée).
  5. Redémarrer et tester HTTPS
    • Redémarrez MAMP pour appliquer les changements.
    • Ouvrez votre navigateur et accédez à :
      
                                          https://localhost
                                      
    • Grâce à mkcert, votre certificat est automatiquement reconnu par le système et aucun avertissement de sécurité ne devrait s'afficher.

Exo-requetes-reseau-01

  • Objectif : Effectuer une requête réseau simple avec fetch(), analyser les informations affichées dans l'onglet Réseau de votre navigateur et retrouver l'URL cachée qui vous permettra d'accéder à l'exercice suivant.
  • Instructions :
    1. Créez un nouveau projet dans un dossier nommé exo-requetes-reseau-01.
    2. Créez un dossier js à la racine du projet.
    3. Dans le dossier js, créez un fichier nommé app.js.
    4. À la racine du projet, créez un fichier index.html contenant une structure HTML de base et importez-y le fichier app.js.
    5. Dans app.js, effectuez une requête avec fetch() vers l'URL suivante : https://js-exo-fetch.cvmdev.be/exo-01, sans configuration supplémentaire, c'est-à-dire en passant uniquement l'URL en argument.
    6. Récupérez la réponse du serveur dans le bloc fetch() et affichez cette réponse dans la console avec console.log().
    7. Ajoutez un catch() à votre requête fetch() afin de gérer les erreurs potentielles et affichez-les dans la console avec console.error().
    8. Ouvrez l'application dans votre navigateur.
    9. Accédez à la console des outils de développement.
    10. Si la requête est bien exécutée, un objet Response apparaîtra dans la console. Dépliez cet objet et vérifiez que la propriété ok est bien à true pour vous assurer que le serveur a répondu avec succès et que la requête n'a pas rencontré d'erreur.
    11. Ouvrez l'onglet Réseau des outils de développement. Si la liste est vide, rechargez la page.
    12. Identifiez la requête envoyée vers https://js-exo-fetch.cvmdev.be/exo-01 et explorez les informations disponibles dans les différentes sections afin de retrouver l'URL cachée, que vous devrez utiliser pour l'exercice suivant :
      • Les en-têtes : section En-têtes.
      • Les cookies : section Cookies.
      • Les détails de la requête : section Requête.
      • Le contenu de la réponse : section Réponse.

Exo-requetes-reseau-02

  • Objectif : Effectuer une requête réseau simple avec fetch(), analyser les informations affichées dans l'onglet Réseau de votre navigateur et retrouver le paramètre d'URL secret caché, indispensable pour réaliser l'exercice suivant.
  • Instructions :
    1. Dupliquez le dossier exo-requetes-reseau-01 provenant de l'exercice précédent et renommez-le en exo-requetes-reseau-02.
    2. Remplacez l'URL utilisée dans la requête fetch() par celui que vous avez récupéré lors de l'exercice précédent.
    3. Si ce n'est pas déjà fait, modifiez le bloc then() afin de détecter une erreur si la réponse de la requête est invalide. Utilisez la propriété ok pour vérifier si la requête a réussi. Si la requête a échoué, générez une erreur en affichant son code status ainsi que son message statusText.
    4. Ouvrez l'application dans votre navigateur.
    5. Accédez à la console des outils de développement.
    6. Si la requête est bien exécutée, un objet Response apparaîtra dans la console. Dépliez cet objet et vérifiez que la propriété ok est bien à true pour vous assurer que le serveur a répondu avec succès et que la requête n'a pas rencontré d'erreur.
    7. Ouvrez l'onglet Réseau des outils de développement. Si la liste est vide, rechargez la page.
    8. Identifiez la requête envoyée avec fetch() et explorez les informations disponibles dans les différentes sections afin de retrouver le paramètre d'URL secret dont vous aurez besoin pour accéder à l'exercice suivant.

Lire le Corps de la Réponse

Comme nous l'avons vu précédemment, l'objet Response ne contient pas directement les données demandées. Celles-ci se trouvent dans le corps de la réponse (body), qui est un flux lisible (ReadableStream). Plutôt que de manipuler ce flux manuellement, l'objet Response fournit des méthodes de commodité qui consomment l'intégralité du flux et retournent une promesse contenant les données dans le format souhaité.

Notez que le corps de la réponse ne peut être lu qu'une seule fois. Appeler .json() puis .text() sur le même objet Response provoquera une erreur, car le flux a déjà été consommé par le premier appel. Si vous avez besoin d'utiliser les données sous plusieurs formes, stockez le résultat dans une variable après la première lecture.

Voici les principales méthodes de lecture disponibles :

  • json() : Interprète le corps comme du JSON et retourne une promesse contenant un objet ou un tableau JavaScript. C'est la méthode la plus courante lorsque vous travaillez avec des API REST, qui retournent généralement leurs données au format JSON.
    
                                response.json()
                                    .then(donnees => {
                                        // "donnees" est un objet ou un tableau JavaScript.
                                    })
                            
  • text() : Retourne une promesse contenant le corps de la réponse sous forme de chaîne de caractères. Utile lorsque la réponse est du texte brut, du HTML ou du XML que vous ne souhaitez pas parser automatiquement.
    
                                response.text()
                                    .then(texte => {
                                        // "texte" est une chaîne de caractères.
                                    })
                            
  • blob() : Retourne une promesse contenant un objet Blob (Binary Large Object). Un Blob est une représentation binaire brute des données, utilisée pour manipuler des fichiers comme des images, vidéos ou documents. Vous pouvez ensuite le convertir en URL temporaire avec URL.createObjectURL(), ce qui permet d'utiliser le contenu directement dans la page (par exemple comme source d'une balise <img>).
    
                                response.blob()
                                    .then(blob => {
                                        // Convertir le Blob en une URL temporaire utilisable par le navigateur.
                                        const fichierUrl = URL.createObjectURL(blob);
    
                                        // Cette URL peut être utilisée comme source d'une image, d'une vidéo, etc.
                                        // Exemple : document.querySelector("img").src = fichierUrl;
                                    });
                            

D'autres méthodes existent, comme arrayBuffer() pour obtenir les données sous forme de tampon binaire brut (utile pour le traitement de fichiers au niveau octet) ou formData() pour interpréter le corps comme des données de formulaire. Ces méthodes sont plus spécialisées et moins fréquentes dans un usage courant.

Voici un exemple complet qui récupère la liste des utilisateurs depuis l'API, extrait les données JSON, puis les exploite pour afficher le nom et l'email de chaque utilisateur :


                    // L'URL à partir de laquelle nous souhaitons importer des données JSON.
                    const url = "https://jsonplaceholder.typicode.com/users";

                    // Utilisation de la méthode fetch pour effectuer la requête HTTP.
                    fetch(url)
                        .then(reponse => 
                        {
                            // Vérification que la réponse HTTP est réussie (code 200 à 299).
                            if (!reponse.ok)
                            {
                                // Si le statut HTTP n'est pas "ok", on génère une erreur avec le code associé.
                                throw new Error(`La requête HTTP a échoué : ${reponse.status}`);
                            }

                            // La méthode "json()" consomme le flux de la réponse et retourne une promesse.
                            // Cette promesse se résout avec les données converties en objet ou tableau JavaScript.
                            return reponse.json();
                        })
                        .then(utilisateurs => 
                        {
                            // "utilisateurs" contient désormais un tableau d'objets JavaScript.
                            // On parcourt ce tableau pour afficher le nom et l'email de chaque utilisateur.
                            utilisateurs.forEach(utilisateur => 
                            {
                                console.log(`${utilisateur.name} — ${utilisateur.email}`);
                            });
                        })
                        .catch(erreur => 
                        {
                            // Ce bloc intercepte :
                            // - Les erreurs réseau (rejet de la promesse fetch).
                            // - Les exceptions levées dans les blocs ".then()" (ex. le "throw" ci-dessus).
                            // - Les erreurs de parsing JSON (si le corps n'est pas du JSON valide).
                            console.error(erreur.message);
                        });
                    /*
                        Affiche :
                            Leanne Graham — Sincere@april.biz
                            Ervin Howell — Shanna@melissa.tv
                            Clementine Bauch — Nathan@yesenia.net
                            Patricia Lebsack — Julianne.OConner@kory.org
                            Chelsey Dietrich — Lucio_Hettinger@annie.me
                            Mrs. Dennis Schulist — Karley_Dach@jasper.info
                            Kurtis Weissnat — Telly.Hoeger@billy.biz
                            Nicholas Runolfsdottir V — Sherwood@rosamond.me
                            Glenna Reichert — Chaim_McDermott@dana.io
                            Clementina DuBuque — Rey.Padberg@karina.biz
                    */
                

Exercices - Partie 02

Exo-requetes-reseau-03

  • Objectif : Réaliser une requête réseau en JavaScript à l'aide de fetch(), en utilisant le paramètre d'URL récupéré lors de l'exercice précédent. Analyser la réponse pour identifier le type de contenu retourné, appliquer la méthode de conversion appropriée, et obtenir l'URL à utiliser pour l'exercice suivant.
  • Instructions :
    1. Dupliquez le dossier exo-requetes-reseau-02 provenant de l'exercice précédent et renommez-le en exo-requetes-reseau-03.
    2. Remplacez l'URL utilisée dans la requête fetch() par https://js-exo-fetch.cvmdev.be/exo-03 en y ajoutant le paramètre d'URL récupéré lors de l'exercice précédent. Pour rappel, voici comment ajouter un paramètre dans une URL :
      
                                          https://js-exo-fetch.cvmdev.be/exo-03?nomDuParametre=valeurDuParametre
                                      
    3. Lancez l'application dans votre navigateur.
    4. Dans la console des outils de développement, observez le type de contenu obtenu dans la réponse afin de déterminer quelle méthode de conversion utiliser, à savoir json(), text() ou blob().
    5. Modifiez le bloc then() en appliquant la méthode de conversion appropriée pour rendre la réponse exploitable, puis retournez le résultat obtenu.
    6. Ajoutez un deuxième bloc then() pour récupérer les données converties et affichez le résultat dans la console.
    7. Ouvrez à nouveau la console du navigateur et récupérez l'URL nécessaire pour effectuer la requête du prochain exercice.

Exo-requetes-reseau-04

  • Objectif : Effectuer une requête réseau en JavaScript avec fetch(), en utilisant l'URL obtenue lors de l'exercice précédent, et afficher dynamiquement une image récupérée depuis le serveur.
  • Instructions :
    1. Dupliquez le dossier exo-requetes-reseau-03 provenant de l'exercice précédent et renommez-le en exo-requetes-reseau-04.
    2. Remplacez l'URL utilisée dans la requête fetch() avec celle obtenue à la fin de l'exercice précédent.
    3. La réponse du serveur contiendra une image au format binaire. Utilisez la méthode blob() pour convertir cette réponse en un objet de type Blob.
    4. Avec le résultat obtenu, utilisez la méthode URL.createObjectURL() pour générer une URL temporaire que vous pourrez utiliser comme source pour afficher l'image.
    5. Créez une balise img en JavaScript en instanciant un nouvel objet de la classe Image(), puis définissez l'URL générée comme valeur de l'attribut src.
    6. Ajoutez cette balise img au document HTML en l'insérant dans la balise main à l'aide de la méthode append().
    7. Ouvrez l'application dans votre navigateur et vérifiez que l'image s'affiche correctement.
    8. Sur l'image, vous trouverez l'URL vous permettant d'accéder à l'exercice suivant.
    9. Si l'image ne s'affiche pas, vérifiez les éventuels messages d'erreur dans la console de l'inspecteur du navigateur.

Configuration des Requêtes

Dans le deuxième argument de la méthode fetch(), vous avez la possibilité de fournir une configuration personnalisée pour votre requête. Cette configuration vous permet de définir des options telles que la méthode de la requête (GET ou POST), les en-têtes HTTP, le corps de la requête, etc. Cela vous offre un contrôle précis sur la manière dont votre requête HTTP est effectuée.

Pour une requête GET simple, il n'est généralement pas nécessaire de fournir explicitement un deuxième argument de configuration. Cependant, lorsque vous souhaitez effectuer un type de requête différent, comme une requête de type POST, vous devrez spécifier la méthode, les en-têtes, et éventuellement le corps de la requête dans ce deuxième argument. Cela vous permet de personnaliser votre requête en fonction de vos besoins spécifiques :


                    // URL verse laquelle nous souhaitons effectuer une requête de type "POST".
                    const url = "https://jsonplaceholder.typicode.com/posts";

                    // Données à envoyer sous forme JSON (chaîne de caractères).
                    const donneesJSON = JSON.stringify({
                        title: "Titre de test",
                        body: "Contenu de test"
                    });

                    // Configuration de la requête HTTP.
                    const requeteConfig = 
                    {
                        // La méthode HTTP utilisée (POST pour envoyer des données)
                        method: "POST",

                        // Configuration de l'entête HTTP.
                        headers: 
                        {
                            // Spécifie que les données envoyées sont au format JSON
                            "Content-Type": "application/json"
                        },

                        body: donneesJSON, // Envoi des données JSON ici.
                    };

                    // Utilisation de la méthode fetch pour effectuer la requête HTTP.
                    fetch(url, requeteConfig)
                        .then((reponse) => 
                        {
                            // Vérification que la réponse HTTP est réussie (code 200 à 299).
                            if (reponse.ok) 
                            {
                                // La méthode "json()" lit le flux de la réponse et retourne une promesse.
                                // Cette promesse se résout avec les données converties en objet ou tableau JavaScript.
                                return reponse.json();
                            }
                            // Si le statut HTTP n'est pas "ok", on génère une erreur avec le code et le message associés.
                            throw new Error(`La requête HTTP a échoué : ${reponse.status} ${reponse.statusText}`);
                        })
                        .then((donnees) => 
                        {
                            // Une fois les données JSON extraites de la réponse, on les affiche dans la console.
                            console.log(donnees);
                        })
                        .catch((erreur) => 
                        {
                            // En cas d'erreur, on affiche l'erreur dans la console.
                            console.error(erreur.message);
                        });
                    /*
                        Affiche : 
                            Object { title: "Titre de test", body: "Contenu de test", id: 101 }
                            body: "Contenu de test"
                            id: 101
                            title: "Titre de test"
                    */
                

Options de configuration

Les options de configuration permettent de personnaliser le comportement des requêtes HTTP en définissant divers paramètres, tels que la méthode, les en-têtes, la gestion des cookies ou encore le mode de cache.

  • method :
    • Description : Définit la méthode HTTP utilisée pour la requête.
    • Valeurs disponibles :
      • GET : Récupérer des données depuis un serveur.
      • POST : Créer ou déclencher une action côté serveur.
      • PUT : Remplacer entièrement une ressource existante.
      • PATCH : Modifier partiellement une ressource.
      • DELETE : Supprimer une ressource.
      • HEAD : Identique à GET mais renvoie uniquement les en-têtes, utile pour vérifier l'existence, la taille ou la date de modification d'une ressource.
      • OPTIONS : Utilisée notamment lors d'une requête preflight liée au mécanisme CORS, elle permet au navigateur d'interroger le serveur avant la requête principale afin de vérifier quelles méthodes, quels en-têtes et quelles origines sont autorisés pour accéder à la ressource.
  • credentials :
    • Description : Contrôle l'envoi des cookies et identifiants d'authentification avec la requête.
    • Valeurs disponibles :
      • omit : N'envoie pas les cookies.
      • same-origin : Envoie les cookies uniquement si la requête est faite vers la même origine.
      • include : Envoie toujours les cookies, même pour les requêtes cross-origin.
  • mode :
    • Description : Indique au navigateur quelles règles appliquer lorsque vous appelez une autre origine. Cette option ne donne pas une autorisation à elle seule. L'autorisation réelle dépend des en-têtes CORS renvoyés par le serveur (voir l'encadré sur CORS ci-dessous).
    • Valeurs disponibles :
      • cors : Mode normal pour appeler une API sur un autre domaine. La réponse ne sera lisible en JavaScript que si le serveur renvoie des en-têtes CORS valides.
      • same-origin : Refuse toute requête vers une autre origine. Utile comme garde-fou si vous voulez être certain de ne jamais appeler un autre domaine.
      • no-cors : La requête est bien envoyée et le serveur la reçoit, mais le navigateur rend la réponse "opaque", c'est-à-dire qu'il est impossible d'en lire les données, les en-têtes ou le véritable code HTTP. La propriété response.status vaudra 0 et response.ok sera false, quel que soit le résultat réel côté serveur. Ce mode ne convient donc pas aux API dont on doit exploiter la réponse. Il sert uniquement aux envois sans attente de retour, comme l'enregistrement d'une visite ou l'envoi de statistiques.
  • body :
    • Description : Contenu envoyé avec la requête, principalement utilisé pour les méthodes POST, PUT ou PATCH.
    • Valeurs disponibles : Chaîne ou objet JSON.
  • headers :
    • Description : Contient l'ensemble des en-têtes HTTP à inclure dans la requête.
    • Valeurs disponibles : Consultez la section suivante pour découvrir les différentes options disponibles.
  • ...

CORS (Cross-Origin Resource Sharing)

Lorsqu'une page web tente d'accéder à une ressource située sur un domaine différent du sien, le navigateur applique une politique de sécurité appelée Same-Origin Policy (politique de même origine). Cette politique empêche par défaut un script d'accéder aux réponses provenant d'une autre origine. Une "origine" est définie par la combinaison du protocole, du domaine et du port (par exemple, https://monsite.com:443).

Le mécanisme CORS (Cross-Origin Resource Sharing) permet au serveur d'indiquer explicitement quelles origines sont autorisées à lire ses réponses. Concrètement, le serveur ajoute des en-têtes spécifiques dans sa réponse HTTP, comme Access-Control-Allow-Origin, pour autoriser certaines origines (ou toutes, avec la valeur *).

En pratique, si vous rencontrez une erreur CORS dans la console de votre navigateur, cela signifie que le serveur n'a pas autorisé votre origine à accéder à la ressource. C'est un mécanisme de protection côté navigateur et non une erreur dans votre code JavaScript. La solution se trouve côté serveur, en configurant les en-têtes CORS appropriés.

Pour les requêtes non triviales (par exemple celles qui utilisent une méthode autre que GET ou POST, ou qui incluent des en-têtes personnalisés), le navigateur envoie automatiquement une requête préliminaire (dite "preflight") avec la méthode OPTIONS pour vérifier que le serveur autorise bien la requête prévue avant de l'envoyer réellement.

Les en-têtes

Les en-têtes HTTP (headers) sont des informations supplémentaires envoyées avec une requête ou une réponse HTTP. Ils permettent de fournir des métadonnées qui aident le serveur ou le client à interpréter correctement la requête ou la réponse.

Voici quelques exemples d'en-têtes standards souvent inclus ou configurables avec Fetch :

  • Accept :
    • Description : Indique les types de contenu que le client peut traiter (ce que le client veut recevoir).
    • Valeurs disponibles :
      • text/html : Accepte les réponses en HTML.
      • application/json : Accepte les réponses au format JSON.
      • */* : Accepte tous les types de contenu.
  • Content-Type :
    • Description : Indique le format des données envoyées par le client dans une requête et celui des données renvoyées par le serveur dans une réponse.
    • Valeurs disponibles :
      • application/json : Données au format JSON.
      • text/plain : Contenu en texte brut.
      • multipart/form-data : Utilisé pour les formulaires avec fichiers.
  • Authorization :
    • Description : Permet d'indiquer au serveur comment vous vous authentifiez.
    • Valeurs disponibles :
      • Basic : Envoie un couple identifiant et mot de passe encodé en Base64. Souvent utilisé pour obtenir un jeton.
      • Bearer : Envoie un jeton fourni par le serveur. Utilisé ensuite pour accéder aux routes protégées sans renvoyer le mot de passe.
  • ...

L'en-tête Authorization

L'en-tête Authorization sert à prouver au serveur que vous avez le droit d'accéder à une ressource. Il existe plusieurs formats, mais nous allons nous concentrer ici sur deux formats particulièrement répandus.

Authorization Basic

L'authentification Basic envoie un couple identifiant + mot de passe dans l'en-tête. Le couple est encodé en Base64. Attention, Base64 n'est pas un chiffrement. C'est seulement un encodage. Pour cette raison, Basic doit être utilisé avec HTTPS.

Basic est souvent utilisé pour une étape courte, par exemple obtenir un jeton. Ensuite, on utilise ce jeton pour éviter de renvoyer le mot de passe à chaque requête.

Notez que dans l'exemple ci-dessous, la méthode GET est utilisée pour illustrer l'envoi de l'en-tête. En pratique, les endpoints d'authentification (comme une route /token ou /login) acceptent souvent la méthode POST. Le choix de la méthode dépend de la documentation de l'API que vous utilisez.


                    // Construire la chaîne "login:motDePasse".
                    const utilisateur = "Christophe";
                    const motDePasse = "123456";
                    const identifiants = `${utilisateur}:${motDePasse}`;

                    // btoa() encode la chaîne "login:motDePasse" en Base64, format requis par l'authentification HTTP Basic.
                    const identifiantsBase64 = btoa(identifiants);

                    // Construire la valeur finale de l'en-tête Authorization.
                    const valeurAuthorization = `Basic ${identifiantsBase64}`;

                    fetch("https://api.example.com/token", {
                        method: "GET",
                        headers: {
                            "Authorization": valeurAuthorization,
                            "Accept": "application/json"
                        }
                    })
                    .then((reponse) =>
                    {
                        if (!reponse.ok)
                        {
                            throw new Error(`Erreur HTTP : ${reponse.status}`);
                        }

                        // Le serveur renvoie souvent du JSON contenant un jeton.
                        return reponse.json();
                    })
                    .then((donnees) =>
                    {
                        console.log(donnees);
                    })
                    .catch((erreur) =>
                    {
                        console.error(erreur.message);
                    });
                
Authorization Bearer

L'authentification Bearer envoie un jeton. Ce jeton est fourni par le serveur après une étape d'identification, souvent réalisée avec Basic ou via un formulaire de connexion. Une fois le jeton obtenu, on l'utilise à la place du mot de passe pour accéder aux routes protégées.

Un jeton Bearer se transmet tel quel, sans btoa() et sans JSON.stringify(). Il va simplement dans l'en-tête.


                    // Exemple de jeton renvoyé par le serveur.
                    const jeton = "eyJhbGciOi...";

                    fetch("https://api.example.com/messages", {
                        method: "GET",
                        headers: {
                            "Authorization": `Bearer ${jeton}`,
                            "Accept": "application/json"
                        }
                    })
                    .then((reponse) =>
                    {
                        if (!reponse.ok)
                        {
                            throw new Error(`Erreur HTTP : ${reponse.status}`);
                        }

                        return reponse.json();
                    })
                    .then((donnees) =>
                    {
                        console.log(donnees);
                    })
                    .catch((erreur) =>
                    {
                        console.error(erreur.message);
                    });
                

En cas d'échec d'authentification, le serveur répond souvent avec 401 ou 403. Un jeton peut aussi expirer. Dans ce cas, il faut en redemander un.

Exercices - Partie 03

Exercice : Exo-requêtes-réseau-05

  • Objectif : Obtenir l'URL de l'exercice suivant ainsi qu'un jeton à usage unique permettant de s'enregistrer comme nouvel utilisateur de l'API.
  • Instructions :
    1. Dupliquez le dossier exo-requetes-reseau-04 de l'exercice précédent et renommez-le en exo-requetes-reseau-05.
    2. Remplacez l'URL utilisée dans la méthode fetch() par celle récupérée à partir de l'image affichée lors de l'exercice précédent.
    3. Ouvrez l'application dans votre navigateur.
    4. Dans la console ou l'onglet Réseau des outils de développement, examinez le type de contenu retourné dans la réponse afin de choisir la méthode de conversion appropriée, à savoir json(), text() ou blob().
    5. Adaptez le bloc then() pour utiliser la bonne méthode de conversion et affichez le résultat dans la console.
    6. Retournez dans la console de votre navigateur et rafraîchissez la page.
    7. Si tout a été correctement mis en place, vous devriez obtenir l'URL du prochain exercice ainsi qu'une indication sur l'endroit où récupérer le jeton à usage unique nécessaire à la création de votre compte sur l'API.

Exercice : Exo-requêtes-réseau-06

  • Objectif : Choisir un nom d'utilisateur et un mot de passe pour s'enregistrer sur l'API à l'aide du jeton à usage unique récupéré lors de l'exercice précédent. Une fois l'enregistrement réussi, vous obtiendrez l'URL permettant d'accéder à l'exercice suivant.
  • Instructions :
    1. Dupliquez le dossier exo-requetes-reseau-05 de l'exercice précédent et renommez-le en exo-requetes-reseau-06.
    2. Remplacez l'URL utilisée dans la méthode fetch() par celle récupérée lors de la résolution de l'exercice précédent.
    3. Préparez l'objet JSON que vous allez envoyer à l'API pour enregistrer votre compte utilisateur. Il devra avoir la structure suivante :
      • pseudo : une chaîne de 2 à 255 caractères.
      • mdp : une chaîne de 8 à 72 caractères.
      • jetonUsageUnique : le jeton à usage unique récupéré lors de l'exercice précédent.
    4. Convertissez cet objet en JSON avant de l'envoyer à l'API. Pour cela, utilisez la méthode JSON.stringify().
    5. Ajoutez une configuration à la requête fetch() en passant un objet de configuration en deuxième argument :
      • Indiquez que la requête est de type POST, puisque nous envoyons des données au serveur.
      • Ajoutez un en-tête pour préciser que les données envoyées sont au format JSON.
      • N'oubliez pas d'inclure les données à envoyer dans le corps de la requête.
    6. Ouvrez l'application dans votre navigateur.
    7. Dans la console des outils de développement, si tout a été correctement mis en place, vous devriez obtenir un message de validation ainsi que l'URL à utiliser pour la requête du prochain exercice.

Exercice : Exo-requêtes-réseau-07

  • Objectif : Utiliser votre pseudo et votre mot de passe pour configurer une requête avec l'authentification Basic, afin d'obtenir un jeton Bearer qui vous permettra de vous authentifier auprès de l'API sans transmettre à nouveau vos identifiants sensibles, ainsi que l'URL permettant d'accéder à l'exercice suivant.
  • Instructions :
    1. Dupliquez le dossier exo-requetes-reseau-06 de l'exercice précédent et renommez-le en exo-requetes-reseau-07.
    2. Remplacez l'URL utilisée dans la méthode fetch() par celle obtenue lors de la résolution de l'exercice précédent.
    3. Pour que la requête soit acceptée, vous devez fournir le pseudo et le mot de passe que vous avez choisis lors de l'enregistrement de votre compte à l'exercice précédent.
    4. Combinez ces deux éléments sous la forme suivante : login:motDePasse, puis encodez cette chaîne en base64 en utilisant la fonction btoa(). Cette étape est nécessaire pour respecter les standards d'authentification Basic.
    5. Ajoutez un en-tête d'autorisation au format suivant : Authorization: Basic [coupleLoginMotDePasseEnBase64], où le contenu entre crochets correspond au résultat de l'encodage.
    6. Cette partie de l'API avec laquelle vous allez communiquer n'attend aucune donnée dans le corps de la requête. Son unique rôle est de vous fournir un jeton Bearer à partir de votre identifiant et de votre mot de passe transmis dans l'en-tête. Veillez donc à adapter votre configuration en conséquence.
    7. Si la requête est correctement configurée, le serveur répondra avec des données au format JSON. Utilisez la méthode json() pour convertir ces données et les afficher dans la console.
    8. Ouvrez l'application dans votre navigateur.
    9. Vérifiez le résultat obtenu dans la console de l'inspecteur du navigateur. Si tout est correct, vous devriez récupérer votre jeton Bearer ainsi que l'URL à utiliser pour la requête du prochain exercice.

Exercice : Exo-requêtes-réseau-08

  • Objectif : Utiliser un jeton Bearer au lieu d'un pseudo et d'un mot de passe pour effectuer une requête sécurisée à l'API, récupérer les messages et leurs auteurs, puis les intégrer dynamiquement dans le DOM pour les afficher. Le jeton Bearer est privilégié car il évite d'exposer les identifiants à chaque requête et améliore la sécurité des échanges.
  • Instructions :
    1. Dupliquez le dossier exo-requetes-reseau-07 de l'exercice précédent et renommez-le en exo-requetes-reseau-08.
    2. Remplacez l'URL utilisée dans la méthode fetch() par celle obtenue lors de la résolution de l'exercice précédent.
    3. Modifiez la configuration de la requête pour passer de l'authentification Basic à l'authentification Bearer. Le jeton Bearer récupéré lors de l'exercice précédent peut être utilisé tel quel, sans conversion.
    4. La méthode json() appliquée à la réponse devrait vous retourner un objet contenant une clé spécifique qui inclut tous les messages enregistrés dans la base de données du serveur.
    5. Ouvrez la console des outils de développement du navigateur pour vérifier que tout fonctionne et que vous avez accès aux messages.
    6. Dans le fichier app.js (ou dans un nouveau fichier .js si vous préférez), créez une fonction nommée ajouterMessagesDansLeMain(). Cette fonction aura pour objectif d'ajouter dynamiquement dans la balise <main> une liste affichant les messages et leurs auteurs sous forme d'éléments HTML.
    7. La fonction doit respecter les spécifications suivantes :
      • Elle prend en paramètre un tableau nommé messages. Ce tableau contient des objets, où chaque objet possède deux propriétés :
        • message : le contenu du message.
        • utilisateur : le nom de l'auteur du message.
      • Pour chaque élément du tableau, la fonction doit :
        • Créer une balise <ul>.
        • Créer deux balises <li> à l'intérieur de cette <ul> :
          • La première balise <li> affichera : Nom : [nom du rédacteur].
          • La deuxième balise <li> affichera : Message : [contenu du message].
        • Ajouter les balises <li> à la balise <ul>.
        • Ajouter la balise <ul> à la balise <main>.
      • Pour réaliser ces étapes, utilisez :
        • La fonction createElement() pour créer les balises HTML (<ul> et <li>).
        • Une des propriétés suivantes pour insérer du texte dans les balises <li> : textContent, innerText ou innerHTML.
        • La fonction querySelector() pour cibler la balise <main>.
        • La fonction append() pour insérer les balises <li> dans les <ul>, et la <ul> dans la balise <main>.
    8. Dans la requête fetch(), une fois que la réponse du serveur a été reçue et convertie en JSON avec la méthode json(), appelez la fonction ajouterMessagesDansLeMain(). Passez-lui les messages extraits des données JSON (assurez-vous d'utiliser la bonne clé dans la réponse).
    9. Lancez l'application dans votre navigateur.
    10. Vérifiez que les messages et leurs auteurs s'affichent bien sous forme de liste sur la page web.

L'Approche async/await

Comme nous l'avons vu dans le chapitre précédent, le mot-clé async déclare une fonction qui retourne toujours une promesse, et le mot-clé await suspend l'exécution de cette fonction jusqu'à la résolution de la promesse qui le suit. Appliquée à fetch, cette syntaxe permet d'écrire des requêtes HTTP avec une lecture proche du code synchrone, tout en gérant les erreurs avec try/catch plutôt qu'avec .catch().

Le comportement est identique à celui des blocs .then(). La différence est purement syntaxique, async/await supprime l'imbrication des blocs .then(), ce qui peut rendre le code plus lisible lorsque les opérations s'enchaînent.

Pour illustrer la correspondance entre les deux approches, reprenons un exemple déjà réalisé avec le chaînage .then() et réécrivons-le avec async/await.

Plutôt que de répéter la logique de vérification HTTP et de lecture JSON dans chaque requête, on peut l'isoler dans une fonction utilitaire réutilisable. Cette fonction utilitaire suppose que le serveur renvoie toujours du JSON. Si vous travaillez avec une API qui retourne d'autres formats (texte brut, blob, etc.), il faudra adapter la méthode de lecture en conséquence.


                    // Fonction utilitaire : envoie une requête HTTP et renvoie les données JSON.
                    // Le paramètre "config" a une valeur par défaut (objet vide), ce qui signifie
                    // qu'il est optionnel. Sans configuration, fetch utilisera la méthode GET.
                    // Important : cette fonction suppose que le serveur répond toujours en JSON.
                    async function effectuerRequete(url, config = {})
                    {
                        const reponse = await fetch(url, config);

                        if (!reponse.ok)
                        {
                            throw new Error(`La requête HTTP a échoué : ${reponse.status}`);
                        }

                        // Ici, "await" n'est pas nécessaire avant "reponse.json()".
                        // Lorsqu'on retourne une promesse depuis une fonction "async",
                        // celle-ci est automatiquement propagée à l'appelant : c'est le "await"
                        // de l'appelant qui se chargera d'attendre la résolution.
                        // Écrire "return await reponse.json()" fonctionnerait aussi,
                        // mais le "await" serait alors redondant.
                        return reponse.json();
                    }
                

Cette fonction peut maintenant être utilisée par n'importe quelle fonction métier. Voici un exemple qui envoie une requête POST pour créer un article :


                    // Fonction métier qui utilise effectuerRequete() pour créer un article.
                    async function ajouterArticle()
                    {
                        // URL de test (service de démonstration).
                        const url = "https://jsonplaceholder.typicode.com/posts";

                        // Configuration de la requête POST avec un corps JSON.
                        const config =
                        {
                            method: "POST",
                            headers:
                            {
                                "Content-Type": "application/json"
                            },
                            body: JSON.stringify({
                                title: "Titre de test",
                                body: "Contenu de test"
                            })
                        };

                        try
                        {
                            // "await" attend la résolution de la promesse retournée par effectuerRequete().
                            // Si effectuerRequete() lève une exception (erreur HTTP),
                            // l'exécution saute directement au bloc "catch".
                            const donnees = await effectuerRequete(url, config);
                            console.log(donnees);
                        }
                        catch (erreur)
                        {
                            console.error(`Erreur lors de l'ajout d'article : ${erreur.message}`);
                        }
                    }

                    ajouterArticle();
                    /*
                        Affiche :
                            { title: "Titre de test", body: "Contenu de test", id: 101 }
                    */
                

Cette séparation entre fonction utilitaire et fonction métier présente plusieurs avantages. La logique de requête HTTP n'est écrite qu'une seule fois, chaque fonction métier reste concentrée sur son propre rôle, et la gestion des erreurs est centralisée côté appelant via try/catch.

Exercices - Partie 04

Exercice : Exo-requetes-reseau-09

  • Objectif : Utiliser async/await pour simplifier la syntaxe de la requête fetch().
  • Instructions :
    1. Dupliquez le dossier exo-requetes-reseau-08 provenant de l'exercice précédent et renommez-le en exo-requetes-reseau-09.
    2. Créez la fonction asynchrone executerRequeteHTTP() qui permettra d'accueillir la requête fetch() et de simplifier sa syntaxe :
      • Supprimez les blocs then().
      • Placez la requête fetch() dans un bloc try et connectez-y un bloc catch() pour gérer les erreurs.
      • Préfixez l'appel à fetch() avec le mot-clé await afin d'attendre la réponse du serveur avant de l'assigner à une constante reponse.
      • Vérifiez que la réponse reponse est valide en testant sa propriété ok. Si ce n'est pas le cas, levez une exception qui sera capturée par le bloc catch().
      • Convertissez les données reçues en données exploitables à l'aide de json(), préfixé du mot-clé await, et assignez le résultat à une constante resultat.
      • Appelez la fonction ajouterMessagesDansLeMain() en lui passant en argument les données provenant de l'objet resultat.
    3. Au chargement de l'application, appelez executerRequeteHTTP() pour exécuter la requête serveur.
    4. Ouvrez l'application dans votre navigateur.
    5. Vérifiez que les messages apparaissent correctement sur la page web.

Soumission de Formulaires

Pour améliorer l'expérience utilisateur, il est souvent utile d'envoyer les données d'un formulaire sans recharger entièrement la page.

Une méthode simple consiste à utiliser FormData. Cet outil récupère automatiquement les champs d'un formulaire qui possèdent un attribut name, puis prépare des paires clé/valeur prêtes à être envoyées au serveur. Attention, FormData ne renvoie pas un objet JavaScript classique, mais une structure spéciale dédiée aux données de formulaire.

Du côté serveur, cela reproduit le comportement d'un envoi de formulaire classique. Par exemple en PHP, les champs texte se retrouvent dans $_POST et les fichiers éventuels dans $_FILES, exactement comme lors d'une soumission traditionnelle.


                    // Fonction asynchrone pour effectuer une requête POST à partir des saisies utilisateur provenant d'un formulaire HTML.
                    async function effectuerRequete(url, requConfig)
                    {
                        // Effectuer la requête HTTP avec fetch et attendre la réponse.
                        const reponse = await fetch(url, requConfig);

                        // Vérification que la réponse HTTP est réussie (code 200 à 299).
                        if (reponse.ok)
                        {
                            // Extraire le corps de la réponse sous forme de texte.
                            // Si votre serveur renvoie du JSON, vous pouvez remplacer text() par json().
                            return reponse.text();
                        }

                        // Si le statut HTTP n'est pas "ok", on génère une erreur avec le code et le message associés.
                        throw new Error(`La requête HTTP a échoué : ${reponse.status} ${reponse.statusText}`);
                    }

                    async function soumettreFormulaire(requeteUrl, form)
                    {
                        // FormData(formulaire) crée un objet "FormData" à partir des champs ayant un attribut "name".
                        // Notez que les champs "disabled" ne sont pas inclus.
                        const donnees = new FormData(form);

                        // Configuration de la requête HTTP.
                        const requeteConfig =
                        {
                            // Type de requête désirée.
                            method: "POST",

                            // Aucun en-tête n'est nécessaire ici, car FormData est pris en charge automatiquement par le navigateur.
                            // Avec FormData, il est recommandé de ne pas définir manuellement "Content-Type".
                            // Le navigateur configurera automatiquement cet en-tête en "multipart/form-data" avec un boundary.
                            // Le boundary est une chaîne générée par le navigateur qui sert de séparateur.
                            // Ici, le boundary est utilisé pour séparer chaque champ du formulaire dans le corps multipart.

                            // Envoyer les entrées utilisateurs provenant des champs du formulaire.
                            body: donnees
                        };

                        try
                        {
                            // Appel à la fonction "effectuerRequete()" pour effectuer la requête HTTP.
                            const resultat = await effectuerRequete(requeteUrl, requeteConfig);

                            // Afficher la réponse du serveur.
                            console.log(resultat);
                        }
                        catch (e)
                        {
                            console.error(`Erreur lors de l'envoi du formulaire : ${e.message}`);
                        }
                    }

                    // Référencer le formulaire.
                    const monFormulaire = document.querySelector("form#monForm");

                    // Soumettre via l'événement "submit" plutôt que d'envoyer dès le chargement de la page.
                    monFormulaire.addEventListener("submit", (event) =>
                    {
                        // Empêche le rechargement de page.
                        event.preventDefault();

                        // Utiliser l'action du formulaire si elle existe, sinon rester sur l'URL actuelle.
                        const urlCible = monFormulaire.action || window.location.href;

                        // Déclencher l'envoi avec fetch.
                        soumettreFormulaire(urlCible, monFormulaire);
                    });
                

Lorsque vous utilisez FormData avec fetch, il est déconseillé de définir manuellement l'en-tête Content-Type. Le navigateur choisit automatiquement multipart/form-data et ajoute une délimitation (boundary) qui permet au serveur de séparer correctement les différentes valeurs, et éventuellement les fichiers. Si vous définissez Content-Type vous-même, le boundary risque de manquer, ce qui rend la requête difficile à interpréter côté serveur.

Une fois cette mise en place effectuée, il reste à traiter les données côté serveur comme pour un formulaire classique.

Exercices - Partie 05

Exercice : Exo-requetes-reseau-10

  • Objectif : Ajouter un formulaire pour envoyer un message via une interface graphique.
  • Instructions :
    1. Dupliquez le dossier exo-requetes-reseau-09 provenant de l'exercice précédent et renommez-le en exo-requetes-reseau-10.
    2. Dans le fichier index.html, ajoutez un formulaire avec le champ suivant :
      • message :
        • Un champ textarea.
        • Ajoutez l'attribut name="message".
        • Ce champ est requis et doit contenir entre 10 et 3000 caractères.
    3. Dans le fichier app.js :
      • Remplacez l'URL utilisée dans la méthode fetch() avec celle récupérée lors de la résolution de l'un des deux derniers exercices.
      • Ajoutez un écouteur d'événement submit sur le formulaire.
      • Déclenchez une fonction intermédiaire nommée gererSoumissionForm() à la soumission.
    4. Créez la fonction gererSoumissionForm() dont l'objectif sera de récupérer les données du formulaire, les formater et les transmettre à la fonction executerRequeteHTTP() :
      • Paramètre : un objet event, automatiquement fourni par l'écouteur d'événement.
      • Annulez le comportement par défaut avec event.preventDefault().
      • Récupérez la référence au formulaire avec event.currentTarget.
      • Formatez les données utilisateur en créant un objet FormData() à partir de cette référence.
      • Appelez executerRequeteHTTP() en lui passant cet objet FormData.
    5. Modifiez la fonction executerRequeteHTTP() dont l'objectif sera maintenant de centraliser la gestion des requêtes fetch() pour les opérations GET et POST :
      • La fonction prend maintenant un paramètre nouveauMessage, qui a pour valeur par défaut null.
      • Si nouveauMessage est fourni, effectuez une requête POST avec les données du formulaire.
      • Sinon, effectuez une requête GET pour récupérer les messages.
    6. Au chargement de l'application, appelez executerRequeteHTTP() sans argument pour récupérer tous les messages avec une requête GET.
    7. Vérifiez que les messages s'affichent correctement sur la page. Si ce n'est pas le cas, consultez la console pour identifier les erreurs.
    8. Ajoutez un message avec le formulaire et vérifiez qu'il est correctement envoyé et affiché.
    9. Modifiez la fonction ajouterMessagesDansLeMain() dont l'objectif sera d'empêcher les doublons en remplaçant l'affichage précédent des messages :
      • Créez une balise div avec document.createElement() pour contenir les messages.
      • Ajoutez l'attribut id="conteneur-messages" avec setAttribute().
      • Ajoutez tous les messages dans ce conteneur.
      • Vérifiez si un conteneur avec l'identifiant conteneur-messages existe déjà dans le DOM :
        • Si oui, remplacez-le avec la méthode replaceWith().
        • Sinon, ajoutez ce conteneur à la balise main avec append().
    10. Testez que la liste des messages s'actualise correctement à chaque ajout, sans conserver les anciennes listes. Notez que le serveur est configuré pour accepter un maximum de 3 messages par compte.