fix(r2): reject empty remote manifests before scoped upload - #161
Conversation
Require Array.isArray(parsed.entries) before treating an R2 or file-path remote manifest as a hit. Empty objects, null, and JSON arrays now fail closed so a scoped upload cannot rewrite the remote catalog from an empty set. Valid empty entries arrays still hit. Dry-run missing and HTTP 404 stay missing. Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
|
Verified head Ran the production All 12 Exact-head checks:
Both workflows succeeded after approval of the pending fork runs. Ready for the orchestrator's squash merge. Contributor credit is preserved; the shared changelog update will be in #167. |
Apply configurable request deadlines to hostname cutover API calls and reject malformed or overflowing budgets before requests begin. Document both request-timeout controls and consolidate the prepared dependency and publishing changelog entries. Land after PRs #132, #152, #161, #163, #164, #165, and #166. Co-authored-by: Sebastien Tardif <SebTardif@ncf.ca>
What Problem This Solves
Fixes an issue where a docs publish that received a 200 remote manifest of
{},null, or a JSON array would treat that body as a cache hit. A later locale or page scoped upload then rewrote the remote catalog from that empty set, dropping objects that were not in the current scope.Why This Change Was Made
getRemoteManifest()already refuses invalid JSON. It did not requireentriesto be an array before stampingstatus: "hit". Both the R2 GET path and the local file-path path now requireArray.isArray(parsed.entries). Dry-run with no remote file and HTTP 404 staystatus: "missing"with empty entries. A valid{ version: 1, entries: [], objectCount: 0 }remains a hit.User Impact
Scoped docs publishes no longer replace the remote R2 catalog with a partial tree when the previous manifest body is empty or the wrong JSON shape. Operators see a fail-closed error instead of a successful upload that deleted unpublished locales.
Evidence
terminal output from running
scripts/docs-site/r2-upload.mjsthrough a small node driver against the unfixed tree, then the patched file.Before, a file-path remote body of
{}duringR2_UPLOAD_SCOPE=localecompleted as a hit and wrotedist/docs-r2-manifest.locale.merged.json:An HTTP 200 body of
{}from the synthetic R2 endpoint also completed as a hit:After the patch, the same driver refuses both empty shapes and still accepts a valid empty
entriesarray:node --check scripts/docs-site/r2-upload.mjsexits 0.The hit-without-shape-check has been present since the R2 publish path landed in d6e2734 (2026-05-07). File-path hits were added in 40cd8f4 (2026-05-16). Related publish work: #74 , #139 , #143 .
Real behavior proof
{},null, or a JSON array was treated as a hit, so a scoped upload rewrote the remote catalog from an empty entry set.{}with R2_UPLOAD_SCOPE=locale, an HTTP 200{}, and an HTTP 200{ version: 1, entries: [], objectCount: 0 }.{}and HTTP 200{}exit 1 with remote manifest must contain an entries array; refusing to reupload the full docs tree. The valid empty entries array stays a hit and completes the upload plan.