HTTP : Requêtes conditionnelles

HTTP a un concept de requête conditionnelle où le résultat, et même le succés d'une requête, peut être changé en comparant les ressources affectées avec la valeur d'un validateur. De telles requêtes peuvent être utiles pour valider le contenu d'un cache et mettre de côté un contrôle inutile pour vérifier l'intégrité d'un document, comme le sommaire d'un téléchargement, ou éviter de perdre des mises à jour quand on télécharge ou modifie un document sur le serveur.

Principes

Les requêtes conditionnelles HTTP s'exécutent différemment en fonction de la valeur spécifique des en-têtes. Ces en-têtes définissent une condition de départ (pré-condition) et le résultat de la requête sera différent selon que la pré-condition est satisfaite ou non.

Les comportements différents sont définis par la méthode qu'utilise la requête et par un ensemble d'en-têtes propres aux préconditions :

  • Pour une méthode safe comme GET, qui, habituellement va chercher un document, la requête conditionnelle peut renvoyer le document si c'est pertinent seulement. Cela économise de la bande passante.
  • Pour les méthodes unsafe comme {HTTPMethod("PUT")}}, qui charge habituellement un document, la requête conditionnelle peut servir à charger le document, uniquement si l'original sur lequel la requête est basée est le même que celui stocké sur le serveur.

Validateurs

Toutes les en-têtes conditionnelles vérifient si la ressource stockée sur le serveur correspond à une version spécifique. Pour accomplir ceci, la requête conditionnelle doit préciser la version de la ressource car comparer l'ensemble bit à bit n'est pas faisable et pas toujours désiré non plus. La requête transmet une valeur qui caractérise la version. Ces valeurs sont appelées validateurs et il y en a de deux sortes :

  • La date de dernière modification du document, la dernière date modifiée.
  • une chaîne de caractére sans signification particulière identifiant uniquement chaque version. On l'appelle "étiquette d'entité" ou "etag".

Comparer les versions d'une même ressource est un peu délicat : en fonction du contexte, il y a deux sortes de vérification d'équivalence :

  • Une validation forte est utilisée quand une vérification bit à bit est demandé, par exemple pour reprendre un téléchargement.
  • Une validation faible est utilisée quand l'agent-utilisateur doit seulement déterminer si deux ressources ont le même contenu. Ils sont égaux même s'ils ont des différences minimes comme des publicités différentes ou un pied de page (footer) avec une date différente.

La sorte de la vérification est indépendante du validateur utilisé.  Last-Modified et ETag  permettent les deux tupes de validation bien que la complexité d'implémentation côté serveur soit variable.  HTTP se sert de la validation forte par défaut et spécifie quand la validation faible peut être employée.

Validation forte

La validation forte consiste à garantir que la ressource est identique à celle à laquelle elle est comparée, au bit prés. C'est obligatoire pour certaines en-têtes et le défaut pour les autres. La validation forte est stricte et peut être difficile à garantir côté serveur mais cela garantit qu'à aucun moment une donnée n'est perdu, parfois au détriment de la performance.

Il est assez difficile d'avoir un identifiant unique pour la validation forte avec Last-Modified. On le fait souvent en employant une ETag avec le hachage MD5 de la ressource(ou un dérivé).

Validation faible

La validation faible différe de la validation forte car elle considère que deux versions du document ayant le même contenu sont équivalentes. Par exemple, une page qui différerait d'une autre seulement par sa date dans le pied de page ou une publicité, sera considérée identique à l'autre avec la validation faible. Ces mêmes deux versions seront évaluées comme étant différentes avec la validation forte. Construire un système d'ETags pour la validation faible peut être complexe car cela induit de connaître l'importance des différents éléments de la page mais est trés utile dans le but d'optimiser les performances du cache.

En-têtes conditionnelles

Plusieurs en-têtes HTTP, appelées en-têtes conditionelles, apportent des conditions aux reques. Ce sont :

