[BUG]: sf package uninstall hides the real uninstall error behind MALFORMED_QUERY
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
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:
pollUninstall,defaultbranch (L57-64):sf package uninstall --waitSubscriberPackageVersion.uninstallStatus,Status === 'Error'(subscriberPackageVersion.ts L215-222):sf package uninstall report
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
- Unit tests, under Node 22. Save the patch below 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 (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):
Result: the uninstall command ends with the Actual output; the request'spublic with sharing class SubRefProbe { public static Integer countRows() { return [SELECT COUNT() FROM <ns>__UninstProbe__c]; } }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.
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 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/packaging5.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/packaginglibrary 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
- Nessun Dockerfile né file Docker Compose
- Nessun modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di forcedotcom/cli
-
investigating validated
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
forcedotcom/cli#3657 · 2 commenti ·
-
`sf agent preview` fails with `AgentApiNotFound` against staging (aws-stage1) orgs — `stage.api.salesforce.com` missing from endpoint fallbackForse già presa Una pull request collegata a questa issue è aperta o già unita. Apertaarea:afdx owned by another team
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
forcedotcom/cli#3645 · 2 commenti ·
-
bug investigating validated
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
forcedotcom/cli#3644 · 6 commenti ·
-
sf agent mcp asset replace --assets null throws raw TypeError instead of InvalidShapeForse già presa @konkonrong-lgtm l’ha presa 58 giorni fa. Apertaarea:afdx bug investigating owned by another team validated
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
forcedotcom/cli#3625 · 4 commenti ·
-
area:afdx bug owned by another team
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
forcedotcom/cli#3608 · 2 commenti ·
Tutte le issue di forcedotcom/cli
Issue simili
-
type/automation type/performance
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
I maintainer di solito rispondono entro 2 giorni
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 88/100
I maintainer di solito rispondono entro 1 giorno
-
S-Needs repro
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
biomejs/biome#12233 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
area/ci bug difficulty/easy
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
ArtVsMark/Stepik-Python-Grader#1600 ·
I maintainer di solito rispondono entro 1 giorno
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 77/100
yunaremaia/agent-undo#66 ·