Procesamiento condicional de vistas

Los clientes HTTP pueden enviar varios encabezados para informar al servidor sobre copias de un recurso que ya han visto. Esto se utiliza comúnmente cuando se recupera una página web (utilizando una solicitud HTTP GET) para evitar enviar toda la información para algo que el cliente ya ha recuperado. Sin embargo, los mismos encabezados pueden usarse para todos los métodos HTTP (POST, PUT, DELETE, etc.).

Para cada página (respuesta) que Django envía desde una vista, puede proporcionar dos encabezados HTTP: el encabezado ETag y el encabezado Last-Modified. Estos encabezados son opcionales en las respuestas HTTP. Pueden establecerse por la función de vista o pueden confiar en el middleware ConditionalGetMiddleware para establecer el encabezado ETag.

Cuando el cliente solicita nuevamente el mismo recurso, puede enviar un encabezado como If-Modified-Since o If-Unmodified-Since, conteniendo la fecha de la última modificación que se envió, o If-Match o If-None-Match, conteniendo el último ETag que se envió. Si la versión actual de la página coincide con el ETag enviado por el cliente, o si el recurso no ha sido modificado, se puede enviar un código de estado 304 en lugar de una respuesta completa, informando al cliente de que nada ha cambiado. Dependiendo del encabezado, si la página ha sido modificada o no coincide con el ETag enviado por el cliente, se puede devolver un código de estado 412 (Precondición fallida).

Cuando necesitas controlar con mayor precisión puedes utilizar funciones de procesamiento condicional por vista.

El decorador condition

A veces (de hecho, muy a menudo) puedes crear funciones para calcular rápidamente el valor del ETag o la fecha de última modificación para un recurso sin necesitar hacer todos los cálculos necesarios para construir la vista completa. Django puede utilizar estas funciones para proporcionar una opción de «bailout temprano» para el procesamiento de vistas. Informando al cliente de que el contenido no ha sido modificado desde la última solicitud.

Estas dos funciones se pasan como parámetros al decorador django.views.decorators.http.condition(). Este decorador utiliza las dos funciones (solo necesitas proporcionar una, si no puedes calcular fácilmente y rápidamente ambas cantidades) para determinar si los encabezados en la solicitud HTTP coinciden con los del recurso. Si no coinciden, se debe calcular una copia nueva del recurso y se llama a tu vista normal.

La firma del decorador condition tiene el siguiente aspecto:

condition(etag_func=None, last_modified_func=None)

Los textos traducidos manteniendo todas sus etiquetas intactas son:

El decorador establece los encabezados ETag y Last-Modified en la respuesta si no están ya establecidos por la vista y si el método de solicitud es seguro (GET o HEAD).

Usar esta característica de manera útil se explica probablemente mejor con un ejemplo. Supongamos que tienes este par de modelos, representando un pequeño sistema 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 página principal, que muestra las últimas entradas del blog, solo cambia cuando agregas una nueva entrada de blog, puedes calcular el tiempo de modificación muy rápidamente. Necesitas la fecha published más reciente para cada entrada asociada con ese blog. Una forma de hacer esto sería:

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

Puedes utilizar luego esta función para proporcionar detección temprana de una página inmodificada para tu vista de página principal:

from django.views.decorators.http import condition


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

Ten cuidado con el orden de los decoradores

Cuando condition() devuelve una respuesta condicional, cualquier decorador debajo de ella se saltará y no aplicará a la respuesta. Por lo tanto, cualquier decorador que deba aplicarse tanto a la respuesta regular como a una respuesta condicional debe estar por encima de condition(). En particular, vary_on_cookie(), vary_on_headers() y cache_control() deben venir primero porque el RFC 9110 <9110#section-15.4.5> requiere que los encabezados que establecen estén presentes en las respuestas 304.

Atajos para calcular solo un valor

