Traitement conditionnel de vue

Les clients HTTP peuvent envoyer plusieurs en-têtes pour signaler au serveur des copies de ressources qu’ils ont déjà rencontrées. C’est fréquemment employé lors de l’obtention d’une page Web (par une requête HTTP GET) pour éviter d’envoyer toutes les données quand le client en a déjà récupéré une partie. Cependant, les mêmes en-têtes peuvent être utilisés par toutes les méthodes HTTP (POST, PUT, DELETE, etc.).

Pour chaque page (réponse) que Django renvoie depuis une vue, il est susceptible de définir deux en-têtes HTTP : l’en-tête ETag et l’en-tête Last-Modified. Ces en-têtes sont facultatifs dans les réponses HTTP. Ils peuvent être définis par la fonction de vue ou il est possible de compter sur l’intergiciel ConditionalGetMiddleware pour définir l’en-tête ETag.

Lorsqu’un client demande une nouvelle fois la même ressource, il peut envoyer avec la requête un en-tête tel que If-modified-since ou If-unmodified-since contenant la date de dernière modification qu’il a reçue, ou If-match ou If-none-match contenant la dernière valeur ETag qu’il a reçue. Si la version actuelle de la page correspond à la valeur ETag envoyée par le client ou que la ressource n’a pas été modifiée entre temps, le serveur peut renvoyer un code de statut 304 au lieu d’une réponse complète, indiquant par là au client que rien n’a changé. En fonction de l’en-tête, si la page a été modifiée ou ne correspond pas au contenu ETag envoyé par le client, un code de statut 412 (Precondition Failed) peut être renvoyé.

Dans les cas où vous avez besoin d’un contrôle plus fin, vous pouvez utiliser les fonctions de traitement conditionnel par vue.

Le décorateur condition

Sometimes (in fact, quite often) you can create functions to rapidly compute the ETag value or the last-modified time for a resource, without needing to do all the computations needed to construct the full view. Django can then use these functions to provide an « early bailout » option for the view processing. Telling the client that the content has not been modified since the last request, perhaps.

Ces deux fonctions sont transmises comme paramètres au décorateur django.views.decorators.http.condition. Ce décorateur les exploite (vous pouvez n’en fournir qu’une s’il n’est pas possible de calculer facilement et rapidement les deux éléments) pour savoir si les en-têtes de la requête HTTP correspondent à ceux de la ressource. Lorsque ce n’est pas le cas, une nouvelle copie de la ressource doit être calculée et donc la vue normale est appelée.

La signature du décorateur condition correspond à ceci :

condition(etag_func=None, last_modified_func=None)

The two functions, to compute the ETag and the last modified time, will be passed the incoming request object and the same parameters, in the same order, as the view function they are helping to wrap. The function passed last_modified_func should return a standard datetime value specifying the last time the resource was modified, or None if the resource doesn’t exist. The function passed to the etag decorator should return a string representing the ETag for the resource, or None if it doesn’t exist.

Le décorateur définit les en-têtes ETag et Last-Modified de la réponse s’ils ne sont pas déjà définis par la vue et si la méthode de la requête est sûre (GET ou HEAD).

Il est probablement plus adéquat d’expliquer la façon d’utiliser cette fonctionnalité par un exemple. Admettons que vous ayez ces deux modèles représentant un petit système de blog :

import datetime
from django.db import models

class Blog(models.Model):
    ...

class Entry(models.Model):
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
    published = models.DateTimeField(default=datetime.datetime.now)
    ...

Si la page d’accueil affichant les derniers articles de blog ne change que lorsque vous ajoutez un nouvel article, vous pouvez calculer très rapidement la date de dernière modification. Vous avez besoin de la date published la plus récente de chaque article associé à ce blog. Une façon de le faire serait :

def latest_entry(request, blog_id):
    return Entry.objects.filter(blog=blog_id).latest("published").published

Vous pouvez dès lors utiliser cette fonction pour fournir une détection rapide d’une page d’accueil inchangée :

from django.views.decorators.http import condition

@condition(last_modified_func=latest_entry)
def front_page(request, blog_id):
    ...

Attention à l’ordre des décorateurs

Lorsque condition() renvoie une réponse conditionnelle, tout décorateur qui le suit sera ignoré et ne s’appliquera pas à la réponse. Ainsi, tout décorateur devant s’appliquer à la fois à la réponse de vue normale et à la réponse conditionnelle doit se trouver au-dessus de condition(). En particulier, vary_on_cookie(), vary_on_headers() et cache_control() doivent figurer en premier car la RFC 7232 exige que les en-têtes qu’ils définissent soient présents dans les réponses 304.

Raccourcis pour le calcul d’une seule valeur

