Skip to content

fix(cli): prevent handled errors from producing duplicate output [CLI-1765] - #7130

Closed
bdemeo12 wants to merge 4 commits into
mainfrom
CLI-1765/prevent-duplicate-error-output
Closed

fix(cli): prevent handled errors from producing duplicate output [CLI-1765]#7130
bdemeo12 wants to merge 4 commits into
mainfrom
CLI-1765/prevent-duplicate-error-output

Conversation

@bdemeo12

@bdemeo12 bdemeo12 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Pull Request Submission Checklist

  • Follows CONTRIBUTING guidelines
  • Commit messages
    are release-note ready, emphasizing
    what was changed, not how.
  • Includes detailed description of changes
  • Contains risk assessment (Low | Medium | High)
  • Highlights breaking API changes (if applicable) — none
  • Links to automated tests covering new functionality
  • Includes manual testing instructions (if necessary)
  • Updates relevant GitBook documentation (PR link: ___) — n/a, no user-facing docs change
  • Includes product update to be announced in the next stable release notes

What does this PR do?

CLI Ticket: https://snyksec.atlassian.net/browse/CLI-1765?actionerId=712020%3Aed5c88da-ffa1-47ff-9ea7-b4345eaadf9f&sourceType=assign

Fixes:

json output being unparseable because an extra error was printed after the result. This happens when the CLI receives an error (aside from exit status 1), it prints it — so once teardown joins the exit error together with the handled network errors, the result is no longer exit status 1 and gets printed after the real output.

