Before You Submit
Related Issue
#3661
Command
sf package uninstall --wait (and sf package uninstall report)
Org Type
Scratch Org
Did this work before?
Yes, this is a regression
Last Working Version
sfdx-cli 7.182.1 (sfdx force:package:uninstall). First broken: sfdx-cli 7.183.0.
Summary
This replaces #3661, which I filed without the bug form and wrongly marked as not a regression.
It is one: measured below. I'll close #3661 as a duplicate of this issue.
TL;DR: the SOQL in getUninstallErrors (src/package/packageUninstall.ts L30) is wrapped in
literal ". The patch below removes them and adds a test for each caller. I'd like to open it as
a PR on forcedotcom/packaging. If that's OK, assign this issue to me and I'll open it.
I'm a Salesforce employee (product management for ISV extensibility). I found this while testing
managed-package installs and uninstalls the way our ISV partners run them.
When an uninstall fails after the org accepts the request, sf package uninstall --wait and
sf package uninstall report hide the failure: no "uninstall failed", no request id, no reasons
(see Actual Result). The uninstall itself fails correctly; only the message is lost. I've hit it
four times in three months in my own ISV test pipelines, once mistaking it for a platform error
and once losing a real failure behind a passing retry. It has been broken since January 2023, so
I don't see it as urgent, but the fix is one line.
Repository to Reproduce
No response
Steps to reproduce
- Unit tests, under Node 22. Save the diff under Additional Information as
tool01.patch in an
empty directory, then from that directory:
git clone https://github.com/forcedotcom/packaging && cd packaging
git checkout 51fc61b # the commit the patch is cut against
yarn install
git apply ../tool01.patch # fix + tests
git checkout -- src/package/packageUninstall.ts # undo only the fix
npx mocha test/package/uninstallPackage.test.ts test/package/subscriberPackageVersion.test.ts
Result: 13 passing, 2 failing ("should send the uninstall request, and handle errors
appropriately" and "uninstallStatus reports a failed request with its errors, using valid
SOQL"), each with
AssertionError: expected '"SELECT Message FROM PackageVersionUn…' to equal 'SELECT Message FROM PackageVersionUni…'
- Against a scratch org, the query string from L30:
sf data query --use-tooling-api -o <org> -q "\"SELECT Message FROM PackageVersionUninstallRequestError WHERE ParentRequest.Id = '06y000000000000AAA' ORDER BY Message\""
Result: MALFORMED_QUERY: unexpected token: '"'. Without the outer quotes it runs
(totalSize: 0).
End to end, with a real uninstall that fails late, on the current CLI and the last working one (optional; needs a Dev Hub with a namespace)
Only an uninstall the org accepts and that fails later reaches this code. One the org refuses up
front (for example, a subscriber class references the package) is reported correctly. So this
recipe adds the blocking reference while a slow uninstall script holds the job open.
-
Build a managed beta with one custom object (UninstProbe__c) and an uninstall script that
waits 40 seconds:
// sfdx-project.json, package directory
{
"path": "force-app",
"package": "UninstLateFailProbe",
"versionNumber": "0.1.0.NEXT",
"uninstallScript": "UninstProbeHandler",
"default": true
}
global without sharing class UninstProbeHandler implements UninstallHandler {
global void onUninstall(UninstallContext ctx) {
Long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 40000) {
}
}
}
sf package version create -p UninstLateFailProbe -x --skip-validation -w 30
-
Install it in a scratch org and start sf package uninstall -p <04t> -o <org> -w 20.
-
Once the uninstall prints Status = InProgress, deploy this class from a second terminal, finishing
within the script's 40 seconds (<ns> is the package's namespace):
public with sharing class SubRefProbe {
public static Integer countRows() { return [SELECT COUNT() FROM <ns>__UninstProbe__c]; }
}
Result: the uninstall command ends with the Actual Result output; the request's Status is
Error and the package is still installed. sf package uninstall report -i <06y> prints the same.
-
Control: delete SubRefProbe and uninstall again (the package is still installed):
Successfully uninstalled package.
-
The regression: repeat steps 2 to 4 with the old CLIs. They need Node 18 (on current Node they
crash with util.isNullOrUndefined is not a function):
npm install sfdx-cli@7.182.1 # in an empty directory; 7.183.0 in another
node node_modules/sfdx-cli/bin/run force:package:uninstall -p <04t> -u <org> -w 20
| CLI |
Uninstall command comes from |
Output on the late failure |
| sfdx-cli 7.182.1 |
salesforce-alm 54.8.4 |
Can't uninstall the package <04t> during uninstall request <06y>. |
| sfdx-cli 7.183.0 |
plugin-packaging 1.12.0 |
unexpected token: '"' |
With no blocking class, 7.183.0 uninstalls the package (Successfully uninstalled package).
Measured 2026-10-04 (current CLI) and 2026-10-08 (old CLIs), Winter '27 scratch orgs.
Expected Result
UNINSTALL_ERROR: Can't uninstall the package <04t> during uninstall request <06y>., plus the
=== Errors list when PackageVersionUninstallRequestError has rows. That's what sfdx-cli 7.182.1
printed, with the package's 04t id first and the request's 06y id second.
With only the quotes fixed, today's code would print the request id in both places
(package <06y> during uninstall request <06y>), because packageUninstall.ts L58 and
subscriberPackageVersion.ts L219 pass the request id for the package. That's a separate, minor
issue; this one is about the error being hidden.
Actual Result
The CLI prints Error (1): unexpected token: '"' (exit 1). With --json, the error code is
MALFORMED_QUERY. This is the uninstall command's output, from the end-to-end steps; the quick
repro (step 2) shows the underlying query error.
System Information
cliVersion: @salesforce/cli/2.152.14
architecture: darwin-arm64 (Darwin 25.6.0), shell zsh
nodeVersion: node-v24.20.0
plugin-packaging: 3.0.7 (core); it loads the @salesforce/packaging library 5.0.12, where this bug is
other plugins: deploy-retrieve 4.2.2, auth 5.0.7, org 6.0.13, data 5.1.8 (core); code-analyzer 5.16.0 (user)
diagnostics: all pass except "sourceApiVersion matches apiVersion" (warn: run outside a project)
I reproduced on 2.152.14. The latest, 2.153.5, I checked by inspection, not by a run: its npm-shrinkwrap.json pins the same plugin-packaging 3.0.7 and @salesforce/packaging
5.0.12, and its `lib/package/packageUninstall.js` L33 has the same quoted query.
Additional Information
Cause. src/package/packageUninstall.ts L28-33:
const errorQueryResult = await conn.tooling.query<{ Message: string }>(
`"SELECT Message FROM PackageVersionUninstallRequestError WHERE ParentRequest.Id = '${id}' ORDER BY Message"`
);
The query throws before UNINSTALL_ERROR is built. Its only two callers:
The quotes came in when uninstall moved to this library (243f4e8, August 2022).
The command it replaced, in salesforce-alm, sent the same query without them. The CLI switched over
in sfdx-cli 7.183.0: it bundles plugin-packaging 1.12.0, the first version with the uninstall
command, and salesforce-alm 54.8.5, which no longer registers force:package:uninstall (54.8.4,
in 7.182.1, still did; checked in each version's oclif.manifest.json).
No test caught it: the pollUninstall error test
(uninstallPackage.test.ts L85)
stubs conn.tooling.query without checking its argument, and uninstallStatus's Error branch
has no test in
subscriberPackageVersion.test.ts.
Fix and tests. Checked at 51fc61b (5.0.14): with the patch, the two files pass 15 tests. main
hasn't changed these three files since.
- Fix: drop the outer
" at L30.
uninstallPackage.test.ts, "should send the uninstall request, and handle errors
appropriately": now also checks the SOQL pollUninstall sends. The id it expects,
04t4p000002BaHYAA0, is the request id this file's create() stub returns, not the package id.
subscriberPackageVersion.test.ts, new "uninstallStatus reports a failed request with its
errors, using valid SOQL": reuses the queryStub from beforeEach, and checks the error after
the try, so a call that doesn't throw fails.
diff --git a/src/package/packageUninstall.ts b/src/package/packageUninstall.ts
index 927a1c4..7680743 100644
--- a/src/package/packageUninstall.ts
+++ b/src/package/packageUninstall.ts
@@ -27,7 +27,7 @@ type UninstallResult = PackagingSObjects.SubscriberPackageVersionUninstallReques
export async function getUninstallErrors(conn: Connection, id: string): Promise<Array<{ Message: string }>> {
const errorQueryResult = await conn.tooling.query<{ Message: string }>(
- `"SELECT Message FROM PackageVersionUninstallRequestError WHERE ParentRequest.Id = '${id}' ORDER BY Message"`
+ `SELECT Message FROM PackageVersionUninstallRequestError WHERE ParentRequest.Id = '${id}' ORDER BY Message`
);
return errorQueryResult?.records ?? [];
}
diff --git a/test/package/subscriberPackageVersion.test.ts b/test/package/subscriberPackageVersion.test.ts
index 0f191f4..44e5ccf 100644
--- a/test/package/subscriberPackageVersion.test.ts
+++ b/test/package/subscriberPackageVersion.test.ts
@@ -170,6 +170,24 @@ describe('subscriberPackageVersion', () => {
expect(queryStub.called).to.be.true;
}
});
+ it('uninstallStatus reports a failed request with its errors, using valid SOQL', async () => {
+ const id = '06y000000000001AAA';
+ // @ts-ignore
+ $$.SANDBOX.stub(connection.tooling, 'retrieve').resolves({ Id: id, Status: 'Error' });
+ queryStub.resolves({ records: [{ Message: 'this is a server-side error message' }], done: true, totalSize: 1 });
+
+ let error: Error | undefined;
+ try {
+ await SubscriberPackageVersion.uninstallStatus(id, connection);
+ } catch (e) {
+ error = e as Error;
+ }
+ expect(error?.name).to.equal('UNINSTALL_ERROR');
+ expect(error?.message).to.include('(1) this is a server-side error message');
+ expect(queryStub.firstCall.args[0]).to.equal(
+ `SELECT Message FROM PackageVersionUninstallRequestError WHERE ParentRequest.Id = '${id}' ORDER BY Message`
+ );
+ });
it('should propagate the same error from the SPV query', async () => {
connection = await testOrg.getConnection();
diff --git a/test/package/uninstallPackage.test.ts b/test/package/uninstallPackage.test.ts
index 3ebf4cc..89c5ca0 100644
--- a/test/package/uninstallPackage.test.ts
+++ b/test/package/uninstallPackage.test.ts
@@ -91,7 +91,7 @@ describe('Package Uninstall', () => {
}),
});
// @ts-ignore
- $$.SANDBOX.stub(conn.tooling, 'query').resolves({
+ const queryStub = $$.SANDBOX.stub(conn.tooling, 'query').resolves({
records: [{ Message: 'this is a server-side error message' }, { Message: 'this is a second error message' }],
});
@@ -108,6 +108,9 @@ describe('Package Uninstall', () => {
expect(error.message).to.include('(2) this is a second error message');
expect(error.actions).to.deep.equal(['Verify installed package ID and resolve errors, then try again.']);
}
+ expect(queryStub.firstCall.args[0]).to.equal(
+ "SELECT Message FROM PackageVersionUninstallRequestError WHERE ParentRequest.Id = '04t4p000002BaHYAA0' ORDER BY Message"
+ );
});
it('should send the uninstall request, and handle errors appropriately (0 error messages)', async () => {
Environment. Affected: @salesforce/packaging 5.0.12 (L30 at its commit 7ec944d)
through main. Unit tests (step 1): Node 22, on 51fc61b; the repo's mocha doesn't start on Node 26.
Screenshots or Log Files
No response
Before You Submit
sf doctorto diagnose common issuesRelated Issue
#3661
Command
sf package uninstall --wait (and sf package uninstall report)
Org Type
Scratch Org
Did this work before?
Yes, this is a regression
Last Working Version
sfdx-cli 7.182.1 (
sfdx force:package:uninstall). First broken: sfdx-cli 7.183.0.Summary
This replaces #3661, which I filed without the bug form and wrongly marked as not a regression.
It is one: measured below. I'll close #3661 as a duplicate of this issue.
TL;DR: the SOQL in
getUninstallErrors(src/package/packageUninstall.tsL30) is wrapped inliteral
". The patch below removes them and adds a test for each caller. I'd like to open it asa PR on forcedotcom/packaging. If that's OK, assign this issue to me and I'll open it.
I'm a Salesforce employee (product management for ISV extensibility). I found this while testing
managed-package installs and uninstalls the way our ISV partners run them.
When an uninstall fails after the org accepts the request,
sf package uninstall --waitandsf package uninstall reporthide the failure: no "uninstall failed", no request id, no reasons(see Actual Result). The uninstall itself fails correctly; only the message is lost. I've hit it
four times in three months in my own ISV test pipelines, once mistaking it for a platform error
and once losing a real failure behind a passing retry. It has been broken since January 2023, so
I don't see it as urgent, but the fix is one line.
Repository to Reproduce
No response
Steps to reproduce
tool01.patchin anempty directory, then from that directory:
appropriately" and "uninstallStatus reports a failed request with its errors, using valid
SOQL"), each with
MALFORMED_QUERY: unexpected token: '"'. Without the outer quotes it runs(
totalSize: 0).End to end, with a real uninstall that fails late, on the current CLI and the last working one (optional; needs a Dev Hub with a namespace)
Only an uninstall the org accepts and that fails later reaches this code. One the org refuses up
front (for example, a subscriber class references the package) is reported correctly. So this
recipe adds the blocking reference while a slow uninstall script holds the job open.
Build a managed beta with one custom object (
UninstProbe__c) and an uninstall script thatwaits 40 seconds:
sf package version create -p UninstLateFailProbe -x --skip-validation -w 30Install it in a scratch org and start
sf package uninstall -p <04t> -o <org> -w 20.Once the uninstall prints
Status = InProgress, deploy this class from a second terminal, finishingwithin the script's 40 seconds (
<ns>is the package's namespace):Result: the uninstall command ends with the Actual Result output; the request's
StatusisErrorand the package is still installed.sf package uninstall report -i <06y>prints the same.Control: delete
SubRefProbeand uninstall again (the package is still installed):Successfully uninstalled package.The regression: repeat steps 2 to 4 with the old CLIs. They need Node 18 (on current Node they
crash with
util.isNullOrUndefined is not a function):Can't uninstall the package <04t> during uninstall request <06y>.unexpected token: '"'With no blocking class, 7.183.0 uninstalls the package (
Successfully uninstalled package).Measured 2026-10-04 (current CLI) and 2026-10-08 (old CLIs), Winter '27 scratch orgs.
Expected Result
UNINSTALL_ERROR:Can't uninstall the package <04t> during uninstall request <06y>., plus the=== Errorslist whenPackageVersionUninstallRequestErrorhas rows. That's what sfdx-cli 7.182.1printed, with the package's 04t id first and the request's 06y id second.
With only the quotes fixed, today's code would print the request id in both places
(
package <06y> during uninstall request <06y>), because packageUninstall.ts L58 andsubscriberPackageVersion.ts L219 pass the request id for the package. That's a separate, minor
issue; this one is about the error being hidden.
Actual Result
The CLI prints
Error (1): unexpected token: '"'(exit 1). With--json, the error code isMALFORMED_QUERY. This is the uninstall command's output, from the end-to-end steps; the quickrepro (step 2) shows the underlying query error.
System Information
Additional Information
Cause. src/package/packageUninstall.ts L28-33:
The query throws before
UNINSTALL_ERRORis built. Its only two callers:pollUninstall,defaultbranch (L57-64):sf package uninstall --waitSubscriberPackageVersion.uninstallStatus,Status === 'Error'(subscriberPackageVersion.ts L215-222):sf package uninstall reportThe quotes came in when uninstall moved to this library (243f4e8, August 2022).
The command it replaced, in salesforce-alm, sent the same query without them. The CLI switched over
in sfdx-cli 7.183.0: it bundles plugin-packaging 1.12.0, the first version with the uninstall
command, and salesforce-alm 54.8.5, which no longer registers
force:package:uninstall(54.8.4,in 7.182.1, still did; checked in each version's oclif.manifest.json).
No test caught it: the
pollUninstallerror test(uninstallPackage.test.ts L85)
stubs
conn.tooling.querywithout checking its argument, anduninstallStatus'sErrorbranchhas no test in
subscriberPackageVersion.test.ts.
Fix and tests. Checked at 51fc61b (5.0.14): with the patch, the two files pass 15 tests.
mainhasn't changed these three files since.
"at L30.uninstallPackage.test.ts, "should send the uninstall request, and handle errorsappropriately": now also checks the SOQL
pollUninstallsends. The id it expects,04t4p000002BaHYAA0, is the request id this file'screate()stub returns, not the package id.subscriberPackageVersion.test.ts, new "uninstallStatus reports a failed request with itserrors, using valid SOQL": reuses the
queryStubfrombeforeEach, and checks the error afterthe
try, so a call that doesn't throw fails.Environment. Affected:
@salesforce/packaging5.0.12 (L30 at its commit 7ec944d)through
main. Unit tests (step 1): Node 22, on 51fc61b; the repo's mocha doesn't start on Node 26.Screenshots or Log Files
No response