Skip to content

feat(npm): rebuild a missing packument and serve the abbreviated form to installers - #958

Merged
ertime037 merged 4 commits into
mainfrom
fix/npm-packument-rebuild
Sep 5, 2026
Merged

feat(npm): rebuild a missing packument and serve the abbreviated form to installers#958
ertime037 merged 4 commits into
mainfrom
fix/npm-packument-rebuild

Conversation

@devitway

@devitway devitway commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Closes #956. Closes #957.

Two npm packument fixes. #959 was stacked on this branch and has already merged into it, so this PR now carries both.

Housekeeping: this PR was auto-closed for a moment when I force-pushed the branch to correct a Co-authored-by address. Reopened; the branch was never lost.


1. A missing packument is rebuilt instead of 404'd (#956)

GET /npm/<pkg> answered 404 for a hosted package whose derived metadata.json was missing, while every published version was still sitting in storage.

The reassembly already existed — regenerate_packument() lists versions/, dist-tags/ and pkg.json — but only the publish path called it. A read fell through to the upstream proxy, which then 404'd for a package that exists only in this registry.

The read path now rebuilds when versions/ is non-empty, serves the result, and re-materializes metadata.json so the cost is paid once.

  • Before the namespace guard, on purpose: serving locally-owned bytes is always allowed, and that guard exists to stop the upstream fetch below it.
  • Under the same publish_lock the publish path takes, so a fleet stampeding one package rebuilds it once rather than once per request.
  • A name with nothing behind it still returns 404 — the rebuild must not invent a package out of an empty prefix.

Measured on a polygon: real S3 (MinIO), real npm publish, 500 versions, metadata.json deleted from the bucket.

result
main 404, 0 B, 0.93 ms — 500 version objects still in the bucket
this branch 200, 234 186 B, 185 ms
second request 200, 2.8 ms — plain cache hit, packument re-materialized
package never published 404, 1.2 ms — unchanged

Adds nora_packument_rebuilt_total{registry}. A non-zero rate means storage was written or restored outside NORA — worth an alert rather than a silent repair.

