Visitar URL original
pnpm unpublish <pkg>@<version> reports success but removes nothing when the registry URL has a path · Issue #16568 · pnpm/pnpm · GitHub
Skip to content

pnpm unpublish <pkg>@<version> reports success but removes nothing when the registry URL has a path #16568

Closed

Description

@aadrian

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

  1. github-actions commented on Oct 3, 2026

    @github-actions
    Contributor

    @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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions