Repository navigation
pnpm unpublish <pkg>@<version> reports success but removes nothing when the registry URL has a path #16568
Description
Verify latest release
- I verified that the issue exists in the latest pnpm release (12.9.1)
pnpm version
12.9.1 (also seen with 12.8.1)
Which area(s) of pnpm are affected?
CLI
Link to the code that reproduces this issue or a replay of the bug
the script below (needs Docker; it uses Gitea's npm registry, which is served under a path)
Reproduction steps
#!/usr/bin/env bash
# pnpm unpublish <pkg>@<version> against a registry served under a path (Gitea's npm registry,
# http://localhost:3000/api/packages/demo/npm/). Needs docker, curl, jq and pnpm.
set -euo pipefail
docker run -d --rm --name pnpm-unpublish-repro -p 127.0.0.1:3000:3000 \
-e GITEA__security__INSTALL_LOCK=true -e GITEA__database__DB_TYPE=sqlite3 \
-e GITEA__server__ROOT_URL=http://localhost:3000/ docker.gitea.com/gitea:28 >/dev/null
trap 'docker stop pnpm-unpublish-repro >/dev/null' EXIT
until curl -sf http://localhost:3000/api/healthz >/dev/null; do sleep 1; done
docker exec -u git pnpm-unpublish-repro gitea admin user create --username demo --password demo-password \
--email demo@example.com --must-change-password=false >/dev/null
TOKEN=$(docker exec -u git pnpm-unpublish-repro gitea admin user generate-access-token --username demo \
--token-name t --scopes write:package --raw)
REGISTRY=http://localhost:3000/api/packages/demo/npm/
work=$(mktemp -d); cd "$work"
printf 'registry=%s\n//localhost:3000/api/packages/demo/npm/:_authToken=%s\n' "$REGISTRY" "$TOKEN" > .npmrc
for v in 1.0.0 1.1.0; do
echo "{\"name\":\"demo-pkg\",\"version\":\"$v\"}" > package.json
pnpm publish --no-git-checks >/dev/null
done
echo "versions before: $(curl -s "${REGISTRY}demo-pkg" | jq -c '.versions | keys')"
pnpm --version
pnpm unpublish demo-pkg@1.0.0
echo "versions after: $(curl -s "${REGISTRY}demo-pkg" | jq -c '.versions | keys')"Output:
versions before: ["1.0.0","1.1.0"]
12.9.1
Successfully unpublished 1 version(s) of demo-pkg
versions after: ["1.0.0","1.1.0"]
npm unpublish demo-pkg@1.0.0 (npm 12.2.0) against the same registry removes the version.
Describe the Bug
For a one-version unpublish, pnpm sends PUT <pkg>/-rev/<rev> and then a DELETE for each removed tarball. The
DELETE goes to the host root instead of the registry. Recorded through a proxy, with registry
http://localhost:6296/api/packages/e2e/npm/ and tarball
http://localhost:6296/api/packages/e2e/npm/m-pnpm/-/1.2.0/m-pnpm-1.2.0.tgz:
PUT /api/packages/e2e/npm/m-pnpm/-rev/undefined 200
DELETE /m-pnpm/-/1.2.0/m-pnpm-1.2.0.tgz/-rev/undefined 404 <- registry path missing
pnpm treats the 404 as success (the comment says some registries remove tarballs on the PUT), so it prints
Successfully unpublished …. Gitea ignores the PUT and removes versions on the tarball DELETE, so nothing is
removed. Any registry under a path that works this way is affected: Gitea, and others served under a path.
Registries at the host root, such as Verdaccio at /, aren't, because there the origin and the registry URL are
the same.
The cause is in pnpm/crates/cli/src/cli_args/unpublish.rs
(L272-L280 at bac8077):
let registry_origin = registry_origin(registry_url)?;
// ...
let pathname = tarball_pathname(tarball, registry_url)?;
let url = format!("{registry_origin}/{pathname}/-rev/{}", rev_str(updated.rev.as_deref()));tarball_pathname() strips the registry's path prefix from the tarball path, which is right and has a unit test,
but the URL is then built on the registry's origin (scheme://host:port), so the stripped prefix never comes
back.
npm does the same strip in libnpmpublish
(getPathname),
but then calls npmFetch(\${tarballUrl}/-rev/${_rev}`, opts), and npm-registry-fetch resolves that relative to opts.registry`, path included
(L102-L103).
Expected Behavior
The DELETE goes to <registry URL>/<tarball path without the registry path>/-rev/<rev>, as with npm:
DELETE /api/packages/e2e/npm/m-pnpm/-/1.2.0/m-pnpm-1.2.0.tgz/-rev/…. The version is removed, or pnpm reports an
error. A possible fix is to join the stripped pathname to registry_url (with its trailing slash) instead of
registry_origin, plus a test with a registry mounted under a path that checks the URL actually requested, not only
tarball_pathname().
Which Node.js version are you using?
24.21.0
Which operating systems have you used?
- Linux
If your OS is a Linux based, which one it is? (Include the version if relevant)
Ubuntu 24.04.4 LTS
Activity
github-actions commentedon Oct 3, 2026
@aadrian, are you planning to open a pull request for this? If so, reply here and a maintainer will assign the issue to you, so nobody else starts on the same fix.
fix(unpublish): delete tarballs under the registry path (pnpm#16572)
fix(deps): update dependencias no-major (#34)