2. The abbreviated packument for install-v1 clients (#957)

npm install asks for application/vnd.npm.install-v1+json and NORA ignored it, serving the full document to everyone. The fields it carries are exactly the ones an installer never reads.

  • The per-version field list is an allowlist, in one place. Dropping a field an installer consults — os, cpu, engines, peerDependenciesMeta — silently changes what gets resolved.
  • Vary: Accept is load-bearing. The body now depends on a request header while metadata stays Cache-Control: public; without it a shared cache can hand the short document to a client that wanted the full one.
  • Derived locally, never requested upstream, so the cache keeps one canonical object per package and a short document can never displace the full one.
package full install-v1 versions
lodash 247 652 B 71 989 B −70.9% 117 → 117
express 804 975 B 344 703 B −57.2% 288 → 288

The polygon caught a real bug mid-flight: the first pass abbreviated the body on the stale-serve path but left Content-Type: application/json on it — a short document announced as the full one. Fixed by routing that branch through the same helper, and a test now asserts the x-nora-stale precondition together with the content type, so the degraded path cannot drift from the normal one again.

Validation

Seven tests across both changes, flip-verified — with the rebuild disabled two go red while the "absent package stays 404" guard stays green, which is what it is for.

End-to-end on the polygon with the same binary: publish 200 versions, read the packument, npm install from the registry (hosted and proxied), Maven proxy fetch — all unchanged, and the storage layout is identical to main: same keys, same object count.

Full suite: 63 lib + 1788 bin + 6 doc, zero failures. cargo fmt --check and cargo clippy -p nora-registry --all-targets -- -D warnings clean.

Credit

Both were surfaced while validating #889 on a polygon; the defects are ours. Both commits carry Co-authored-by: ivaseeq.

@devitway
devitway requested a review from ertime037 as a code owner September 5, 2026 08:00
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

🐳 Test image pushed: ghcr.io/getnora-io/nora:pr-958

docker pull ghcr.io/getnora-io/nora:pr-958
docker run --rm -p 4000:4000 ghcr.io/getnora-io/nora:pr-958

@devitway devitway closed this Sep 5, 2026
@devitway
devitway force-pushed the fix/npm-packument-rebuild branch from d0d0129 to 9738ffb Compare September 5, 2026 08:30
devitway and others added 2 commits September 5, 2026 11:30
A hosted package whose derived `metadata.json` was absent answered 404 even
though every published version was still in storage. The reassembly already
existed — `regenerate_packument()` lists `versions/`, `dist-tags/` and
`pkg.json` — but only the publish path ever called it, so a read fell through to
the upstream proxy and 404'd for a package that exists only in this registry.

The read path now rebuilds when `versions/` is non-empty, serves the result and
re-materializes `metadata.json` so the cost is paid once. It runs before the
namespace guard on purpose: serving locally-owned bytes is always allowed, and
that guard exists to stop the upstream fetch below it. The rebuild takes the
same `publish_lock` the publish path uses, so a fleet stampeding one package
rebuilds it once rather than once per request.

A name with nothing behind it still returns 404 — the rebuild must not invent a
package out of an empty prefix.

Measured on an S3 (MinIO) polygon, hosted package with 500 versions and no
materialized packument:

    before    404, 0 B, 0.93 ms
    after     200, 234186 B, 185 ms — then 2.8 ms once materialized
    absent    404, 1.2 ms, unchanged

Storage layout is untouched: same keys and the same object count as before, and
a full publish/install/proxy pass over the polygon shows no change in behaviour.

Adds `nora_packument_rebuilt_total{registry}` — a non-zero rate points at
storage that was written or restored outside NORA.

Closes #956

Co-authored-by: ivaseeq <126263471+ivaseeq@users.noreply.github.com>
`npm install` asks for `application/vnd.npm.install-v1+json` — the abbreviated
packument — and NORA ignored it, serving the full document to every client. The
fields it carries are exactly the ones an installer never reads: readme,
maintainers, repository, per-version description, scripts, gitHead.

The packument serve path now branches on `Accept` and projects the document down
to npm's abbreviated shape. The per-version field list is an allowlist, in one
place: dropping a field an installer consults — `os`, `cpu`, `engines`,
`peerDependenciesMeta` — silently changes what gets resolved, so a field is
added there once rather than at a call site.

`Vary: Accept` goes with it, and it is not decoration. The body now depends on a
request header while NORA still marks metadata `Cache-Control: public`; without
`Vary` a shared cache can hand the abbreviated document to a client that asked
for the full one.

The abbreviation is derived locally rather than requested upstream, so the cache
keeps exactly one canonical object per package and the full document is never
displaced by a short one.

Measured against a live upstream through the proxy, all versions preserved:

    lodash     247652 -> 71989 B    -70.9%   117 versions
    express    804975 -> 344703 B   -57.2%   288 versions

An unparsable body is served unchanged rather than turned into an error: the
client asked for a projection of this document, not for a failure.

Closes #957

Co-authored-by: ivaseeq <126263471+ivaseeq@users.noreply.github.com>
feat(npm): serve the abbreviated packument for install-v1 clients
@devitway devitway reopened this Sep 5, 2026
@devitway devitway changed the title fix(npm): rebuild a missing packument from the per-version keys npm packument: rebuild a missing one, and serve the abbreviated form to installers Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

🐳 Test image pushed: ghcr.io/getnora-io/nora:pr-958

docker pull ghcr.io/getnora-io/nora:pr-958
docker run --rm -p 4000:4000 ghcr.io/getnora-io/nora:pr-958

ertime037
ertime037 previously approved these changes Sep 5, 2026
Both entries were missing from `## [Unreleased]`: the rebuild fix (#956) and
the abbreviated packument (#957), with the numbers measured on the polygon.

Co-authored-by: ivaseeq <126263471+ivaseeq@users.noreply.github.com>
@devitway devitway changed the title npm packument: rebuild a missing one, and serve the abbreviated form to installers feat(npm): rebuild a missing packument and serve the abbreviated form to installers Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

🐳 Test image pushed: ghcr.io/getnora-io/nora:pr-958

docker pull ghcr.io/getnora-io/nora:pr-958
docker run --rm -p 4000:4000 ghcr.io/getnora-io/nora:pr-958

2 similar comments
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

🐳 Test image pushed: ghcr.io/getnora-io/nora:pr-958

docker pull ghcr.io/getnora-io/nora:pr-958
docker run --rm -p 4000:4000 ghcr.io/getnora-io/nora:pr-958

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

🐳 Test image pushed: ghcr.io/getnora-io/nora:pr-958

docker pull ghcr.io/getnora-io/nora:pr-958
docker run --rm -p 4000:4000 ghcr.io/getnora-io/nora:pr-958

@devitway devitway closed this Sep 5, 2026
@devitway devitway reopened this Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

🐳 Test image pushed: ghcr.io/getnora-io/nora:pr-958

docker pull ghcr.io/getnora-io/nora:pr-958
docker run --rm -p 4000:4000 ghcr.io/getnora-io/nora:pr-958

@ertime037
ertime037 enabled auto-merge September 5, 2026 09:22
@ertime037
ertime037 disabled auto-merge September 5, 2026 09:22
@ertime037
ertime037 enabled auto-merge September 5, 2026 09:22
@ertime037
ertime037 disabled auto-merge September 5, 2026 09:25
@ertime037
ertime037 added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit 8495e06 Sep 5, 2026
37 of 38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants