Skip to content

fix(scripts): shadcn-sync 不再把 403/非 JSON 错误响应当成注册表数据缓存 - #3496

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-5803-shadcn-fetch-cache
Aug 6, 2026
Merged

fix(scripts): shadcn-sync 不再把 403/非 JSON 错误响应当成注册表数据缓存#3496
yinlianghui merged 2 commits into
mainfrom
claude/issue-5803-shadcn-fetch-cache

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Fixes objectstack-ai/objectstack#5803

问题(已在本地复现,与 issue 描述一致)

scripts/shadcn-sync.jsfetchUrl 从不检查 res.statusCode,而 JSON.parse 失败的响应体被 catch 后当成功 resolve。于是 egress 拦截返回的 403 Forbidden + text/plain 正文,被当作组件数据一路流到 fetchRegistry——后者又无条件写盘(TTL 1h)。一次网络抖动就毒化了接下来一小时的所有 --check

复现(修复前,origin/main,沙箱 egress 拦截):

run 1:  46 × "Registry returned no usable file content"
        Registry: 0 cached, 46 fetched
run 2:  46 × "Registry returned no usable file content"
        Registry: 46 cached, 0 fetched      (整小时不再重试)

落盘的缓存条目正文:

{"url":"https://ui.shadcn.com/r/styles/default/calendar.json","fetchedAt":1786022023686,
 "data":"Host not in allowlist: ui.shadcn.com. Add this host to your network egress settings to allow access."}