If-Match
Succés si la ETag de la ressource distante est égale à une de celles listées dans cette en-tête. Par défaut, à moins que l'etag soit préfixée 'W/', c'est une validation forte. it performs a strong validation.
If-None-Match
Succés si la ETag de la ressource distante est différente de toutes celles listées dans l'en-tête. Par défaut, à moins que l'etag soit préfixée 'W/', c'est une validation forte.
If-Modified-Since
Succés si la date Last-Modified de la ressource distante est plus récente que celle donnée dans l'en-tête.
If-Unmodified-Since
Succés si la date Last-Modified de la ressource distante est plus ancienne ou égale à celle donnée dans l'en-tête.
If-Range
Similaire à If-Match, ou If-Unmodified-Since, mais peut n'avoir qu'une seule etag, ou une date. Si ça ne colle pas, la requête est rejetée et à la place d'un statut de réponse 206 Partial Content , un 200 OK est envoyé avec la totlité de la ressource.

Cas d'utilisation

Mise à jour du Cache

Le cas d'utilisation le plus commun pour les requêtes conditionnelles est la mise à jour du cache. Avec un cache vide ou absent, la ressource demandée est renvoyée avec un statut 200 OK.

The request issued when the cache is empty triggers the resource to be downloaded, with both validator value sent as headers. The cache is then filled.

Dans la ressource les validateurs sont renvoyés dans les en-têtes. Dans cet exemple, deux validateurs Last-Modified et  ETag sont envoyés mais il pourrait tout aussi bien n'y en avoir qu'un. Ces validateurs sont en cache avec la ressource (comme toutes les en-têtes) et seront utilisés pour embarquer les requêtes conditionnelles quand le cache est périmé.

Tant que le cache n'est pas obsolète, aucune requête n'esst publiée. Mais une fois qu'il est dépassé, il est principalement contrôlé par l'en-tête Cache-Control , le client n'utilise pas directement la valeur en cache mais publie une requête conditionnelle. La valeur du validateur est employé comme paramètre des en-têtes If-Modified-Since et If-Match.

Si la ressource n'a pas changé, le serveur renvoie une réponse 304 Not Modified. Cela rafraîchit le cache et le client peut se servir de la valeur en cache. Bien qu'il y ait un aller-retour requête-réponse qui consomme quelques ressources, cette méthode est plus efficace que de transmettre à nouveau la totalité de la ressource via le réseau.

With a stale cache, the conditional request is sent. The server can determine if the resource changed, and, as in this case, decide not to send it again as it is the same.

Si la ressource n'a pas changée, le serveur renvoie juste une réponse  200 OK avec la nouvelle version de la ressource comme si la requête n'était pas conditionnelle et le client utilise cette nouvelle ressource et la met en cache.

In the case where the resource was changed, it is sent back as if the request wasn't conditional.

De plus, la configuration des validateurs côté serveur est totalement transparente : tous les navigateurs gèrent un cache et envoient de telles requêtes conditionnelles sans que cela ne nécessite de travail supplémentaire de la part du développeur.

Intégrité d'un téléchargement partiel

Un téléchargement partiel de fichiers est une fonctionnalité de HTTP qui permet de reprendre des opérations en cours en économisant de la bande passante et du temps en conservant les données déjà reçues :

A download has been stopped and only partial content has been retrieved.

Un serveur qui supporte le téléchargement partiel le diffuse en envoyant une en-tête Accept-Ranges. Quand il la reçoit, le client peut reprendre le téléchargement en envoyant une en-tête de requête Ranges avec les données manquantes :

The client resumes the requests by indicating the range he needs and preconditions checking the validators of the partially obtained request.

Le principe est simple mais il y a un problème potentiel : si la ressource téléchargée a été modifiée entre deux téléchargements, les données reçues correspondront à deux versions différentes de la ressource et le fichier final sera corrompu. Pour prévenir cela, des en-têtes conditionnelles sont employées.  Il y a deux manières de faire : la plus flexible se sert de If-Modified-Since et de  If-Match, le serveur retourne alors une erreur si la "pré-condition" n'est pas satisfaite et le client reprend le téléchargement depuis le début :

When the partially downloaded resource has been modified, the preconditions will fail and the resource will have to be downloaded again completely.

Même si cette méthode marche, elle ajoute un échange requête/réponse quand le document a été modifié. Cela impacte la performance et HTTP a prévu une en-tête spécifique pour éviter ce scénario : If-Range:

The If-Range headers allows the server to directly send back the complete resource if it has been modified, no need to send a 412 error and wait for the client to re-initiate the download.

Cette solution est plus efficace mais légèrement moins flexible puisqu' une etag seulement peut être utilisée dans la condition. On a rarement besoin d'une telle flexibilité additionnelle.

Èviter les problèmes de perte de mise à jour avec le "verrouillage optimiste"

