Project Oxygen & Ideo-LabIDEO LAB Dashboard 2026

⚡ AJAX – Le Web Asynchrone

Guide dense : XHR, Fetch, async/await, CORS & Intégration Django.

1.1 Facile

Principes & Concepts

Le "Pourquoi". Synchrone (bloquant) vs. Asynchrone (non-bloquant). "X" (XML) vs. "J" (JSON).

Asynchrone JSON
1.2 Moyen

L'Historique : XMLHttpRequest

L'ancêtre. onreadystatechange, readyState (0-4), status (200, 404). Basé sur les événements.

XHR readyState
1.3 Facile

Le Moderne : fetch()

Basé sur les Promises. Chaînage .then(), objet Response, headers, body.

fetch Promise
2.1 Facile

L'Évolution : async/await

Sucre syntaxique pour les Promises. Rend le code asynchrone lisible. Gestion try...catch.

async await
2.2 Moyen

Formats de Données

JSON (le standard), FormData (fichiers), text/html, XML (l'ancêtre).

JSON.stringify() FormData
2.3 Avancé

Gestion des Erreurs (Avancée)

Erreur Réseau (.catch()) vs. Erreur HTTP (404/500). Le piège response.ok.

response.ok .catch()
3.1 Avancé

🚫 CORS (Cross-Origin)

Le "cauchemar". Same-Origin Policy. Access-Control-Allow-Origin. Requêtes Preflight (OPTIONS).

CORS Preflight
3.2 Avancé

Concepts Avancés

Annuler (AbortController), Debouncing (barre de recherche), Throttling, Polling.

AbortController Debounce
3.3 Avancé

🐍 Intégration Django

Le mur du CSRF Token. JsonResponse, json.loads(request.body), X-CSRFToken.

CSRF Token JsonResponse
1.1 Principes & Concepts (Le "Pourquoi")
Le Problème : Le Web Synchrone (Bloquant)

Avant AJAX (début des années 2000), pour obtenir 1 seule nouvelle donnée (ex: la météo), le navigateur devait recharger la page entière. L'utilisateur voyait un écran blanc, le serveur devait tout recalculer (header, footer, menus...). C'était lent et consommateur de ressources.

Le JavaScript "Synchrone" est également "bloquant". Le thread principal du navigateur ne fait qu'une chose à la fois. Si une tâche est longue, l'UI est gelée.

// 1. Le bouton est cliqué
button.addEventListener('click', () => {
    // 2. Tâche *bloquante* (l'UI est GELÉE)
    alert("Vous ne pouvez rien faire...");
    
    // 3. (Seulement après 5s...) L'UI est libérée
    console.log("Fin");
});
La Solution : Le Web Asynchrone (Non-Bloquant)

AJAX (Asynchronous JavaScript and XML) est une *technique* (pas un langage) qui permet au JavaScript de faire une requête HTTP au serveur *en arrière-plan*, sans geler l'UI et sans recharger la page.

Le navigateur "délègue" la tâche (ex: fetch) et continue sa vie (l'UI reste fluide). Quand le serveur répond, il exécute une fonction "callback" (ou résout une Promise).

// 1. Le bouton est cliqué
button.addEventListener('click', () => {
    // 2. Tâche *non-bloquante* (déléguée)
    // L'UI reste 100% réactive
    fetch('/api/data')
        .then(res => res.json())
        .then(data => {
            // 4. (Bien plus tard) Ce code s'exécute
            console.log(data);
        });
    
    // 3. Ce code s'exécute IMMÉDIATEMENT
    console.log("Requête lancée, l'UI est fluide.");
});
Le "X" (XML) vs "J" (JSON)

Le nom est historique. Le "X" signifie XML. À l'époque, les données étaient échangées en XML (verbeux). Aujourd'hui, 99% des requêtes AJAX utilisent JSON, car il est léger et se "mappe" parfaitement avec les objets JavaScript (JSON.parse, JSON.stringify).

1.2 L'Historique : XMLHttpRequest (XHR)

C'est l'API d'origine (créée par Microsoft !), disponible dans tous les navigateurs. Elle est basée sur les "événements" (events). Elle est verbeuse et complexe, mais essentielle à connaître pour maintenir du vieux code (ou pour gérer des uploads avec progression, ce que fetch fait mal).

Exemple de requête GET
function loadData() {
    // 1. Créer l'objet
    const xhr = new XMLHttpRequest();

    // 2. Configurer la requête (Méthode, URL, Asynchrone=true)
    xhr.open('GET', 'https://api.example.com/data', true);

    // 3. Définir le "listener" (ce qui se passe à chaque étape)
    xhr.onreadystatechange = function() {
        // 4. Vérifier si la requête est terminée (4) ET réussie (200)
        if (xhr.readyState === 4 && xhr.status === 200) {
            // 5. Traiter la réponse (c'est du texte)
            const data = JSON.parse(xhr.responseText);
            console.log(data);
        }
        // Gérer les autres cas (erreurs)
        else if (xhr.readyState === 4) {
            console.error("Erreur HTTP: " + xhr.status);
        }
    };
    
    // (Alternative plus simple que .onreadystatechange)
    // xhr.onload = function() {
    //     if (xhr.status === 200) { /* ... */ }
    // };

    // 6. Envoyer la requête
    xhr.send();
}
readyState (L'état de la requête)

La propriété qui indique où en est la requête.

ÉtatValeurDescription
0UNSENT.open() n'a pas été appelé.
1OPENED.open() a été appelé.
2HEADERS_RECEIVEDLes en-têtes de réponse ont été reçus.
3LOADINGLa réponse (responseText) est en cours de réception.
4DONELa requête est terminée.
status (Le code de réponse HTTP)

Le statut HTTP de la réponse (disponible à readyState 3 ou 4).

CodeDescription
200OK (Réussite)
201Created (ex: POST réussi)
304Not Modified (Cache)
400Bad Request (Erreur client)
401Unauthorized (Authentification requise)
403Forbidden (Droits insuffisants)
404Not Found (Ressource non trouvée)
500Internal Server Error (Erreur serveur)
1.3 Le Moderne : fetch()

L'API fetch() (native dans les navigateurs) remplace XHR. Elle est basée sur les Promises, ce qui la rend beaucoup plus propre et composable. (async/await est du sucre syntaxique au-dessus de fetch).

Exemple (GET)

Le "double .then()" est le piège classique. Le premier .then() reçoit la *réponse* (statut, headers), le second .then() reçoit le *corps* parsé.

fetch('https://api.example.com/data')
    // 1. La Promise est résolue avec un objet "Response"
    .then(response => {
        // 2. ON DOIT VÉRIFIER LES ERREURS HTTP MANUELLEMENT
        if (!response.ok) { // response.ok = true si status 200-299
            throw new Error("Erreur HTTP: " + response.status);
        }
        // 3. .json() retourne une *autre* Promise
        return response.json(); 
    })
    // 4. La 2ème Promise est résolue avec les données
    .then(data => {
        console.log(data);
    })
    // 5. Gère les erreurs RÉSEAU (ou le 'throw' ci-dessus)
    .catch(error => {
        console.error("Erreur Fetch:", error);
    });
Exemple (POST avec JSON)

Pour envoyer des données, on passe un 2ème argument (objet options) à fetch.

const userData = {
    username: "alice",
    role: "admin"
};

fetch('https://api.example.com/users', {
    // 1. Méthode HTTP
    method: 'POST',
    
    // 2. Headers (Comment on envoie)
    headers: {
        'Content-Type': 'application/json',
        // (On ajoutera 'X-CSRFToken' ici pour Django)
    },
    
    // 3. Corps de la requête (converti en string)
    body: JSON.stringify(userData)
})
.then(response => response.json())
.then(data => {
    console.log("Réponse du serveur:", data);
})
.catch(error => {
    console.error("Erreur:", error);
});
2.1 L'Évolution : async/await

async/await (ES2017) est du "sucre syntaxique" au-dessus des Promises. Il ne change pas le fonctionnement de fetch, mais il permet d'écrire du code asynchrone qui se lit comme du code synchrone (bloquant), en "mettant en pause" la fonction (pas le navigateur !). La gestion des erreurs se fait obligatoirement avec try...catch.

Exemple (GET)

Notez comme la logique est plate, sans "pyramide" .then().

// 1. La fonction DOIT être 'async'
async function getData() {
    // 2. 'try...catch' est obligatoire
    try {
        // 3. 'await' met en pause la fonction, pas le navigateur
        const response = await fetch('https://api.example.com/data');
        
        // 4. On vérifie toujours .ok !
        if (!response.ok) {
            throw new Error("Erreur HTTP: " + response.status);
        }
        
        // 5. 'await' pour la 2ème Promise (.json())
        const data = await response.json();
        
        console.log(data);
        return data;

    } catch (error) {
        // 6. Gère les erreurs Réseau ET le 'throw'
        console.error("Erreur:", error);
    }
}
Exemple (POST avec JSON)

La logique reste identique.

async function postData(userData) {
    try {
        const response = await fetch('https://api.example.com/users', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(userData)
        });

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        const data = await response.json();
        console.log("Réponse:", data);
        
    } catch (error) {
        console.error("Impossible de poster:", error);
    }
}
2.2 Formats de Données (Payload & Response)

Le "payload" est la donnée qu'on envoie (le body). Le type doit être spécifié dans le header Content-Type pour que le serveur sache comment l'interpréter.

application/json

Utilisé 99% du temps pour envoyer des objets de données. Le serveur (ex: Django) s'attend à recevoir une chaîne de caractères JSON.

const data = { id: 1, name: "Alice" };

fetch(url, {
    method: 'POST',
    // 1. Dire au serveur: "C'est du JSON"
    headers: { 'Content-Type': 'application/json' },
    // 2. Convertir l'objet JS en string JSON
    body: JSON.stringify(data) 
});

// Côté serveur (Django/Python):
// import json
// data = json.loads(request.body)
multipart/form-data (via FormData)

Utilisé pour envoyer des formulaires qui contiennent des fichiers (<input type="file">) ou des données binaires. C'est l'équivalent JS d'un formulaire HTML classique.

const myForm = document.querySelector('#myForm');
const myFile = document.querySelector('#myFileInput');

const formData = new FormData(); // Pas besoin de myForm, on peut le créer vide

// 1. Ajouter des champs (clé, valeur)
formData.append('username', 'alice');
formData.append('userFile', myFile.files[0]); // Ajoute le fichier

fetch(url, {
    method: 'POST',
    body: formData
    // 2. PRO-TIP: NE PAS METTRE 'Content-Type' ICI !
    // Le navigateur doit l'ajouter lui-même
    // (pour inclure le "boundary" du multipart)
});

// Côté serveur (Django):
// request.POST.get('username')
// request.FILES.get('userFile')
text/plain, text/html, application/xml

Moins courants pour l'envoi, mais fréquents en réception.

  • text/html : Utile pour récupérer un morceau de page HTML (ex: HTMX, un "partial") et l'insérer dans un <div>. On utilise response.text().
  • application/xml : L'ancêtre. Utilisé par les vieilles API (SOAP). On utilise response.text() puis un parseur XML.
  • text/plain : Simple texte.
// Récupérer un template HTML
async function getPartial() {
    const response = await fetch('/mon-template-partial/');
    const html = await response.text(); // Pas .json() !
    document.querySelector('#container').innerHTML = html;
}
2.3 Gestion des Erreurs (Avancée)

C'est le point le plus important. Un fetch() ne "rejette" (.catch()) QUE s'il y a une erreur réseau. Un 404 ou 500 n'est *pas* une erreur réseau, c'est une réponse HTTP valide.

Erreur Réseau (.catch())

La Promise fetch() est rejetée. Le .catch() (ou try...catch) est activé.

Causes :

  • CORS : Le serveur n'autorise pas la requête (le cas le plus fréquent).
  • Réseau : Pas de Wi-Fi, DNS introuvable, serveur éteint.
  • Annulation : La requête a été annulée (ex: AbortController).
try {
    // Le serveur 'non-existent-domain.xxx' n'existe pas
    await fetch('https://non-existent-domain.xxx');
} catch (error) {
    // On entre ICI
    console.error(error); // TypeError: Failed to fetch
}
Erreur HTTP (response.ok)

La Promise fetch() est réalisée (fulfilled) ! Le .catch() n'est PAS activé.

Causes :

  • 404 Not Found (L'URL est fausse)
  • 403 Forbidden (Pas les droits)
  • 500 Internal Server Error (Le serveur a planté)

On DOIT vérifier response.ok (qui vaut true si status est 200-299).

Le "Golden Pattern"
async function getData() {
    try {
        const response = await fetch('/api/url-valide-mais-erreur-500/');

        // 1. Gérer l'erreur HTTP
        if (!response.ok) {
            // Tenter de lire l'erreur JSON du serveur (si elle existe)
            const errorData = await response.json().catch(() => null);
            const message = errorData?.detail || `Erreur HTTP ${response.status}`;
            throw new Error(message);
        }

        const data = await response.json();
        console.log(data);

    } catch (error) {
        // 2. Gérer l'erreur Réseau OU le 'throw' ci-dessus
        console.error("Échec de l'opération:", error.message);
    }
}
3.1 🚫 CORS (Cross-Origin Resource Sharing)
Le Problème : Same-Origin Policy (SOP)

Par défaut, pour des raisons de sécurité, les navigateurs interdisent à un script (ex: https://mon-site.com) de faire un fetch vers une "Origine" différente (ex: https://api-externe.com).

Une "Origine" = Protocole + Domaine + Port.

CORS (Cross-Origin Resource Sharing) est le mécanisme qui permet au *serveur* (api-externe.com) de dire au navigateur : "C'est bon, j'autorise mon-site.com à me parler."

L'erreur fatale : ...has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present...

SOLUTION : C'est TOUJOURS au serveur (backend) de corriger. Le frontend ne peut rien faire (sauf proxy).

1. Requête Simple (ex: GET)

Le navigateur envoie la requête GET.

Le serveur doit répondre en incluant cet en-tête :

HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://mon-site.com
(ou: Access-Control-Allow-Origin: *)
2. Requête "Preflight" (ex: POST, PUT, DELETE)

Pour les requêtes "complexes" (POST, PUT, ou avec Content-Type: application/json), le navigateur envoie d'abord une "demande de permission" : la requête OPTIONS (Preflight).

Étape A : Le navigateur demande (Preflight)

OPTIONS /api/users
Host: api-externe.com
Origin: https://mon-site.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type

Étape B : Le serveur autorise

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://mon-site.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type

Si cette réponse est OK, le navigateur envoie *automatiquement* la vraie requête POST.

3.2 Concepts Avancés (Abort, Debounce, Poll)
Annuler un fetch (AbortController)

Permet d'annuler une requête en cours. Essentiel pour les barres de recherche (annuler la requête précédente quand l'utilisateur tape une nouvelle lettre).

let controller = new AbortController();

async function search(query) {
    // 1. Annuler la requête en cours
    controller.abort();
    
    // 2. Créer un nouveau contrôleur
    controller = new AbortController();
    const signal = controller.signal;

    try {
        // 3. Passer le 'signal' au fetch
        const res = await fetch(`/api/search?q=${query}`, { signal });
        const data = await res.json();
        // ...
    } catch (err) {
        if (err.name === 'AbortError') {
            console.log('Recherche annulée.');
        } else {
            console.error('Erreur:', err);
        }
    }
}
Debouncing (Barre de recherche)

Ne pas lancer la requête à *chaque* touche. Attendre que l'utilisateur ait *fini* de taper (ex: 300ms après la dernière touche).

let debounceTimer;

input.addEventListener('input', (e) => {
    // 1. Vider le timer précédent
    clearTimeout(debounceTimer);
    
    const query = e.target.value;

    // 2. Lancer un nouveau timer
    debounceTimer = setTimeout(() => {
        // 3. Exécuter le fetch seulement si 300ms se sont écoulées
        fetch(`/api/search?q=${query}`);
    }, 300);
});
Polling (Sondage)

Interroger le serveur à intervalle régulier pour voir s'il y a de nouvelles données. C'est l'ancêtre des WebSockets, mais encore très utilisé pour des notifications simples.

async function checkMessages() {
    try {
        const res = await fetch('/api/new-messages');
        const data = await res.json();
        if (data.new) {
            // ... afficher notif ...
        }
    } catch (e) {
        console.error('Erreur Polling');
    }
}

// 1. Lancer la vérification toutes les 30 secondes
setInterval(checkMessages, 30000);

// 2. Lancer immédiatement au chargement
checkMessages();
3.3 🐍 Intégration Django & Le Mur du CSRF

L'intégration d'AJAX (POST) avec Django se heurte toujours à une erreur 403 Forbidden à cause du CSRF Token (Cross-Site Request Forgery). C'est une protection, pas un bug. Voici comment la gérer.

Côté JavaScript (Frontend)

Le token CSRF est dans un cookie (csrftoken) ou dans le template ({{ csrf_token }}). Le plus simple est de le lire depuis le cookie et de l'ajouter à *chaque* fetch POST.

Snippet N°1 : Récupérer le Cookie CSRF

(Ce code est à inclure dans votre JS global)

function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            const cookie = cookies[i].trim();
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}
const csrftoken = getCookie('csrftoken');
Snippet N°2 : Envoyer le fetch avec le Token
async function postToDjango(data) {
    const response = await fetch('/api/ma-vue/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            // 1. AJOUTER LE TOKEN ICI
            'X-CSRFToken': csrftoken 
        },
        body: JSON.stringify(data)
    });
    return response.json();
}
Côté Django (Backend - views.py)

La vue doit gérer le JSON. request.POST ne fonctionne pas avec application/json (il ne marche qu'avec FormData ou x-www-form-urlencoded).

from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
import json

# @require_POST # S'assurer que c'est bien du POST
def ma_vue_json(request):
    
    # 1. Ne s'attendre qu'à du POST
    if request.method == 'POST':
        try:
            # 2. Lire le JSON depuis request.body
            data = json.loads(request.body)
            
            # 3. Traiter les données
            username = data.get('username')
            if not username:
                return JsonResponse({'status': 'erreur', 'message': 'Username manquant'}, status=400)

            # ... faire quelque chose avec 'username' ...
            
            # 4. Renvoyer une réponse JSON
            return JsonResponse({'status': 'succès', 'message': f'Utilisateur {username} traité'})
        
        except json.JSONDecodeError:
            return JsonResponse({'status': 'erreur', 'message': 'JSON invalide'}, status=400)
    
    # 5. Rejeter les autres méthodes
    return JsonResponse({'status': 'erreur', 'message': 'Méthode GET non autorisée'}, status=405)

# Note: csrf_exempt désactive le token. À N'UTILISER QU'EN DÉSESPOIR DE CAUSE
# ou pour des API publiques.