发现于 #5238(#5040 E9 文档追平)。纯注释修真,零行为变更;与本 issue 的父单 #5231 同一类纪律(注释必须与实际一致),故挂为其子单。
事实
examples/app-showcase/src/system/apis/index.ts:80,TaskFeedEndpoint.cacheTtl 上方的注释:
// Seconds. Emitted as `Cache-Control: public, max-age=30` on a SUCCESSFUL
// answer only — never on a 401/429/5xx, ...
运行时发的不是 public。packages/runtime/src/endpoint-policy.ts 的 computeCacheControl:
if (!Number.isFinite(ttl) || ttl <= 0) return 'no-store';
return `private, max-age=${Math.floor(ttl)}`;
PR #5230 自己的真实 boot 探针 P1 也打印了实测值:
########## P1 GET /api/v1/apps/showcase/tasks (authed)
HTTP/1.1 200 OK
cache-control: private, max-age=30
注释的后半句(只随成功答案上线、不随 401/429/5xx)是对的,错的只有 public 这一个词。
为什么值得单独记一笔
private 在这条链上不是调优选择而是安全规则,endpoint-policy.ts 的文档块把理由写清楚了:任何一条响应都可能是按调用者 RLS 裁剪过的,所以共享缓存绝不能存下来再发给别人(#5040 §3.3 的 per-principal cache key 在没有服务端缓存时就落在这个 header 上)。
而这条注释所在的位置是 showcase —— 声明式端点唯一的一手示例,是 AI 作者最可能整段抄走的那份文件。抄走 public 的作者会顺理成章地推断「共享缓存可以存这条响应」,而这正好是该规则要挡住的推断。
修法
把 public 改成 private,并把「private 是安全规则不是调优」这半句带上(与 endpoint-policy.ts 的措辞对齐,不复述第二套规则)。顺带可核一下 cacheTtl: 0 的语义(no-store)是否也需要在示例里点一句。
边界
不动运行时,不动声明本身(cacheTtl: 30 是对的),不动 content/docs/releases/。
发现于 #5238(#5040 E9 文档追平)。纯注释修真,零行为变更;与本 issue 的父单 #5231 同一类纪律(注释必须与实际一致),故挂为其子单。
事实
examples/app-showcase/src/system/apis/index.ts:80,TaskFeedEndpoint.cacheTtl上方的注释:运行时发的不是
public。packages/runtime/src/endpoint-policy.ts的computeCacheControl:PR #5230 自己的真实 boot 探针 P1 也打印了实测值:
注释的后半句(只随成功答案上线、不随 401/429/5xx)是对的,错的只有
public这一个词。为什么值得单独记一笔
private在这条链上不是调优选择而是安全规则,endpoint-policy.ts的文档块把理由写清楚了:任何一条响应都可能是按调用者 RLS 裁剪过的,所以共享缓存绝不能存下来再发给别人(#5040 §3.3 的 per-principal cache key 在没有服务端缓存时就落在这个 header 上)。而这条注释所在的位置是 showcase —— 声明式端点唯一的一手示例,是 AI 作者最可能整段抄走的那份文件。抄走
public的作者会顺理成章地推断「共享缓存可以存这条响应」,而这正好是该规则要挡住的推断。修法
把
public改成private,并把「private是安全规则不是调优」这半句带上(与endpoint-policy.ts的措辞对齐,不复述第二套规则)。顺带可核一下cacheTtl: 0的语义(no-store)是否也需要在示例里点一句。边界
不动运行时,不动声明本身(
cacheTtl: 30是对的),不动content/docs/releases/。