Como regla general, si puedes proporcionar funciones para calcular ambos el ETag y la hora de modificación, deberías hacerlo. No sabes qué encabezados enviarán los clientes HTTP, así que prepárate para manejar ambos. Sin embargo, a veces solo uno de los valores es fácil de calcular y Django proporciona decoradores que manejan solo cálculos de ETag o únicamente la hora de modificación.

Los decoradores django.views.decorators.http.etag y django.views.decorators.http.last_modified se pasan el mismo tipo de funciones que el decorador condition. Sus firmas son:

etag(etag_func)
last_modified(last_modified_func)

Podemos escribir el ejemplo anterior, que solo utiliza una función de última modificación, utilizando uno de estos decoradores:

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

…o:

def front_page(request, blog_id): ...


front_page = last_modified(latest_entry)(front_page)

Utiliza condición cuando se están probando ambas condiciones

Puede parecer más agradable a algunas personas intentar encadenar los decoradores etag y last_modified si deseas probar ambas condiciones previas. Sin embargo, esto conduciría a un comportamiento incorrecto.

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


# End of bad code.

El primer decorador no sabe nada del segundo y podría responder que la respuesta no ha sido modificada aunque el segundo decorador determinaría lo contrario. El decorador condición utiliza las funciones de llamada simultáneamente para determinar la acción correcta a tomar.

Usando los decoradores con otros métodos HTTP

El decorador condición es útil más allá de solo solicitudes GET y HEAD (las solicitudes HEAD son las mismas que GET en esta situación). También se puede utilizar para proporcionar comprobaciones para solicitudes POST, PUT y DELETE. En estas situaciones, la idea no es devolver una respuesta «no modificada», sino informar al cliente de que el recurso que están intentando cambiar ha sido modificado en el meantime.

Por ejemplo, considere el siguiente intercambio entre el cliente y el servidor:

  1. El cliente solicita /foo/.

  2. El servidor responde con algún contenido con un ETag de "abcd1234".

  3. El cliente envía una solicitud HTTP PUT a /foo/ para actualizar el recurso. También envía un encabezado If-Match: "abcd1234" para especificar la versión que está tratando de actualizar.

  4. El servidor verifica si el recurso ha cambiado, calculando el ETag de la misma manera que lo hace para una solicitud GET (utilizando la misma función). Si el recurso ha cambiado, devolverá un código de estado 412, significando «precondición fallida».

  5. El cliente envía una solicitud GET a /foo/, después de recibir una respuesta 412, para recuperar una versión actualizada del contenido antes de actualizarlo.

La cosa importante que muestra este ejemplo es que las mismas funciones se pueden utilizar para calcular el ETag y los valores de última modificación en todas las situaciones. De hecho, deberías usar las mismas funciones, para que se devuelvan los mismos valores cada vez.

Encabezados de validación con métodos de solicitud no seguros

El decorador condition solo establece encabezados de validación (ETag y Last-Modified) para métodos HTTP seguros, es decir, GET y HEAD. Si deseas devolverlos en otros casos, configúralos en tu vista. Consulta RFC 9110 Section 9.3.4 para aprender sobre la distinción entre establecer un encabezado de validación en respuesta a solicitudes realizadas con PUT versus POST.

Comparación con el procesamiento condicional mediante middleware

Django proporciona manejo condicional de solicitudes GET a través de django.middleware.http.ConditionalGetMiddleware. Si bien es adecuado para muchas situaciones, el middleware tiene limitaciones para uso avanzado:

  • Se aplica globalmente a todas las vistas de tu proyecto.

  • No te salva del generación de la respuesta, lo que puede ser costoso.

  • Es solo apropiado para solicitudes HTTP GET.

Debes elegir la herramienta más adecuada para tu problema particular aquí. Si tienes una forma de calcular ETags y tiempos de modificación rápidamente, y si alguna vista tarda en generar el contenido, debes considerar usar el decorador condition descrito en este documento. Si todo ya corre bastante rápido, sigue utilizando el middleware y la cantidad de tráfico de red enviado a los clientes seguirá reduciéndose siempre que la vista no haya cambiado.