# 受保护的 PWA：页面还在，连接已断

手机上已经安装的 PWA 能显示标题和旧页面，点击发送却失败。服务端程序仍然健康，桌面浏览器重新登录也未必能让手机恢复。这种表现很容易让人去重启模型、重发消息，实际断开的却可能是入口会话。

2026-07-28，作者维护的一个受保护的 PWA 就遇到了这个问题。普通 Safari 与安装后的 PWA 都显示不可用页面，但请求没有到达源站：Cloudflare Access 正在要求重新登录，service worker 却仍能从缓存提供应用外壳。页面把请求失败解释成后端 provider 不可用，用户也没有一条确实走网络的重连入口。

## 先看三种彼此独立的状态

| 状态 | 控制什么 | 不能从它推断什么 |
|---|---|---|
| Access 会话 | 当前浏览环境是否通过这个应用的入口认证 | 已登录的另一个浏览环境也通过了认证 |
| service worker 与缓存 | 哪一版 service worker 控制页面，哪些资源能从本机取到 | 网络能到达受保护的 API |
| 源站当前版本 | 服务器正在提供哪份应用文件与接口 | 用户设备已经取到了该版本 |

Cloudflare Access 的应用、策略与全局会话有各自的持续时间，登录资格与当前会话也不是同一个期限。[Access session management 官方说明](https://developers.cloudflare.com/cloudflare-one/access-controls/access-settings/session-management/)。在这次事故中，Safari 和 standalone PWA 的会话需要分别验收；这是一项实际设备观察，不能依靠在另一处登录来推断。

service worker 可以拦截请求并从缓存返回资源，新 worker 安装后还可能等待旧页面退出才激活。因此服务器已更新和设备仍显示旧壳可以同时成立。[MDN：Using Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers)。

当时的路径可以画成两条：

```text
打开首页 → service worker → 缓存外壳：能显示
请求 API → Cloudflare Access → 要求认证：尚未到达源站
```

第一条成功，无法证明第二条成功。也不能把页面里的错误文案当作故障所在层的证据。

## PWA 重连：给用户一条真正走网络的路

这个 PWA 的修复增加了一个精确的 `/api/reconnect` 导航入口。它只用于重新连接：service worker 不用首页缓存代替这条请求；Access 可以拦截它并完成认证；通过以后，源站用同源的相对重定向返回首页。这个请求不发送消息、不重放任务，也不修改会话内容。

界面同时把错误改成「私人连接不可用」，提供用户可点击的重连动作。这样用户知道应先恢复入口，不必猜测模型或服务端程序已经停止。

路径的要点是 **真正走网络，而且不产生业务副作用**。单纯刷新一个被 cache-first 策略接管的首页，可能只重新拿到同一份旧页面。重连路径、业务 API 与 Access 的认证路径也不能被 SPA navigation fallback 变成首页 HTML；返回 200 的首页不是成功的 API 响应。

## 旧 worker 也可能挡住修复本身

增加重连按钮后，还需要解决安装设备上的旧版本。那次恢复准备了一份只改变 service worker 的配对恢复版本，在一个精确、临时的脚本路径窗口内让设备取得更新；整个首页继续受保护。随后恢复正常版本、删除临时入口与规则，再确认匿名首页和 worker 脚本都重新要求 Access 认证。

这是一项带设备验收和清理的历史事故处置，不能变成长期配置建议。**不要为了让 PWA 更新方便，永久 Bypass `/sw.js` 或整个应用。** Bypass 的含义就是让符合规则的请求绕过 Access；是否需要公开某个资源，应从该资源本来能否公开来决定，而不能用“页面终于能开了”作为依据。[Cloudflare Access policies 官方说明](https://developers.cloudflare.com/cloudflare-one/access-controls/policies/)。

日常修复应优先使用已经设计好的网络重连与更新路径。若设备仍受旧 worker 控制，先记录当前 controller、waiting worker、脚本版本与网络返回，再决定该设备的恢复方法。删除全部浏览器数据可能丢弃草稿和本机偏好，也会抹去定位旧版本的线索，适合在明确后果后按具体设备操作。

## 这次恢复证明了哪些结果

| 2026-07-28 的观察 | 说明什么 |
|---|---|
| 服务端健康，但网络请求未到达源站 | 故障首先发生在入口与浏览器路径 |
| 重连路径确实经过网络与 Access | 用户有机会更新该环境的入口会话 |
| 实际 iPhone standalone PWA 重新打开后取得在线历史与状态 | 这台安装设备恢复了实际应用读取 |
| 临时恢复规则移除后，匿名首页与脚本重新受到保护 | 恢复没有依赖永久开放入口 |
| 正常 worker 重新获取，业务接口成功响应 | 设备回到正常版本的受保护路径 |

本文根据这份历史故障重构。2026-10-03 编辑时查阅了官方机制，没有重新登录原系统、重做 iPhone 验收或确认今天的浏览器版本；一次恢复也不保证所有设备、未来更新与登出都已覆盖。

## 怎样验证自己的 PWA

使用自己的测试账号和无私人内容的样本页面。保留一个能独立检查源站健康的入口；下面的会话与更新检查放在测试设备或测试应用中进行。

1. 普通浏览器和安装后的 PWA 分别打开，记录它们各自的登录状态；不要把一处成功当作两处成功。
2. 让测试会话过期或退出，再打开旧页面。确认缓存外壳可以显示时，界面仍明确提示连接问题，业务操作没有自动重发。
3. 点击重连，在开发者工具或应用日志中确认请求真的到达网络，并在认证完成后返回同源页面；它不创建业务记录。
4. 发布只改变一个可见版本标记的测试更新，观察新 worker 是否仍 waiting；完成更新后再确认 controller 与标记一致。
5. 分别检查匿名首页、API、worker 脚本，以及登录后的实际读取。若用过任何临时恢复规则，移除后重查这些请求。

如果应用没有 service worker，先检查 Access 会话与普通 HTTP 路径即可。若问题已经到达源站并由业务接口明确拒绝，就沿该接口继续诊断，不要再把所有失败归为缓存。[手册：私人服务的门与钥匙](../guides/handbook.md#04--私人服务的门与钥匙)解释入口与应用权限的分工；[私有状态页](protected-status.md)进一步区分入口健康与源站健康。
