Repository navigation
fix(no-ticket): exit quietly when Ctrl+C meets a closed pipe - #567
cloudsmith-iduffy wants to merge 4 commits into
Conversation
On Ctrl+C, Click writes a newline to stderr before it raises Abort. When the reader of stderr is gone, that write raises BrokenPipeError inside Click's KeyboardInterrupt handler. Click's own EPIPE handler does not catch it, so AliasGroup.main reported it to Sentry as a crash and wrote an error line to the closed pipe. Catch BrokenPipeError in AliasGroup.main and exit with status 1. Fixes CLOUDSMITH-CLI-1R Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
A closed stderr pipe can leave the Ctrl+C newline buffered, causing Python’s shutdown flush to override exit status 1 with 120; the stream needs safe cleanup and a real-pipe subprocess regression test.
1 open issue
What changed in this PR
Handles broken pipes during Ctrl+C in the CLI’s central error handler, avoiding misleading crash messages and Sentry reports.
Changes:
- Catches
BrokenPipeError, requesting exit status 1 in standalone mode and re-raising for embedded callers. - Adds regression coverage for an interrupt with closed stderr.
- Documents the behavior in the changelog.
| File | Description |
|---|---|
cloudsmith_cli/cli/tests/test_unexpected_errors.py |
Tests silent handling without Sentry reporting using a mocked closed pipe. |
cloudsmith_cli/cli/command.py |
Adds a dedicated BrokenPipeError handler. |
CHANGELOG.md |
Describes the intended quiet exit on interruption with a closed pipe. |
Review effort: Balanced
The newline that Click failed to write stays in the stderr buffer. Python flushes stderr again at exit, the flush fails, and the process exits with status 120. Wrap stdout and stderr in Click's _PacifyFlushWrapper before the exit, as Click does for a closed pipe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@copilot resolve the merge conflicts on this branch. |
…t-broken-pipe # Conflicts: # CHANGELOG.md Co-authored-by: cloudsmith-iduffy <178375997+cloudsmith-iduffy@users.noreply.github.com>
Head branch was pushed to by a user without write access
Resolved in 528f53f by merging |

Description
Sentry CLOUDSMITH-CLI-1R is a
BrokenPipeErrorfromcloudsmith whoamion 1.30.0. The user, or a wrapper process, sent Ctrl+C while the CLI was still starting up. The reader of stderr had already closed the pipe.On Ctrl+C, Click writes a newline to stderr before it raises
Abort. Here that write raisedBrokenPipeErrorinside Click'sKeyboardInterrupthandler. Click has an EPIPE handler, but it only catches errors from the command, not from anotherexceptblock, so the error reachedAliasGroup.main. Our catch-all sent it to Sentry as a crash. The process then exited with status 120, because Python flushed the failed newline again at exit and that flush failed too.AliasGroup.mainnow catchesBrokenPipeError, prints nothing and exits with status 1. Before the exit it wraps stdout and stderr in Click's_PacifyFlushWrapper, as Click's own EPIPE handler does, so the flush at exit can't change the status. Whenstandalone_mode=Falseit re-raises, like the other branches.The Sentry event names #374 (lazy command imports) as the suspect commit. That commit is not the cause. The interrupt landed during the
cloudsmith_apiimport only because that import is the slowest part of start-up.Type of Change
Additional Notes
There are two tests. The CliRunner test checks that nothing is reported and nothing is printed. The subprocess test runs the CLI with stderr on a closed pipe and checks that the exit status is 1. CliRunner never reaches interpreter shutdown, so it can't see the status 120. The subprocess test turns off
PYTHONUNBUFFERED, because unbuffered stderr hides the failure._PacifyFlushWrapperis a private Click name. It is in Click 8.5, and the CLI has no upper bound on Click.A
BrokenPipeErrorraised inside our other handlers, such as the JSON error output orAborted!, still escapes. That was already the case before this change, and I'll fix it separately.🤖 Generated with Claude Code