En règle générale, si vous pouvez fournir des fonctions à la fois pour le calcul de la valeur ETag et la date de dernière modification, il est bien de le faire. On ne connaît pas à l’avance les en-têtes qu’un client HTTP donné va envoyer, on peut donc s’attendre à devoir gérer les deux cas de figure. Cependant, il peut arriver qu’une seule des deux valeurs soit facile à calculer et Django fournit donc des décorateurs qui ne gèrent que la valeur ETag ou que la date de dernière modification.

Les décorateurs django.views.decorators.http.etag et django.views.decorators.http.last_modified reçoivent le même type de fonction que le décorateur condition. Leur signature est :

etag(etag_func)
last_modified(last_modified_func)

Nous pourrions écrire l’exemple précédent qui n’utilisait que la fonction de dernière modification en choisissant l’un de ces décorateurs :

@last_modified(latest_entry)
def front_page(request, blog_id):
    ...

…ou :

def front_page(request, blog_id):
    ...
front_page = last_modified(latest_entry)(front_page)

Test des deux conditions avec condition

Il pourrait paraître plus élégant pour certaines personnes d’essayer d’enchaîner les décorateurs etag et last_modified pour tester les deux pré-conditions. Cependant, l’effet produit serait faux.

# Bad code. Don't do this!
@etag(etag_func)
@last_modified(last_modified_func)
def my_view(request):
    # ...

# End of bad code.

Le premier décorateur n’a aucune conscience du second et pourrait répondre que la réponse n’a pas été modifiée même quand le second décorateur en déciderait autrement. Le décorateur condition utilise les deux fonctions de rappel de concert pour prendre la bonne décision.

Utilisation des décorateurs avec d’autres méthodes HTTP

Le décorateur condition est utile au-delà des requêtes GET et HEAD (les requêtes HEAD sont similaires aux requêtes GET dans cette optique). Il peut aussi être utilisé pour faire les vérifications nécessaires pour les requêtes POST, PUT et DELETE. Dans ces cas de figure, l’idée n’est pas de renvoyer une réponse « non modifiée », mais d’indiquer au client que la ressource qu’ils tentent de modifier a subi des modifications dans l’intervalle.

Par exemple, considérez l’échange suivant entre un client et un serveur :

  1. Le client demande /foo/.
  2. Le serveur répond avec un contenu dont la valeur ETag vaut "abcd1234".
  3. Le client envoie une requête HTTP PUT vers /foo/ pour mettre à jour la ressource. Il envoie également un en-tête If-Match: "abcd1234" pour indiquer la version qu’il tente de mettre à jour.
  4. Le serveur vérifie si la ressource a changé en calculant la valeur ETag comme il l’a fait pour la requête GET (en utilisant la même fonction). Si la ressource a effectivement changé, il renvoie un code de statut 412 signifiant « Échec de condition préalable » (precondition failed).
  5. Le client envoie une requête GET vers /foo/ après la réception de la réponse 412 afin de récupérer une version mise à jour du contenu avant de le mettre à jour.

La chose importante démontrée par cet exemple est que les mêmes fonctions peuvent être utilisées pour calculer les valeurs ETag et date de dernière modification dans toutes les situations. En fait, vous devez utiliser les mêmes fonctions afin que les mêmes valeurs soient renvoyées à chaque fois.

En-têtes de validation avec les méthodes de requête non sûres

Le décorateur condition ne définit les en-têtes de validation (ETag et Last-Modified) que pour les méthodes HTTP sûres, c’est-à-dire GET et HEAD. SI vous souhaitez les avoir avec d’autres méthodes, vous devez les définir dans la vue. Voir RFC 7231#section-4.3.4 pour connaître la distinction entre la définition d’un en-tête de validation dans les réponses aux requêtes envoyées par PUT en comparaison de POST.

Comparaison avec l’intergiciel de traitement conditionnel

Django fournit une gestion conditionnelle de GET via l’intergiciel django.middleware.http.ConditionalGetMiddleware. Bien qu’il soit adapté à de nombreuses situations, les fonctionnalités de cet intergiciel présentent des limites quant à un usage avancé :

  • Il est appliqué globalement à toutes les vues d’un projet.
  • Il ne vous économise pas le travail de génération de la réponse, ce qui peut être coûteux.
  • Il n’est adapté qu’aux requêtes HTTP GET.

Vous devriez choisir ici l’outil le plus approprié à votre problème particulier. Si vous arrivez à calculer rapidement les valeurs ETag et de date de dernière modification et que certaines vues prennent du temps à produire leur contenu, vous devriez envisager d’utiliser le décorateur condition présenté dans ce document. Par contre, si tout fonctionne déjà de manière assez fluide, restez-en à la solution des intergiciels et vous continuerez de réduire la quantité de trafic réseau en direction des clients dans les cas où les contenus des vues n’ont pas changé.

Back to Top