Repository navigation
[BUG]: sf package uninstall hides the real uninstall error behind MALFORMED_QUERY (regression since sfdx-cli 7.183.0) #3664
Description
Before You Submit
- I'm using the latest version of Salesforce CLI
- I've searched the existing issues
- I've run
sf doctorto diagnose common issues
Related Issue
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.patchin an
empty directory, then from that directory:Result: 13 passing, 2 failing ("should send the uninstall request, and handle errorsgit 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
appropriately" and "uninstallStatus reports a failed request with its errors, using valid
SOQL"), each withAssertionError: expected '"SELECT Message FROM PackageVersionUn…' to equal 'SELECT Message FROM PackageVersionUni…' - Against a scratch org, the query string from L30:
Result:
sf data query --use-tooling-api -o <org> -q "\"SELECT Message FROM PackageVersionUninstallRequestError WHERE ParentRequest.Id = '06y000000000000AAA' ORDER BY Message\""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
Statusis
Errorand 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 withutil.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:
pollUninstall,defaultbranch (L57-64):sf package uninstall --waitSubscriberPackageVersion.uninstallStatus,Status === 'Error'(subscriberPackageVersion.ts L215-222):sf package uninstall report
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 SOQLpollUninstallsends. 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 its
errors, using valid SOQL": reuses thequeryStubfrombeforeEach, and checks the error after
thetry, 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
Activity
github-actions commentedon Oct 8, 2026
Hello @jamesquinn103 👋 It looks like you didn't include the full Salesforce CLI version information in your issue.
Please provide the output of
version --verbose --jsonfor the CLI you're using (sforsfdx).A few more things to check:
rcornightlyversions. (docs)doctorcommand to diagnose common issues.Thank you!
github-actions commentedon Oct 8, 2026
Thank you for filing this issue. We appreciate your feedback and will review the issue as soon as possible. Remember, however, that GitHub isn't a mechanism for receiving support under any agreement or SLA. If you require immediate assistance, contact Salesforce Customer Support.
git2gus commentedon Oct 8, 2026
This issue has been linked to a new work item: W-24463742
jamesquinn103 commentedon Oct 8, 2026
Version output the bot asked for. This is my current CLI (2.153.5, the latest). The repro in the issue ran on 2.152.14; both bundle the same @salesforce/packaging 5.0.12.
{ "architecture": "darwin-arm64", "cliVersion": "@salesforce/cli/2.153.5", "nodeVersion": "node-v24.21.0", "osVersion": "Darwin 25.6.0", "shell": "zsh", "pluginVersions": [ "@oclif/plugin-autocomplete 4.0.2 (core)", "@oclif/plugin-commands 5.0.2 (core)", "@oclif/plugin-help 7.0.2 (core)", "@oclif/plugin-not-found 4.0.2 (core)", "@oclif/plugin-plugins 7.0.3 (core)", "@oclif/plugin-search 2.0.0 (core)", "@oclif/plugin-update 5.0.2 (core)", "@oclif/plugin-version 3.0.2 (core)", "@oclif/plugin-warn-if-update-available 4.0.1 (core)", "@oclif/plugin-which 4.0.1 (core)", "@salesforce/cli 2.153.5 (core)", "agent 2.2.2 (core)", "apex 4.3.0 (core)", "api 2.0.10 (core)", "auth 5.0.10 (core)", "code-analyzer 5.16.0 (user) published 43 days ago (Tue Aug 25 2026)", "data 5.1.8 (core)", "deploy-retrieve 4.2.2 (core)", "info 4.0.10 (core)", "limits 4.0.5 (core)", "marketplace 2.0.6 (core)", "org 6.0.17 (core)", "packaging 3.0.7 (core)", "schema 4.0.7 (core)", "settings 3.0.7 (core)", "sobject 2.0.6 (core)", "telemetry 4.1.0 (core)", "templates 57.4.0 (core)", "trust 4.0.12 (core)", "user 5.0.6 (core)" ] }