Skip to content

fix(cli): close undici global dispatcher on exit - #1488

Merged
BYK merged 2 commits into
mainfrom
issue-1237-undici-dispatcher-close
Aug 26, 2026
Merged

fix(cli): close undici global dispatcher on exit#1488
BYK merged 2 commits into
mainfrom
issue-1237-undici-dispatcher-close

Conversation

@jared-outpost

@jared-outpost jared-outpost Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #1396, which added a force-exit timer as a safety net for the process-hang (#1237). This addresses the root cause.

Root cause

Ordinary commands (`org list`, `project list`, `auth status`, …) run through Node's global `fetch` (undici), which keeps a pool of keep-alive sockets open after the response. Those pooled sockets keep the event loop referenced, so the process lingers instead of exiting on its own — most visibly on macOS.

Fix

Close undici's global dispatcher in `runCli`'s `finally`, after all recovery middleware has reached a terminal result. That releases the pooled sockets so the loop drains naturally. No new dependency: the dispatcher is reached via the well-known `Symbol.for("undici.globalDispatcher.1")` and closed only if present, so it's a no-op under any runtime that doesn't expose it.

The macOS force-exit timer from #1396 stays armed as a backstop for any handle the dispatcher close can't reach (e.g. a libuv refcount quirk). It's unref'd, so it remains a no-op on clean exits.

Tests

  • `test/lib/close-dispatcher.test.ts` — covers close-when-present, no-dispatcher, and no-close-method cases.
  • `vitest run test/lib/close-dispatcher.test.ts test/lib/force-exit.test.ts` → 6 passed.
  • biome check on the changed files → clean.

Refs #1237

Ordinary commands left Node's global fetch (undici) keep-alive sockets
pooled after finishing their work, keeping the event loop referenced so
the process lingered on macOS (#1237). #1396 added a force-exit timer as
a safety net; this addresses the root cause by closing the global
dispatcher in runCli's finally, releasing the pooled sockets so the loop
drains on its own. The timer stays as a backstop for handles the close
can't reach.

Refs #1237
@vercel

vercel Bot commented Aug 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cli Ready Ready Preview Aug 26, 2026 1:37pm

Request Review

@jared-outpost
jared-outpost Bot requested a review from BYK August 26, 2026 13:16
@BYK
BYK marked this pull request as ready for review August 26, 2026 13:19
@github-actions github-actions Bot added the risk: medium PR risk score: medium label Aug 26, 2026
Comment thread packages/cli/src/cli.ts
@jared-outpost

jared-outpost Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

The review thread points to code on the PR branch issue-1237-undici-dispatcher-close which is not present in this workspace (only main is available). Cannot investigate or apply a fix. Please push the branch or provide the relevant diff.

@cursor cursor Bot left a comment

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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d66321c. Configure here.

Comment thread packages/cli/src/cli.ts Outdated
Bot review flagged that an unguarded `await closeGlobalDispatcher()` in
runCli's finally could reject (masking the command result, forcing exit
1) or hang (never arming the force-exit backstop).

- Arm scheduleForceExit() before the await so the backstop fires
  regardless of teardown outcome.
- Make closeGlobalDispatcher never reject (try/catch) and use destroy()
  instead of close() so it aborts in-flight requests immediately rather
  than waiting for them.

Refs #1237
@jared-outpost

jared-outpost Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

addressed the bot review findings in 3a2b750:

  • the force-exit backstop is now armed before awaiting dispatcher teardown, so a hang or rejection in teardown can't defeat it
  • closeGlobalDispatcher() uses destroy() (aborts in-flight, returns immediately) instead of close() (waits for in-flight → could hang), and wraps it in try/catch so it never rejects — a shutdown error can no longer mask the command result or force exit 1

added a regression test for the rejection case. both review threads resolved.

@jared-outpost
jared-outpost Bot requested a review from BYK August 26, 2026 13:44
@BYK
BYK merged commit 5049b7d into main Aug 26, 2026
33 checks passed
@BYK
BYK deleted the issue-1237-undici-dispatcher-close branch August 26, 2026 13:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant