Repository navigation
Description
Found while fixing #10965 (PR link below), by following that fix's refusal to its consumers. Not fixed there — different package, different door, and it wants its own triage.
The defect
packages/rest/src/package-routes.ts:642-657, the GET /api/v1/packages handler:
// Database packages (published artifacts)
try {
const dbPackages = await packageService.list();
for (const pkg of dbPackages) { /* merge into packagesMap */ }
} catch {
// Database query failed — continue with registry-only packages
}
const packages = Array.from(packagesMap.values());
sendOk(res, { packages, total: packages.length });A failed durable read is absorbed, and the door answers 200 with { packages, total } built from the registry alone. Nothing on the wire distinguishes:
- "these are all the packages" — the durable read succeeded and returned these, from
- "these are the packages I could still see" — the durable read failed and its contribution is missing.
total is reported as a complete count either way. The registrar-sourced entries even keep source: 'registry', which reads as provenance rather than as a warning that the database half is absent.
The sibling route is not affected: GET /api/v1/packages/:id (:685) lets packageService.get() throw out to sendThrownError, so a declared envelope reaches the client intact.
Why it surfaced now
#10965 made list() refuse (SERVICE_UNAVAILABLE / 503) when the storage seam returns no result set, instead of answering [] over a driver it never queried. That refusal is correct at the producer — and this catch swallows it, so on that door the observable behaviour barely changes: previously a silent empty merge, now a silent skipped merge. The producer-side fix cannot close this; the door has to.
It is the same shape as the class #10965 belongs to (#10677 / PR #10788, #10789 / PR #10964): a read that could not happen is being reported as a read that found nothing. Here it is one level up, in a consumer-side catch rather than in a flattener.
Not measured here
- Whether the empty catch is load-bearing for any real deployment — i.e. whether there is a supported composition where the durable read routinely fails and a degraded 200 is the deliberate posture. The comment says "continue with registry-only packages", which reads as intent, so this may be a deliberate degradation that simply needs to become visible rather than a bug to remove. That is the triage question and it changes the fix.
- The right shape if it is a bug. Options, not decided here: refuse (fail the whole listing), or keep the 200 and carry a declared partial-result marker so the caller can tell. The second preserves the degradation the comment intends while ending the ambiguity; it is a response-shape change and therefore a contract decision, not a drive-by.
- Whether the dispatcher's own
/packagesdoor has the same catch.
Activity
os-zhuang commentedon Aug 22, 2026
Triage: re-labelled
domain:rest→domain:cli(anchoring rule:packages/restis the cli lane's package family;domain:restis not in the lane table — self-minteddomain:*labels are removed on triage, the label object's cleanup is tracked seat-side). Stays Bug, nowpm:queue.Grading rationale: this inherits the standing "silent drop ⇒ loud refusal" family ruling (the same lineage as #10965 / #10677→PR #10788 / #10789→PR #10964): a read that could not happen must not be reported as a read that found nothing — here the swallowed failure additionally corrupts
total, which is reported as a complete count. The sibling routeGET /api/v1/packages/:idalready lets the error reachsendThrownError, so the aligned fix is to stop absorbing the failure on the list door too (let the producer's declared refusal — 503SERVICE_UNAVAILABLEafter #10965 — reach the client through the declared envelope). ⛔ The alternative the card sketches (keep 200 + add a declared partial-result marker) is a response-shape/public-surface change — that is a contract decision and is NOT authorized by this grading; if the dev finds real evidence the degraded 200 is a load-bearing deliberate posture for a supported composition, stop and reportneeds_decisioninstead of picking. Checking whether the dispatcher's own/packagesdoor has the same catch is in scope as a read (report it; fix only if it is the same one-line shape). Size S.Generated by Claude Code
fix(rest): stop absorbing a failed registry read into a 200 on GET /a…
fix(rest): stop absorbing a failed durable read into a 200 packages l…
fix(runtime): refuse `POST /packages/:id/duplicate` on a source that …
fix(runtime): refuse `POST /packages/:id/duplicate` on a source that …