Une opération commune des applications web est la mise à jour de document distants. C'est trés usuel dans tout système de fichiers ou dans les applications de contrôle de source et toute application qui permet de stocker des ressources distantes a besoin de ce mécanisme. Les sites comme les wikis et autres CMS s'en servent habituellement.

Vous pouvez l'implémenter avec la méthode PUT. Le client lit d'abord les fichiers originaux, les modifie et finalement, les envoie au serveur.

Updating a file with a PUT is very simple when concurrency is not involved.

Cependant, les choses deviennent un peu moins précises dés que l'on parle de simultanéité des comptes. Pendant qu'un client est en train de modifier  localement sa nouvelle copie de la ressource, un second client peut récupérer la même ressource et faire de même avec sa copie. Ce qui arrive ensuite est regrettable : quand ils enregistrent les modifications sur le serveur, celles du premier client sont écartées par l'enregistrement du second client qui n'est pas au courant des changements effectués sur la ressource par le premier client. Le choix qui est fait n'est pas communiqué aux autres protagonistes. Les changements adoptés changeront avec la vitesse d'enregistrement, ce qui dépend de la performance des clients, des serveurs et même de l'humain qui édite le document sur le client. Le "gagnant" changera d'une fois à l'autre. C'est donc une "course des conditions" (race condition) qui conduit à des comportements problématiques difficiles à cerner et à débugger.

When several clients update the same resource in parallel, we are facing a race condition: the slowest win, and the others don't even know they lost. Problematic!

Il n'existe aucune manière de gérer ce problème sans ennuyer l'un ou l'autre client. De toutes façons, les mises à jour perdues et la "course des conditions" sont appelées à disparaître. Nous voulons des résultats prévisibles et être notifiés quand les changements sont rejetés.

Les requêtes conditionnelles permettent d'implémenter l'algorithme de contrôle de concurrence (optimistic locking algorithm) utilisé par la plupart des wikis ou systèmes de contrôle des sources. Le concept est de permettre au client d'avoir des copies de la ressource, les laisser se modifier localement puis de contrôler la mise en concurrence en autorisant celles du premier client soumettant une mise à jour. Toutes les mises à jour ultèrieures basées sur la version maintenant obsolète sont rejetées :

Conditional requests allow to implement optimistic locking: now the quickest wins, and the others get an error.

Ce ci est implémenté par les en-têtes If-Match ou If-Unmodified-Since . Si l'etag ne correspond pas au fichier original ou si le fichier a été modifié depuis son obtention, le changement est alors simplement rejeté avec une erreur 412 Precondition Failed. C'est maintenant à l'initiative du client que se réglera l'erreur : soit en prévenant le client de redémarrer avec la nouvelle version, soit en présentant au client les différences entre les deux versions pour l'aider à choisir les modifications à conserver.

Gérer le premier téléchargement d'une ressource

Le premier téléchargement d'une ressource est un des cas résultant du comportement précédent. Comme toute mise à jour d'une ressource, le téléchargement va faire l'objet d'une "course des conditions" si deux clients essaient un enregistrement au même instant. Pour éviter cela, les en-têtes conditionnelles peuvent être employées : on ajoute If-None-Match avec la valeur particulière '*', représentant n'importe quelle etag. La requête aboutira seulement si la ressource n'existait pas avant :

Like for a regular upload, the first upload of a resource is subject to a race condition: If-None-Match can prevent it.

If-None-Match fonctionnera seulement avec les serveurs compatibles HTTP/1.1 (et postérieur). Si vous n'êtes pas sûr que le serveur le soit, vous devez d'abord envoyer une requête HEAD à la ressource pour vérifier.

Conclusion

Les requêtes conditionnelles sont une fonctionnalité essentielle d'HTTP et permettent la construction d'applications efficaces et complexes. Pour le cache et la reprise des téléchargements, la seule obligation du webmaster est de configurer le serveur correctement, en paramètrant les bonnes etags : dans certains environnements, c'est un véritable défi. Une fois cela fait, le serveur renverra les requêtes conditionnelles adaptées.

Pour verrouiller ces dispositifs, c'est l'inverse : les développeurs web devront publier une requête avec les en-têtes appropriées tandis que les webmasters peuvent en général se fier à l'application pour effectuer ces vérifications.

Dans les deux cas, c'est clair, les requêtes conditionnelles sont une des fonctionnalités essentielles du Web.