Обработка условных представлений

Клиенты HTTP могут отправлять ряд заголовков, чтобы сообщить серверу о копиях ресурса, которые они уже видели. Это обычно используется при получении веб-страницы (с помощью HTTP-запроса GET), чтобы избежать отправки всех данных для того, что клиент уже получил. Однако одни и те же заголовки могут использоваться для всех методов HTTP (POST, PUT, DELETE и т.д.).

Для каждой страницы (ответа), которую Django отправляет обратно из представления, он может предоставить два HTTP-заголовка: заголовок ETag и заголовок Last-Modified. Эти заголовки являются необязательными для HTTP-ответов. Они могут быть установлены вашей функцией представления, или вы можете положиться на промежуточное программное обеспечение ConditionalGetMiddleware для установки заголовка ETag.

Когда клиент в следующий раз запрашивает тот же ресурс, он может послать заголовок типа If-modified-since или If-unmodified-since, содержащий дату последней модификации, или If-match или If-none-match, содержащий последний ETag, который был отправлен. Если текущая версия страницы совпадает с ETag, отправленной клиентом, или если ресурс не был изменен, вместо полного ответа может быть отправлен код состояния 304, сообщающий клиенту, что ничего не изменилось. В зависимости от заголовка, если страница была изменена или не соответствует ETag, отправленному клиентом, может быть возвращен код состояния 412 (Предварительное условие не выполнено).

Если вам нужен более тонкий контроль, вы можете использовать функции условной обработки для каждого вида.

Декоратор condition

Иногда (на самом деле, довольно часто) вы можете создавать функции для быстрого вычисления значения ETag или времени последнего изменения для ресурса, **без необходимости выполнять все вычисления, необходимые для построения полного представления. Затем Django может использовать эти функции для обеспечения «раннего выхода из игры» при обработке представления. Возможно, сообщая клиенту, что содержимое не было изменено с момента последнего запроса.

Эти две функции передаются в качестве параметров декоратору django.views.decorators.http.condition. Этот декоратор использует эти две функции (вам нужно передать только одну, если вы не можете легко и быстро вычислить оба значения), чтобы определить, совпадают ли заголовки в HTTP-запросе с заголовками ресурса. Если они не совпадают, должна быть вычислена новая копия ресурса и вызвано ваше обычное представление.

Сигнатура декоратора condition выглядит следующим образом:

condition(etag_func=None, last_modified_func=None)

Двум функциям, для вычисления ETag и времени последнего изменения, будет передан входящий объект request и те же параметры, в том же порядке, что и функция представления, которую они помогают обернуть. Функция, переданная last_modified_func, должна вернуть стандартное значение времени, указывающее последний раз, когда ресурс был изменен, или None, если ресурс не существует. Функция, передаваемая декоратору etag, должна возвращать строку, представляющую ETag для ресурса, или None, если он не существует.

Декоратор устанавливает заголовки ETag и Last-Modified в ответ, если они еще не установлены представлением и если метод запроса безопасен (GET или HEAD).

Полезное использование этой функции, вероятно, лучше всего объяснить на примере. Предположим, у вас есть пара моделей, представляющих небольшую систему блогов:

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)
    ...

Если главная страница, на которой отображаются последние записи блога, меняется только при добавлении новой записи, то время последнего изменения можно вычислить очень быстро. Вам нужна последняя дата published для каждой записи, связанной с этим блогом. Один из способов сделать это может быть следующим:

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

Вы можете использовать эту функцию для раннего обнаружения неизмененной страницы для просмотра на первой странице:

from django.views.decorators.http import condition

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

Будьте осторожны с заказом декораторов

Когда condition() возвращает условный ответ, все декораторы, расположенные ниже него, будут пропущены и не будут применяться к ответу. Поэтому все декораторы, которые должны применяться как к обычному ответу представления, так и к условному ответу, должны находиться выше condition(). В частности, vary_on_cookie(), vary_on_headers() и cache_control() должны быть на первом месте, потому что RFC 7232 требует, чтобы заданные ими заголовки присутствовали в 304 ответах.

