From e085333a03646a93b4cd15d38f0607b3adb01423 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Wed, 10 Jun 2026 20:04:33 +0800 Subject: [PATCH] maximize safe Cloudflare edge caching --- docs/CLOUDFLARE_FREE_CACHE_ZH.md | 41 +++++---------- scripts/configure_cloudflare_free.py | 75 ++++++++++++++++++++++------ tests/test_cloudflare_free_config.py | 25 ++++++++-- 3 files changed, 92 insertions(+), 49 deletions(-) diff --git a/docs/CLOUDFLARE_FREE_CACHE_ZH.md b/docs/CLOUDFLARE_FREE_CACHE_ZH.md index 637cdae3..f763e5d1 100644 --- a/docs/CLOUDFLARE_FREE_CACHE_ZH.md +++ b/docs/CLOUDFLARE_FREE_CACHE_ZH.md @@ -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 由最后一条绕过规则排除。 ## 验证 diff --git a/scripts/configure_cloudflare_free.py b/scripts/configure_cloudflare_free.py index d675bf09..519fce59 100644 --- a/scripts/configure_cloudflare_free.py +++ b/scripts/configure_cloudflare_free.py @@ -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", diff --git a/tests/test_cloudflare_free_config.py b/tests/test_cloudflare_free_config.py index a6fbe821..e637c1c4 100644 --- a/tests/test_cloudflare_free_config.py +++ b/tests/test_cloudflare_free_config.py @@ -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