maximize safe Cloudflare edge caching
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
目标:让 Cloudflare 承担公开页面、公开 API 和静态资源的重复读取,降低 VPS CPU、Next.js worker 和后端 detail-batch 压力,同时避免缓存账户、支付、反馈和实时连接。
|
||||
|
||||
代码已通过 `Cache-Control` 与 `Cloudflare-CDN-Cache-Control` 声明 TTL。Cloudflare Dashboard 应设置为尊重源站缓存头,不要给所有 `/api/*` 统一启用 Cache Everything。
|
||||
代码已通过 `Cache-Control` 与 `Cloudflare-CDN-Cache-Control` 声明 TTL。由于免费版会严格遵守源站的 `max-age=0`,稳定公开路径的 Cache Rules 额外按状态码覆盖 Edge TTL;可能返回 partial、stale 或 failed 的接口继续尊重源站缓存头。不要给所有 `/api/*` 统一启用 Cache Everything。
|
||||
|
||||
缓存规则只允许作用于前端域名 `polyweather.top`。`api.polyweather.top` 是带服务令牌和会员校验的后端源站,必须绕过 Cloudflare 缓存,避免缓存命中绕过后端权限检查。
|
||||
|
||||
@@ -36,30 +36,13 @@ python scripts/configure_cloudflare_free.py --apply
|
||||
|
||||
### 1. 缓存公开内容
|
||||
|
||||
动作:Eligible for cache;Edge TTL 使用源站 Cache-Control。
|
||||
脚本会创建 5 条互不重叠的公开缓存规则。动作均为 Eligible for cache:
|
||||
|
||||
把下面的公开页面、静态资源和公开数据接口合并成一条规则即可:
|
||||
|
||||
```text
|
||||
http.host eq "polyweather.top"
|
||||
and http.request.method in {"GET" "HEAD"}
|
||||
and (
|
||||
http.request.uri.path eq "/"
|
||||
or starts_with(http.request.uri.path, "/_next/static/")
|
||||
or starts_with(http.request.uri.path, "/docs/")
|
||||
or starts_with(http.request.uri.path, "/modern/")
|
||||
or starts_with(http.request.uri.path, "/probabilities/")
|
||||
or starts_with(http.request.uri.path, "/subscription-help/")
|
||||
or http.request.uri.path eq "/api/cities"
|
||||
or http.request.uri.path eq "/api/cities/detail-batch"
|
||||
or starts_with(http.request.uri.path, "/api/city/")
|
||||
or http.request.uri.path eq "/api/scan/terminal"
|
||||
or http.request.uri.path eq "/api/system/status"
|
||||
or lower(http.request.uri.path.extension) in {
|
||||
"js" "css" "woff" "woff2" "png" "jpg" "jpeg" "webp" "avif" "svg" "ico"
|
||||
}
|
||||
)
|
||||
```
|
||||
- 静态资源:2xx Edge TTL 覆盖为 1 年。
|
||||
- 首页和公开文档页:2xx Edge TTL 覆盖为 10 分钟。
|
||||
- `/api/cities`:2xx Edge TTL 覆盖为 5 分钟。
|
||||
- 城市详情与 detail-batch:尊重源站成功、partial、stale 和错误缓存策略。
|
||||
- `/api/scan/terminal` 与 `/api/system/status`:尊重源站成功、busy、failed 和错误缓存策略。
|
||||
|
||||
### 2. 最后绕过后端域名、动态和敏感请求
|
||||
|
||||
@@ -94,7 +77,7 @@ or http.cookie contains "sb-"
|
||||
|
||||
### 静态资源
|
||||
|
||||
动作:Eligible for cache;Edge TTL 使用源站 Cache-Control。
|
||||
动作:Eligible for cache;2xx Edge TTL 覆盖为一年。
|
||||
|
||||
表达式:
|
||||
|
||||
@@ -108,11 +91,11 @@ and (
|
||||
)
|
||||
```
|
||||
|
||||
源站 TTL:一年 immutable。
|
||||
源站仍声明一年 immutable,Cloudflare Edge TTL 同样固定为一年。
|
||||
|
||||
### 公开页面
|
||||
|
||||
动作:Eligible for cache;Edge TTL 使用源站 Cache-Control。
|
||||
动作:Eligible for cache;2xx Edge TTL 覆盖为 10 分钟。
|
||||
|
||||
表达式:
|
||||
|
||||
@@ -131,7 +114,7 @@ and (
|
||||
|
||||
### 公开数据接口
|
||||
|
||||
动作:Eligible for cache;Edge TTL 使用源站 Cache-Control。
|
||||
动作:Eligible for cache;城市列表的 2xx Edge TTL 覆盖为 5 分钟,详情、扫描和系统状态尊重源站缓存头。
|
||||
|
||||
表达式:
|
||||
|
||||
@@ -151,7 +134,7 @@ and (
|
||||
- `/api/cities`:5 分钟,过期后可复用 1 小时。
|
||||
- 城市详情与 detail-batch:60 秒,过期后可复用 5 分钟。
|
||||
- `/api/scan/terminal`:5 分钟,过期后可复用 15 分钟。
|
||||
- partial、busy、timeout、stale、force refresh 和错误响应:`no-store`,不得缓存。
|
||||
- partial、busy、timeout、stale、force refresh 和错误响应:源站为 `no-store`,详情和扫描规则不会覆盖;force refresh 由最后一条绕过规则排除。
|
||||
|
||||
## 验证
|
||||
|
||||
|
||||
@@ -22,24 +22,50 @@ API_BASE = "https://api.cloudflare.com/client/v4"
|
||||
MANAGED_RULE_REF_PREFIX = "polyweather_free_cache_"
|
||||
PHASE = "http_request_cache_settings"
|
||||
|
||||
PUBLIC_CACHE_EXPRESSION = """(
|
||||
STATIC_CACHE_EXPRESSION = """(
|
||||
http.host eq "polyweather.top"
|
||||
and http.request.method in {"GET" "HEAD"}
|
||||
and (
|
||||
starts_with(http.request.uri.path, "/_next/static/")
|
||||
or lower(http.request.uri.path.extension) in {
|
||||
"js" "css" "woff" "woff2" "png" "jpg" "jpeg" "webp" "avif" "svg" "ico"
|
||||
}
|
||||
)
|
||||
)"""
|
||||
|
||||
PUBLIC_PAGES_EXPRESSION = """(
|
||||
http.host eq "polyweather.top"
|
||||
and http.request.method in {"GET" "HEAD"}
|
||||
and (
|
||||
http.request.uri.path eq "/"
|
||||
or starts_with(http.request.uri.path, "/_next/static/")
|
||||
or starts_with(http.request.uri.path, "/docs/")
|
||||
or starts_with(http.request.uri.path, "/modern/")
|
||||
or starts_with(http.request.uri.path, "/probabilities/")
|
||||
or starts_with(http.request.uri.path, "/subscription-help/")
|
||||
or http.request.uri.path eq "/api/cities"
|
||||
or http.request.uri.path eq "/api/cities/detail-batch"
|
||||
)
|
||||
)"""
|
||||
|
||||
CITIES_EXPRESSION = """(
|
||||
http.host eq "polyweather.top"
|
||||
and http.request.method in {"GET" "HEAD"}
|
||||
and http.request.uri.path eq "/api/cities"
|
||||
)"""
|
||||
|
||||
CITY_DETAIL_EXPRESSION = """(
|
||||
http.host eq "polyweather.top"
|
||||
and http.request.method in {"GET" "HEAD"}
|
||||
and (
|
||||
http.request.uri.path eq "/api/cities/detail-batch"
|
||||
or starts_with(http.request.uri.path, "/api/city/")
|
||||
or http.request.uri.path eq "/api/scan/terminal"
|
||||
)
|
||||
)"""
|
||||
|
||||
SCAN_EXPRESSION = """(
|
||||
http.host eq "polyweather.top"
|
||||
and http.request.method in {"GET" "HEAD"}
|
||||
and (
|
||||
http.request.uri.path eq "/api/scan/terminal"
|
||||
or http.request.uri.path eq "/api/system/status"
|
||||
or lower(http.request.uri.path.extension) in {
|
||||
"js" "css" "woff" "woff2" "png" "jpg" "jpeg" "webp" "avif" "svg" "ico"
|
||||
}
|
||||
)
|
||||
)"""
|
||||
|
||||
@@ -80,16 +106,33 @@ def _compact_expression(expression: str) -> str:
|
||||
return " ".join(line.strip() for line in expression.splitlines() if line.strip())
|
||||
|
||||
|
||||
def _cache_rule(ref: str, description: str, expression: str, ttl: int | None = None) -> Dict[str, Any]:
|
||||
action_parameters: Dict[str, Any] = {"cache": True}
|
||||
if ttl is not None:
|
||||
action_parameters["edge_ttl"] = {
|
||||
"mode": "respect_origin",
|
||||
"status_code_ttl": [
|
||||
{"status_code_range": {"from": 200, "to": 299}, "value": ttl},
|
||||
{"status_code_range": {"from": 300}, "value": 0},
|
||||
],
|
||||
}
|
||||
return {
|
||||
"ref": f"{MANAGED_RULE_REF_PREFIX}{ref}",
|
||||
"description": description,
|
||||
"expression": _compact_expression(expression),
|
||||
"action": "set_cache_settings",
|
||||
"action_parameters": action_parameters,
|
||||
"enabled": True,
|
||||
}
|
||||
|
||||
|
||||
def build_managed_cache_rules() -> List[Dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"ref": f"{MANAGED_RULE_REF_PREFIX}public",
|
||||
"description": "PolyWeather: cache public pages, assets, and public data APIs",
|
||||
"expression": _compact_expression(PUBLIC_CACHE_EXPRESSION),
|
||||
"action": "set_cache_settings",
|
||||
"action_parameters": {"cache": True},
|
||||
"enabled": True,
|
||||
},
|
||||
_cache_rule("static", "PolyWeather: cache immutable static assets for one year", STATIC_CACHE_EXPRESSION, 31536000),
|
||||
_cache_rule("pages", "PolyWeather: cache public pages for ten minutes", PUBLIC_PAGES_EXPRESSION, 600),
|
||||
_cache_rule("cities", "PolyWeather: cache the public city list for five minutes", CITIES_EXPRESSION, 300),
|
||||
_cache_rule("city_detail", "PolyWeather: cache public city detail when the origin allows it", CITY_DETAIL_EXPRESSION),
|
||||
_cache_rule("scan", "PolyWeather: cache scan and system status when the origin allows it", SCAN_EXPRESSION),
|
||||
{
|
||||
"ref": f"{MANAGED_RULE_REF_PREFIX}bypass",
|
||||
"description": "PolyWeather: bypass backend, sensitive, realtime, and force-refresh requests",
|
||||
|
||||
@@ -5,14 +5,27 @@ from scripts.configure_cloudflare_free import (
|
||||
)
|
||||
|
||||
|
||||
def test_cloudflare_managed_rules_cache_public_content_then_bypass_sensitive_requests():
|
||||
def test_cloudflare_managed_rules_apply_path_specific_edge_ttls_then_bypass_sensitive_requests():
|
||||
rules = build_managed_cache_rules()
|
||||
|
||||
assert [rule["ref"] for rule in rules] == [
|
||||
f"{MANAGED_RULE_REF_PREFIX}public",
|
||||
f"{MANAGED_RULE_REF_PREFIX}static",
|
||||
f"{MANAGED_RULE_REF_PREFIX}pages",
|
||||
f"{MANAGED_RULE_REF_PREFIX}cities",
|
||||
f"{MANAGED_RULE_REF_PREFIX}city_detail",
|
||||
f"{MANAGED_RULE_REF_PREFIX}scan",
|
||||
f"{MANAGED_RULE_REF_PREFIX}bypass",
|
||||
]
|
||||
assert rules[0]["action_parameters"]["cache"] is True
|
||||
assert [
|
||||
rule["action_parameters"]["edge_ttl"]["status_code_ttl"][0]["value"]
|
||||
for rule in rules[:3]
|
||||
] == [31536000, 600, 300]
|
||||
for rule in rules[:3]:
|
||||
assert rule["action_parameters"]["cache"] is True
|
||||
assert rule["action_parameters"]["edge_ttl"]["mode"] == "respect_origin"
|
||||
assert rule["action_parameters"]["edge_ttl"]["status_code_ttl"][1]["value"] == 0
|
||||
for rule in rules[3:-1]:
|
||||
assert rule["action_parameters"] == {"cache": True}
|
||||
assert rules[-1]["action_parameters"]["cache"] is False
|
||||
assert 'http.host eq "api.polyweather.top"' in rules[-1]["expression"]
|
||||
assert 'http.request.uri.query contains "force_refresh=true"' in rules[-1]["expression"]
|
||||
@@ -42,7 +55,11 @@ def test_cloudflare_rule_merge_preserves_unmanaged_rules_and_puts_bypass_last():
|
||||
|
||||
assert [rule["ref"] for rule in merged] == [
|
||||
"existing_rule",
|
||||
f"{MANAGED_RULE_REF_PREFIX}public",
|
||||
f"{MANAGED_RULE_REF_PREFIX}static",
|
||||
f"{MANAGED_RULE_REF_PREFIX}pages",
|
||||
f"{MANAGED_RULE_REF_PREFIX}cities",
|
||||
f"{MANAGED_RULE_REF_PREFIX}city_detail",
|
||||
f"{MANAGED_RULE_REF_PREFIX}scan",
|
||||
f"{MANAGED_RULE_REF_PREFIX}bypass",
|
||||
]
|
||||
assert merged[-1]["action_parameters"]["cache"] is False
|
||||
|
||||
Reference in New Issue
Block a user