Ярлыки для вычисления только одного значения

Как правило, если вы можете предоставить функции для вычисления и ETag, и времени последнего изменения, вы должны сделать это. Вы не знаете, какие заголовки отправит вам тот или иной HTTP-клиент, поэтому будьте готовы обрабатывать оба. Однако иногда легко вычислить только одно значение, и Django предоставляет декораторы, которые обрабатывают только вычисления ETag или только last-modified.

Декораторам django.views.decorators.http.etag и django.views.decorators.http.last_modified передаются функции того же типа, что и декоратору condition. Их сигнатуры следующие:

etag(etag_func)
last_modified(last_modified_func)

Мы могли бы написать предыдущий пример, в котором используется только функция last-modified, используя один из этих декораторов:

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

…или:

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

Используйте condition при проверке обоих условий

Для некоторых людей может показаться более удобной попытка выстроить цепочку декораторов etag и last_modified, если вы хотите проверить оба предусловия. Однако это приведет к некорректному поведению.

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

# End of bad code.

Первый декоратор ничего не знает о втором и может ответить, что ответ не изменен, даже если второй декоратор определит обратное. Декоратор condition использует обе функции обратного вызова одновременно, чтобы выработать правильное действие.

Использование декораторов с другими методами HTTP

Декоратор condition полезен не только для запросов GET и HEAD (запросы HEAD в этой ситуации то же самое, что и GET). Его также можно использовать для обеспечения проверки запросов POST, PUT и DELETE. В этих ситуациях идея заключается не в том, чтобы вернуть ответ «не изменено», а в том, чтобы сообщить клиенту, что ресурс, который он пытается изменить, был изменен за прошедшее время.

Например, рассмотрим следующий обмен между клиентом и сервером:

  1. Клиент запрашивает /foo/.
  2. Сервер отвечает некоторым содержимым с ETag "abcd1234".
  3. Клиент отправляет HTTP PUT запрос на /foo/ для обновления ресурса. Он также посылает заголовок If-Match: "abcd1234", чтобы указать версию, которую он пытается обновить.
  4. Сервер проверяет, не изменился ли ресурс, вычисляя ETag так же, как это делается для запроса GET (используя ту же функцию). Если ресурс изменился, он вернет код состояния 412, означающий «предварительное условие не выполнено».
  5. Клиент посылает запрос GET на /foo/, получив ответ 412, чтобы получить обновленную версию содержимого перед его обновлением.

Этот пример показывает, что для вычисления значений ETag и последней модификации во всех ситуациях можно использовать одни и те же функции. На самом деле, вы должны использовать одни и те же функции, чтобы каждый раз возвращались одни и те же значения.

Заголовки валидатора с небезопасными методами запроса

Декоратор condition устанавливает заголовки валидатора (ETag и Last-Modified) только для безопасных методов HTTP, т.е. GET и HEAD. Если вы хотите возвращать их в других случаях, установите их в своем представлении. Смотрите RFC 7231#section-4.3.4, чтобы узнать о различии между установкой заголовка валидатора в ответ на запросы, сделанные с помощью PUT и POST.

Сравнение с условной обработкой промежуточного программного обеспечения

Django обеспечивает условную обработку GET через django.middleware.http.ConditionalGetMiddleware. Несмотря на то, что эта промежуточная программа подходит для многих ситуаций, она имеет ограничения для расширенного использования:

  • Он применяется глобально ко всем представлениям в вашем проекте.
  • Это не избавит вас от необходимости генерировать ответ, что может стоить дорого.
  • Он подходит только для HTTP GET запросов.

Здесь вы должны выбрать наиболее подходящий инструмент для решения вашей конкретной задачи. Если у вас есть способ быстро вычислить ETags и время модификации, и если для создания содержимого какого-либо представления требуется некоторое время, вам следует рассмотреть возможность использования декоратора condition, описанного в этом документе. Если все и так работает достаточно быстро, придерживайтесь использования промежуточного программного обеспечения, и количество сетевого трафика, отправляемого обратно клиентам, все равно будет уменьшено, если представление не изменилось.

Вернуться на верх