Django 条件视图处理装饰器添加陈旧的 Etag

Django conditional view processing decorator adds stale Etag

我一直在尝试使用 Django 条件视图处理功能。基本上我想拒绝对一个实体的更新操作,如果它已经被另一个用户修改,并且这似乎与 Django 提供的 @condition 装饰器一起工作。

然而,我在测试它时注意到了一个问题,后来我检查了 Django 源代码,发现我认为可能是一个错误,但我只想在向 Django 提交错误报告之前先在这里确认一下,然后修复。

装饰器在新请求到来时被调用,它首先根据传入装饰器的函数计算 ETag 和 Last Modified 时间戳,然后将控制权交给 get_conditional_response() 函数。此处将执行 ETag 和 Last Modified 验证,如果它们与请求中提供的内容不匹配,请求将被拒绝。到目前为止一切顺利。

如果检查通过,则允许请求并调用视图来处理请求并生成响应。在处理请求时,如果它是不安全的方法,例如PUTPATCH,它会更新实体,这很可能会更改 ETag 和 Last Modified 值。

但是,我注意到对 PUTPATCH 的成功响应被发回,并在更新 之前计算出 ETag 或 Last Modified 时间戳实际执行,现在这些值无效或过时。这对我来说似乎是错误的。在同一实体上执行新的 GET 然后在响应中为用户提供更新的 ETag 和 Last Modified 值。

你不觉得,condition()装饰器应该检查请求方法是否不安全,然后它应该在视图处理后重新计算 ETag 和 Last Modified,然后添加新值回应?

我同意这里有一个错误,尽管我认为它与您描述的有点不同。

条件请求在 RFC 7232, but unfortunately that document is not very explicit about exactly when the conditional headers should be used in a response. It does say:

中定义

2.4. When to Use Entity-Tags and Last-Modified Dates

In 200 (OK) responses to GET or HEAD, an origin server...

这可能会让人认为 headers 的使用未在其他回复中定义。

然而,RFC 7231 explicitly allows for the use of ETags in the response to a PUT, matching the new representation (as was your intuition). However, note this caveat:

An origin server MUST NOT send a validator header field (Section 7.2), such as an ETag or Last-Modified field, in a successful response to PUT unless the request's representation data was saved without any transformation applied to the body...

也就是说,客户端将使用 ETag 的存在与否来确定它的表示(它刚刚作为 body 发送到 PUT)是否是实际存储的. (有关这一点的更多详细信息,请参阅 this question。)

然而,Django 的条件请求 API 不允许进行这种区分。具体来说,如果没有“将转换应用到 body”,用户无法指示视图是否保存了表示。所以 condition() 装饰器无法知道是否需要添加 ETag。

所以在这种情况下,唯一要做的就是保守,而不是 return 有条件 headers。随意创建一个工单(否则我可以做)。

创建自定义中间件来处理 GET/HEAD 请求中的 etag。 下面的代码(Django 1.10)展示了如何使用中间件创建和处理etag。

注意:不要在设置文件中启用USE_ETAGS

from django.utils.cache import get_conditional_response, set_response_etag
from django.utils.http import unquote_etag


class ETag(object):
    def __init__(self, get_response):
        self.get_response = get_response
        # One-time configuration and initialization.

    def __call__(self, request):
        # before view

        response = self.get_response(request)

        # after view
        try:
            if request.method in ('GET', 'HEAD'):
                if not response.has_header('ETag'):
                    set_response_etag(response)
                etag = response.get('ETag')
                return get_conditional_response(
                    request,
                    etag=unquote_etag(etag),
                    last_modified=None,
                    response=response,
                )
        except Exception, e:
            pass

        return response

我正在使用 Django 1.10。如果您使用的是较低版本,则使用 __call__ 方法中实现的逻辑覆盖 process_response(self, request, response) 方法。并且不要忘记将其添加到设置文件

中的 MIDDLEWARE/MIDDLEWARE_CLASSES
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',


    # myapp contains middleware.py file and 
    # ETag class is implemented inside the middleware.py file 
    'myapp.middleware.Etag',
]