Also pins snyk-docker-plugin to 9.20.0 so CI exercises the failure being fixed. (Original PR: #7047 + discussion: https://snyksec.atlassian.net/servicedesk/customer/portal/64/CLIA-1576)

Where should the reviewer start?

cliv2/pkg/core/main.goshouldSuppressDisplay and the third return value from processError, then the guarded displayError call in tearDown.

Then cliv2/pkg/core/main_test.go for the two new cases in Test_processError and the new Test_shouldSuppressDisplay.

How should this be manually tested?

Against an image whose provenance fetch fails (SDP version 9.20.0 +):

snyk container test <image> --json > out.json
jq . out.json

The container.spec.ts acceptance job is the automated equivalent — it fails on 9.20.0 without this fix, which is why the pin is included here.

What's the product update that needs to be communicated to CLI users?

n/a

@snyk-io

snyk-io Bot commented Aug 13, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@bdemeo12

Copy link
Copy Markdown
Contributor Author

This PR originates from: #7047

Comment thread cliv2/pkg/core/main.go Outdated
Comment thread cliv2/pkg/core/main.go
//
// Joined errors match no direct type assertion, so they are unwrapped and checked
// one at a time.
func shouldSuppressDisplay(err error) bool {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All errors are combined here:

resultErrorList = append([]error{resultError}, resultErrorList...)

shouldSuppressDisplay returns true when there's nothing to show:

if shouldSuppressDisplay(err) {

Previously displayError checked the error's type directly. it didnt account for a combined error, so it fell through to the printing code and emitted a second JSON object.:

if isExitError || isErrorWithCode || errorHasBeenShown(err) {

Now this check suppresses the display err by returning early, before the printing

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Code reproduction also makes me think the original error is the better place to make this decision. Before processError, we know whether the command returned exec.ExitError or ErrorWithExitCode; after joining, we have to infer that recursively and risk suppressing unrelated sibling errors.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could calculate suppression from the original error in tearDown func, before processError joins anything?

suppressDisplay := shouldSuppressDisplay(err)
if err != nil {
    allErrors, outputError = processError(err, errorList)
    // If processError selected a different error, such as maintenance or the
    // captured cause of TS_CLI_TERMINATED, that error still needs displaying.
    suppressDisplay = suppressDisplay && errors.Is(outputError, err)
}
if !suppressDisplay {
    displayError(outputError, ...)
}

shouldSuppressDisplay could then retain the simple direct checks for exec.ExitError, ErrorWithExitCode, and already-displayed errors. This covers both cases while avoiding recursive unwrapping.

@bdemeo12
bdemeo12 force-pushed the CLI-1765/prevent-duplicate-error-output branch from 668cf76 to bc48bab Compare August 17, 2026 00:33
Comment thread package.json
"snyk-config": "^5.0.0",
"snyk-cpp-plugin": "^2.24.3",
"snyk-docker-plugin": "9.19.0",
"snyk-docker-plugin": "9.20.0",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bumped SDP to use as an acceptance test

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bdemeo12
bdemeo12 force-pushed the CLI-1765/prevent-duplicate-error-output branch from bc48bab to 33b1438 Compare August 17, 2026 01:08
@bdemeo12
bdemeo12 marked this pull request as ready for review August 17, 2026 01:59
@bdemeo12
bdemeo12 requested a review from a team as a code owner August 17, 2026 01:59
@snyk-pr-review-bot

This comment has been minimized.

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor
Warnings
⚠️ There are multiple commits on your branch, please squash them locally before merging!
⚠️

"[fix(cli): prevent handled errors from producing duplicate output CLI-1765](#7130)" is too long. Keep the first line of your commit message under 72 characters.

⚠️

"[fix(cli): decide error suppression from the original exit code CLI-1765](https://api.github.com/repos/snyk/cli/git/commits/5958a11905ae98d26c61e2ed757e1e2a5bfcdd11)" is too long. Keep the first line of your commit message under 72 characters.

Generated by 🚫 dangerJS against a858775

@snyk-pr-review-bot

This comment has been minimized.

@snyk-pr-review-bot

This comment has been minimized.

})
}

func Test_shouldSuppressDisplay(t *testing.T) {

@CatalinSnyk CatalinSnyk Aug 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Could we also use the ticket mentioned tests here? They assert that both exec.ExitError and ErrorWithExitCode remain the returned outputError, while the auxiliary error remains in allErrors. I think it better shows the separate concern for the user facing output and analytics error list.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! Added!

Comment thread cliv2/pkg/core/main.go Outdated
if err != nil {
allErrors, outputError = processError(err, errorList)

// Determine suppression from the original exit code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: I think we can keep this responsibility inside processError instead of returning a joined display error and having carrying a suppression state suppression?

processError already returns both allErrors and outputError. We can keep the joined errors in allErrors for the analytics payload, but return the original command error as outputError when the selected result still contains it. Something like this is what I'm thinking currently:

mostRelevant := cli_errors.FindMostRelevantError(allErrors)
if commandOwnsOutput(originalError) &&
    errors.Is(mostRelevant, originalError) {
    outputError = originalError
} else {
    outputError = mostRelevant
}

commandOwnsOutput could match the two original command-result types: exec.ExitError and ErrorWithExitCode.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! processError now owns this! Thanks for the help!

@snyk-pr-review-bot

This comment has been minimized.

@snyk-pr-review-bot

This comment has been minimized.

@snyk-pr-review-bot

This comment has been minimized.

@robertolopezlopez

Copy link
Copy Markdown
Contributor

@bdemeo12 please have a look to the failing pipeline 👍

@snyk-pr-review-bot

This comment has been minimized.

@bdemeo12

Copy link
Copy Markdown
Contributor Author

@bdemeo12 please have a look to the failing pipeline 👍

I think this pipeline is flakey! I reran it and its succeeded!

@snyk-pr-review-bot

This comment has been minimized.

@bdemeo12
bdemeo12 enabled auto-merge August 25, 2026 17:03
@snyk-pr-review-bot

This comment has been minimized.

@bdemeo12
bdemeo12 requested review from a team and octavian-snyk August 26, 2026 17:31
@robertolopezlopez
robertolopezlopez dismissed their stale review August 27, 2026 07:41

it looks good to me, but @CatalinSnyk has broader context to judge. I would rather wait for his opinion (thus removing my approval)

@bdemeo12
bdemeo12 enabled auto-merge August 27, 2026 13:01
@bdemeo12
bdemeo12 force-pushed the CLI-1765/prevent-duplicate-error-output branch from 21c0b4a to e13fea9 Compare August 27, 2026 13:02
@snyk-pr-review-bot

This comment has been minimized.

bdemeo12 and others added 4 commits August 27, 2026 13:38
Errors are joined before they reach displayError, and a joined error
matched none of its direct type assertions, so an already-handled error
was printed after the command's real output - emitting a second JSON
object and breaking JSON.parse(stdout).

displayError now unwraps joined errors and checks them one at a time.

Pins snyk-docker-plugin to 9.20.0 so CI exercises the failure being fixed.

CLI-1765

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…765]

Evaluate suppression on the original command error (exec.ExitError or
ErrorWithExitCode, exit code < SNYK_EXIT_CODE_ERROR) instead of the joined
error, covering the snyk code test case, and guard with errors.Is so a
promoted error is still displayed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@bdemeo12
bdemeo12 force-pushed the CLI-1765/prevent-duplicate-error-output branch from e13fea9 to a858775 Compare August 27, 2026 17:38
@snyk-pr-review-bot

Copy link
Copy Markdown

PR Reviewer Guide 🔍

🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Possible Silent Failures 🟡 [minor]

In processError, the new logic returns early if commandOwnsOutput(resultError) is true and it matches mostRelevant. This prevents the subsequent mapErrorToExitCode and createErrorWithExitCode logic from running for that specific error. While commandOwnsOutput currently targets errors that likely already have codes, if a new error type is added that 'owns output' but requires mapping (like a custom exit code from a plugin), this early return would bypass the mapping table.

if commandOwnsOutput(resultError) && errors.Is(mostRelevant, resultError) {
	return resultErrorList, resultError
}
📚 Repository Context Analyzed

This review considered 11 relevant code sections from 7 files (average relevance: 0.90)

🤖 Repository instructions applied (from AGENTS.md)

@PeterSchafer

Copy link
Copy Markdown
Contributor

Closing this PR as it was replaced by #7191

auto-merge was automatically disabled September 1, 2026 15:33

Pull request was closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants