Précédent HTTP/2
Suivant Server-Sent Events (SSE)

XMLHttpRequest

Ce billet fait partie d'une série en 10 volets : #1, #2, #3, #4, #5, #6, #7, #8, #9 et #10.

Notes de lecture de XMLHttpRequest.

XMLHttpRequest (XHR) est une API JavaScript qui a donné naissance à l'architecture AJAX (Asynchronous JavaScript and XML). Elle permet de mettre à jour des portions de pages Web sans avoir à recharger la totalité de la page.

À partir de 2013, l'API XHR est devenue petit à petit un subset du standard Fetch que j'évoquerai en fin de billet.

Rapide histoire d'XMLHttpRequest

Malgré son nom, XMLHttpRequest n'est pas lié au XML. Le préfixe XML provient de la publication (silencieuse) par Microsoft de la première version d'XHR avec Internet Explorer 5.0 en 1999 en tant que partie d'MSXML. Je reprends une citation pointée dans Today, the Trident Era Ends :

[…] we pretty quickly struck a deal to ship the thing as part of the MSXML library. Which is the real explanation of where the name XMLHTTP comes from- the thing is mostly about HTTP and doesn't have any specific tie to XML other than that was the easiest excuse for shipping it so I needed to cram XML into the name (plus- XML was the hot technology at the time and it seemed like some good marketing for the component).

Les autres fabricants ne navigateurs ont conservé le nom de l'API.

Les premières versions d'XHR offraient des capacités limitées : transferts de données en mode texte uniquement, prise en charge restreinte des téléchargements et incapacité à traiter des demandes cross-domain.

Pour améliorer l'API, la spécification "XMLHttpRequest Level 2" publié en 2008 a ajouté de nouvelles fonctionnalités.

Les specifications ont fusionné en 2011 et les distinctions entre XHR et XHR2 n'ont plus lieu d'être.

Partage de ressources entre origines multiples

L'API XHR du navigateur impose une sémantique HTTP stricte aux requêtes. Il est impossible d'en modifier certains en-têtes ce qui "garantit" la non usurpation de l'identité d'un user-agent, d'un utilisateur ou de l'origine.

La protection de l'origin découle de la politique de même origine (same-origin policy).

La raison d'être de cette politique est d'empêcher d'exposer les données privées des utilisateurs (cookies etc.) à d'autres applications (sur d'autres domaines).

Or il peut y avoir des cas d'accès légitimes à d'autres domaines.

C'est ce que permet le partage de ressources entre origines multiples ou cross-origin resource sharing (CORS).

CORS consiste en un jeu d'en-têtes HTTP. Il s'agit pour un client de s'identifier via l'en-tête Origin afin d'obtenir du serveur la permission de charger une ressource. Le serveur décide (ou pas) d'accorder sa permission en retournant une réponse avec un en-tête HTTP Access-Control-Allow-Origin.

Les requêtes CORS côté client peuvent autoriser l'identification via withCredentials. Le serveur répond via un en-tête Access-Control-Allow-Credentials pour indiquer son accord.

Si une requête CORS côté client doit utiliser des en-têtes HTTP personnalisés ou une "méthode non simple" (hors GET, POST et HEAD), il doit d'abord demander la permission au serveur tiers en émettant une preflight request.

Du coup, différent cas génèrent des allers-retours complets de latence pour vérifier les autorisations.

La bonne nouvelle est qu'une preflight request peut être mise en cache côté client pour éviter la même vérification à chaque requête.

Téléchargement de données

XHR permet de transférer des données textuelles et binaires.

Le navigateur offre un encodage et un décodage automatiques pour divers types de données : ArrayBuffer, Blob, Document, JSON et Text.

L'API permet de surcharger explicitement le type des données via responseType.

Téléversement de données

Il suffit de passer les données à la méthode send().

Les données doivent être passées en une fois à send(), pas de streaming (séquence de données mises à disposition au fil du temps).

Les données peuvent être au format : DOMString, Document, FormData, Blob, File, ou ArrayBuffer.

Le navigateur s'occupe de l'encodage, du content-type HTTP approprié et envoie la requête.

Du coup pour envoyer un blob ou un fichier fourni par l'utilisateur via l'API XHR, il suffit d'en passer la référence à la méthode send().

Suivre l'avancement des transferts

L'API fournit un jeu d'évènements pour suivre l'avancement via l'interface ProgressEvent.

Pour estimer la quantité de données transférées, le serveur doit renvoyer le Content-Length dans sa réponse.

Il n'y a pas de timeout par défaut, donc le risque d'une requête infinie existe. Une bonne pratique consiste à toujours fixer un timeout et à gérer l'erreur.

Polling XHR

XHR permet au client d'envoyer des demandes au serveur pour mettre à jour des données. L'inverse est plus compliqué : si les données sont mises à jour côté serveur, comment en informer le client ?

L'une des stratégies les plus basiques consiste à demander au client de faire une vérification périodique.

Une requête XHR est lancée en arrière-plan à intervalles réguliers pour vérifier la disponibilité d'une mise à jour.

Si de nouvelles données sont disponibles, elles sont renvoyées dans la réponse, sinon la réponse est vide.

Cette stratégie est inefficace sauf pour les applications pour lesquelles les nouveaux événements arrivent à un rythme prévisible et où les intervalles peuvent être longs.

Long-Polling XHR

Il s'agit d'une variante permettant d'optimiser la stratégie de polling.

Au lieu de renvoyer une réponse vide, on conserve la connexion inactive (grâce au timeout de Keep-Alive) jusqu'à ce qu'une mise à jour soit disponible.

Cette technique est connue sous différents noms : Comet, reverse AJAX, AJAX push ou HTTP push.

Le long-polling permet d'économiser des requêtes et donc de la latence.

Quand une requête long-polling est terminée, le client n'a plus qu'à en initier une nouvelle.

Mais chaque requête long-polling reste une requête HTTP complète et si la fréquence des mises à jour est élevée, le long-polling émettra plus de requêtes XHR que le polling classique.

C'est une stratégie plus efficace quand la fréquence des mises à jour n'est pas constante.

Cependant il n'existe pas de stratégie idéale pour fournir des mises à jour en temps réel avec XHR. Server-Sent Events et les WebSockets sont de meilleures options.

L'API fetch()

L'API fetch() est une partie du standard Fetch dont la mission est de spécifier toute la sémantique entourant l'obtention d'une ressource à partir d'une URL.

fetch() propose une API plus récente et plus simple à base de promesses JavaScript et règle quelques faiblesses de l'API XMLHttpRequest dont l'incapacité d'interagir avec les objets des requêtes et des réponses, l'incapacité d'émettre une requête no-cors qui peut servir par exemple dans le contexte d'un service worker, l'impossibilité de streamer etc.

Il peut tout de même rester quelques frictions avec fetch() comme l'absence de contrôle de la progression des données uploadées out of the box qui peuvent faire d'XHR un choix justifié :

[fetch()] covers slightly more ground than XMLHttpRequest, although it is currently lacking when it comes to request progression (not response progression).