Skip to content

[BUG]: sf package uninstall hides the real uninstall error behind MALFORMED_QUERY (regression since sfdx-cli 7.183.0) #3664

Description

@jamesquinn103

Before You Submit

  • I'm using the latest version of Salesforce CLI
  • I've searched the existing issues
  • I've run sf doctor to diagnose common issues

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

  1. 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…'
    
  2. 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.

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

  2. Install it in a scratch org and start sf package uninstall -p <04t> -o <org> -w 20.

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

  4. Control: delete SubRefProbe and uninstall again (the package is still installed):
    Successfully uninstalled package.

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

Activity

  1. added
    owned by another teamThe Salesforce CLI team does not own this work but will pass on the information to the correct team.
    regressionIssue that regresses existing functionality
    on Oct 8, 2026
  2. github-actions commented on Oct 8, 2026

    @github-actions

    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 --json for the CLI you're using (sf or sfdx).

    A few more things to check:

    • Make sure you've provided detailed steps to reproduce your issue.
      • A repository that clearly demonstrates the bug is ideal.
    • Make sure you've installed the latest version of Salesforce CLI. (docs)
      • Better yet, try the rc or nightly versions. (docs)
    • Try running the doctor command to diagnose common issues.
    • Search GitHub for existing related issues.

    Thank you!

  3. added
    more information requiredIssue requires more information or a response from the customer
    and removed
    investigatingWe're actively investigating this issue
    on Oct 8, 2026
  4. github-actions commented on Oct 8, 2026

    @github-actions

    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.

  5. git2gus commented on Oct 8, 2026

    @git2gus

    This issue has been linked to a new work item: W-24463742

  6. jamesquinn103 commented on Oct 8, 2026

    @jamesquinn103
    Author

    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)"
      ]
    }
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

    area:packagingmore information requiredIssue requires more information or a response from the customerowned by another teamThe Salesforce CLI team does not own this work but will pass on the information to the correct team.regressionIssue that regresses existing functionality

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions