diff --git a/gvsds/apps/gerp/apps/subscription/views.py b/gvsds/apps/gerp/apps/subscription/views.py index 53914751..e1527bb1 100644 --- a/gvsds/apps/gerp/apps/subscription/views.py +++ b/gvsds/apps/gerp/apps/subscription/views.py @@ -45,11 +45,34 @@ from gvsdsdk.model_utils import UUIDObj from gvsds.apps.gerp.app_manager import AppManager from gvsds.apps.gerp.views import ELTAuthenticatedAPIView -from .manager import REMOTE_ASSET_TTL_SECONDS, SubscriptionManager +from .manager import SubscriptionManager from .permissions import IsSubscriptionProvider, SubscriptionProviderAuthentication logger = logging.getLogger(__name__) +#: 远端资源下发给浏览器的缓存策略 +# 「可存但每次校验」= 浏览器每次都带 If-None-Match 回主服务,内容未变即 304: +# - 提供者重建后立即生效(不会像 max-age=24h 那样被浏览器旧副本挡住) +# - 回源(打子服务器)仍受主服务 24h TTL 限制,负载不增加 +_REMOTE_ASSET_CACHE_CONTROL = 'no-cache' + + +def _NormalizeEtag(value): + """归一化 ETag:去掉弱校验前缀 W/ 与两侧引号(RFC 7232 弱比较口径)""" + return (value or '').strip().removeprefix('W/').strip().strip('"') + + +def _EtagMatches(HeaderValue, Etag): + """If-None-Match 是否命中给定 ETag + + RFC 7232:If-None-Match 用**弱比较**(忽略 W/ 前缀与引号);上游/网关可能把 + 我们的强 ETag 改成弱 ETag(实测响应是 W/""),故必须归一化后比较。 + """ + if not HeaderValue: + return False + target = _NormalizeEtag(Etag) + return any(_NormalizeEtag(item) == target for item in HeaderValue.split(',')) + def _ToBytes(value): if value is None: @@ -430,8 +453,8 @@ class SubscriptionRemoteAssetView(APIView): GET /API/Subscription/RemoteAsset// - 缓存:内容落库,TTL 24 小时;过期回源并按内容 SHA256 判断是否变更 - - 浏览器侧同为 24h(`Cache-Control: public, max-age=86400`),并带 ETag(SHA256): - 过期/硬刷新后走条件请求,内容未变即 304,不回源 + - 下发策略 `no-cache` + ETag(SHA256):浏览器每次校验、未变即 304(不回源), + 因此提供者重建后立即生效,而回源频率上限仍是 24h - 免登录:与子服务器原本的公开资源一致(页面渲染前浏览器无法携带业务鉴权) """ @@ -446,15 +469,15 @@ class SubscriptionRemoteAssetView(APIView): err, status=502, content_type='text/plain; charset=utf-8') etag = f'"{payload["SHA256"]}"' - if payload['SHA256'] and request.headers.get('If-None-Match') == etag: + if payload['SHA256'] and _EtagMatches(request.headers.get('If-None-Match'), etag): response = HttpResponse(status=304) response['ETag'] = etag + response['Cache-Control'] = _REMOTE_ASSET_CACHE_CONTROL return response response = HttpResponse(payload['Content'], content_type=payload['ContentType']) response['ETag'] = etag - # 主服务侧与浏览器侧同为 24h(ETag 保留:硬刷新/过期后仍走条件请求,命中即 304) - response['Cache-Control'] = f'public, max-age={REMOTE_ASSET_TTL_SECONDS}' + response['Cache-Control'] = _REMOTE_ASSET_CACHE_CONTROL response['X-Remote-Cache'] = 'HIT' if payload['Hit'] else ('CHANGED' if payload['Changed'] else 'FRESH') return response