两条独立防御(按 issue 建议)

  1. 传输层 —— fetchUrl 非 2xx 直接 reject,错误信息带状态码与净化后的正文摘要(形如 HTTP 403 Forbidden from https://ui.shadcn.com/… — Host not in allowlist: …)。2xx 但解析不出 JSON 同样 reject:旧代码 resolve 原始字符串,而所有消费者读的都是 data.files?.[0]?.content,undefined 让失败静默掉了——这正是 #0.1 说的「宽容消费者」把生产端错误藏起来。重定向刻意不跟随:3xx 落到登录页/中间页是同一类毒药,注册表端点本身是直连 JSON,真要跟随应当是一次显式决定。
  2. 语义层 —— 只有通过 isRegistryEntry(files[0].content 非空)的响应才写缓存。200 但返回 {"error":"…"} 这类错误信封,依旧原样返回给调用方(--check 仍报 "no usable file content"),但不许进磁盘

读侧也校验(选择理由)

同一个 isRegistryEntry的时候也跑一遍,不通过就删掉该条目并重新取。只做写侧校验的话,已经被旧版本毒化的 checkout 仍要靠 TTL 熬满一小时才能自愈,期间除了「等」或「知道有 --no-cache 这个参数」没有出路。读侧校验让恢复立刻发生——这也是实测里 46 条毒条目在第一次运行就被清掉的原因。过期(TTL 到期)但格式正常的条目不算污染,照旧只是重新取,不计入 evicted。

未改动的语义

  • --update 仍然不读缓存(写入路径永远看实时数据),失败时仍拒绝写文件。区别只是错误信息现在说的是真正的原因(HTTP 403 …)而不是误导性的 "No files found in registry"。
  • --check 的退出码语义保持 fix(components,i18n): Sheet/Dialog 关闭按钮接入 common.close,并让 shadcn 同步机制携带该补丁 (objectstack#5505) #3455 不变:只有 declared-patch 失败才非零,普通 fetch 错误仍是 exit 0。
  • 汇总行现在把 failed / evicted 单独列出,一次全失败的运行不会再显示成 "46 fetched"。
  • main() 加了 entry-point 判断(只在文件本身是进程入口时执行),这样离线测试可以 import 内部函数;这条守卫由一个真正 spawn node scripts/shadcn-sync.js --list 的用例钉住——写错的话 pnpm shadcn:check 会变成静默 no-op。

验证

scripts/__tests__/shadcn-sync-fetch-cache.test.ts(23 例,全离线:本地 http fixture server + 注入 get,真 socket / 真状态行 / 真分块)。

反向验证(先预测方向再跑):只回退两条防御、保留导出与 isRegistryEntry 本身 → 预测 10 红,实测 10 红 13 绿,且绿的正是预测的那批(8 个 isRegistryEntry 断言全绿 —— 说明谓词本身被独立覆盖,不是幽灵检查;CLI 用例、正常缓存命中、TTL 过期重取、allowCache 关闭四例也如预期不受影响)。

 Test Files  11 passed (11)        # pnpm exec vitest run scripts
      Tests  167 passed (167)

离线端到端(沙箱 egress 拦截,先把 46 条毒条目放回缓存目录):

run 1: Registry: 0 cached, 0 fetched, 46 FAILED, 46 unusable entries evicted   EXIT=0
       ✗ button   Error fetching from registry: HTTP 403 Forbidden from … — Host not in allowlist: …
       缓存目录条目数: 0
run 2: Registry: 0 cached, 0 fetched, 46 FAILED                                EXIT=0
       缓存目录条目数: 0        (不再有 "46 cached")

健康路径未被误伤(用本地组件内容伪造 46 条格式正确的条目后跑真实 CLI):

✓ Identical: 46 components      Registry: 46 cached, 0 fetched

无 changeset:纯 CI/tooling 脚本,不面向用户(#3437 先例)。node scripts/check-control-bytes.mjs 通过,两个改动文件另做了控制字符自查(bodySnippet 按码位判断,源文件里不含任何控制字符字面量)。

…onse

`fetchUrl` never inspected `res.statusCode`, and a body that failed
`JSON.parse` was resolved as a raw string. A 403 from an egress allowlist
therefore resolved as if it were a component, and `fetchRegistry` wrote it to
disk unconditionally with a 1h TTL — so one blocked run poisoned every
`pnpm shadcn:check` for the next hour, reporting "46 cached, 0 fetched"
while never retrying.

Two independent defences:

- transport: a non-2xx (redirects included) and a 2xx that is not JSON now
  reject, with the status and a sanitised body excerpt in the message.
- semantic: only a response passing `isRegistryEntry` (files[0].content) is
  written to the cache, and the same check runs on READ — an entry poisoned
  by an older build is dropped on first contact instead of being trusted
  until its TTL expires, so recovery is immediate.

`--update` semantics are unchanged (still never reads the cache, still
refuses to write on a failed fetch); the summary line now reports failed and
evicted separately so a fully-blocked run cannot read as a success. `main()`
runs only when the file is the process entry point, so the offline tests can
import the fetch/cache internals.

Refs objectstack-ai/objectstack#5803

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 6, 2026 1:41pm

Request Review

…ype gate

objectui#3494 (PR #3498) adds `tsconfig.scripts.json` with `allowJs: true`,
whose `include` glob covers this branch's new test file too. Neither PR can
see the other, and the two land green individually while the merge is red —
no git conflict, so nothing warns.

Two separate problems, both found by running #3498's compilerOptions against
this branch:

- the `@ts-expect-error` above the `../shadcn-sync.js` import becomes TS2578
  once the import has an inferred type. Removed (the sibling test files get
  the same treatment inside #3498).
- `fetchRegistry`'s `get` option was the one destructured option without a
  default, so it is absent from the function's inferred signature and passing
  it from a `.ts` caller is TS2353. Fixed at the producer by giving it its
  real default (`https.get`), which also drops the `get ? { get } : undefined`
  dance at the call site — not by suppressing it at the consumer.

Behaviour is unchanged: production still resolves `https.get`, the tests still
inject `http.get`. Verified against a local copy of #3498's config — zero
errors in either file; the only remaining diagnostics are the five stale
directives that #3498 itself removes.

Refs objectstack-ai/objectstack#5803

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@yinlianghui
yinlianghui marked this pull request as ready for review August 6, 2026 13:51
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit 1d80eea Aug 6, 2026
17 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-5803-shadcn-fetch-cache branch August 6, 2026 13:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

objectui: shadcn-sync 的 registry 缓存会把 403/HTML 错误响应当成正常数据缓存 1 小时,一次网络抖动毒化后续所有 --check

2 participants