Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

[BUG]: sf package uninstall hides the real uninstall error behind MALFORMED_QUERY

Chiusa
#3,661 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
2/5
Tempo stimato
1-3 ore
Idoneità per principianti
56/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
node.js, typescript
Ambito
cli, testing

Direzione di ricerca

Read src/package/packageUninstall.ts around getUninstallErrors and its callers, pollUninstall and SubscriberPackageVersion.uninstallStatus. Run the two named tests, test/package/uninstallPackage.test.ts and test/package/subscriberPackageVersion.test.ts, to check the malformed SOQL and error-handling paths. Done means both callers query with valid SOQL and their tests verify the resulting uninstall error.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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.

Summary

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

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:

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.

Steps to reproduce
  1. Unit tests, under Node 22. Save the patch below 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 (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 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.

Measured 2026-10-04, CLI 2.152.14, Winter '27 scratch org.

Expected

UNINSTALL_ERROR: Can't uninstall the package <06y> during uninstall request <06y>., plus the
=== Errors list when PackageVersionUninstallRequestError has rows. (Both ids are the request id
today, at packageUninstall.ts L58 and subscriberPackageVersion.ts L219; a separate, minor issue.)

Actual

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.

Fix and tests

Checked on main at 51fc61b (5.0.14): with the patch, the two files pass 15 tests.

  • 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 5.0.14 (main, 51fc61b).
  • CLI runs (step 2 and the end-to-end recipe): @salesforce/cli/2.152.14 darwin-arm64 node-v24.20.0, which bundles the @salesforce/packaging library 5.0.12 (via plugin-packaging 3.0.7).
  • Unit tests (step 1): Node 22, on 51fc61b. The repo's mocha doesn't start on Node 26.
sf doctor (version detail, trimmed)
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)
Lingua principale
Nessun dato sulla lingua
Stelle
571
Fork
80
Merge medio
2g 21h
PR unite (30g)
3

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di forcedotcom/cli

Tutte le issue di forcedotcom/cli

Issue simili

Altre issue su CLI

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.