diff --git a/CLAUDE.md b/CLAUDE.md index 8ba44d69..ea28f167 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2795,6 +2795,78 @@ if (cachedResponse) { - 在返回前验证 `blob.size > 0` 可以自动修复历史问题 - 删除无效缓存后重新获取,确保用户看到正确的内容 +### CDN 响应必须多重验证后才能缓存 + +**场景**: Service Worker 从 CDN 获取静态资源并缓存时 + +❌ **错误示例**: +```typescript +// 错误:只检查 response.ok,可能缓存 CDN 返回的 HTML 错误页面 +const response = await fetch(cdnUrl); +if (response.ok) { + cache.put(request, response.clone()); + return response; // 可能是 404 页面被当作 JS 执行! +} +``` + +✅ **正确示例**: +```typescript +const response = await fetch(cdnUrl); +if (response.ok) { + // 1. Content-Type 验证 + const contentType = response.headers.get('Content-Type') || ''; + const isValidType = contentType.includes('javascript') || + contentType.includes('css') || + contentType.includes('json'); + if (!isValidType) continue; // 尝试下一个源 + + // 2. Content-Length 验证(排除空响应) + const length = parseInt(response.headers.get('Content-Length') || '0', 10); + if (length > 0 && length < 50) continue; + + // 3. 内容采样验证(检测 HTML 错误页面) + const sample = await response.clone().text().then(t => t.slice(0, 200)); + if (sample.includes(' - + diff --git a/apps/web/public/version.json b/apps/web/public/version.json index db908c78..f25d2cf6 100644 --- a/apps/web/public/version.json +++ b/apps/web/public/version.json @@ -1,6 +1,6 @@ { "version": "0.5.25", - "buildTime": "2026-01-20T05:11:17.776Z", + "buildTime": "2026-01-20T05:11:18.087Z", "gitCommit": "unknown", "changelog": [] } \ No newline at end of file diff --git a/apps/web/src/sw/cdn-fallback.ts b/apps/web/src/sw/cdn-fallback.ts index 668b6fe2..a3fd0610 100644 --- a/apps/web/src/sw/cdn-fallback.ts +++ b/apps/web/src/sw/cdn-fallback.ts @@ -234,9 +234,13 @@ export async function fetchFromCDNWithFallback( const response = await fetchWithTimeout(url); if (response.ok) { - // 验证响应类型,防止把 404 页面当作 JS 执行 + // ============================================ + // 多重验证:确保 CDN 返回的是有效资源 + // ============================================ + + // 1. Content-Type 验证 const contentType = response.headers.get('Content-Type') || ''; - const isValidResponse = + const isValidContentType = contentType.includes('javascript') || contentType.includes('css') || contentType.includes('json') || @@ -245,10 +249,38 @@ export async function fetchFromCDNWithFallback( contentType.includes('woff') || contentType.includes('application/octet-stream'); - if (!isValidResponse) { - console.warn(`[CDN Fallback] ${cdn.name} returned invalid Content-Type: ${contentType}`); + if (!isValidContentType) { + console.warn(`[CDN Fallback] ${cdn.name} invalid Content-Type: ${contentType}`); markCDNFailure(cdn.name); - continue; // 尝试下一个 CDN + continue; + } + + // 2. Content-Length 验证(JS/CSS 文件不应该太小) + const contentLength = parseInt(response.headers.get('Content-Length') || '0', 10); + const isTextResource = contentType.includes('javascript') || contentType.includes('css') || contentType.includes('json'); + if (isTextResource && contentLength > 0 && contentLength < 50) { + console.warn(`[CDN Fallback] ${cdn.name} response too small: ${contentLength} bytes`); + markCDNFailure(cdn.name); + continue; + } + + // 3. 内容采样验证(检测 HTML 错误页面) + const clonedResponse = response.clone(); + try { + const textSample = await clonedResponse.text().then(t => t.slice(0, 200)); + const looksLikeHtml = textSample.includes(' { // Listen for custom event from main.tsx const handleUpdateAvailable = async (event: Event) => { const customEvent = event as CustomEvent; - // console.log('[VersionUpdatePrompt] Update available:', customEvent.detail); - const version = customEvent.detail?.version; + const newVersion = customEvent.detail?.version; + + // 获取当前运行的版本(从 HTML meta 标签) + const currentVersionMeta = document.querySelector('meta[name="app-version"]'); + const currentVersion = currentVersionMeta?.getAttribute('content'); try { // Fetch detailed version info (changelog) const res = await fetch(`/version.json?t=${Date.now()}`); if (res.ok) { const data = await res.json(); + + // 如果当前版本已经是最新版本,不显示更新提示 + if (currentVersion && data.version === currentVersion) { + // console.log('[VersionUpdatePrompt] Already on latest version, skipping prompt'); + return; + } + // Use fetched data if versions match or if event didn't specify version - if (!version || data.version === version) { + if (!newVersion || data.version === newVersion) { setUpdateAvailable(data); return; } @@ -35,6 +45,11 @@ export const VersionUpdatePrompt: React.FC = () => { console.warn('Failed to fetch version.json:', error); } + // 如果无法获取 version.json,但事件带了版本号,检查是否相同 + if (currentVersion && newVersion && currentVersion === newVersion) { + return; // 已经是最新版本 + } + // Fallback to event detail setUpdateAvailable(customEvent.detail); };