What happened?
The shared OAuth callback flow creates a five-minute timeout for every authentication attempt but does not clear it when the callback server closes after success, provider error, state rejection, or another terminal path.
In packages/core/src/utils/oauth-flow.ts:226-243:
timeoutId is assigned a five-minute timer.
- The abort listener closes the server and rejects the response promise when that timer fires.
- The server's
close handler removes the abort listener but never calls clearTimeout(timeoutId).
As a result, a successfully completed flow retains the timer callback and its closure until the full timeout elapses. When it eventually fires, it aborts an already-settled flow. unref() prevents the timer alone from keeping Node alive, but does not release the retained objects while the process continues running.
The older Code Assist OAuth flow explicitly clears its corresponding timeout, and its tests assert that cleanup.
How can this be reproduced?
- Use fake timers and spy on
clearTimeout.
- Call
startCallbackServer() and await its selected port.
- Send a valid callback containing the expected state and await
response successfully.
- Observe that the server closes but
clearTimeout is never called.
- Advancing fake time by five minutes still executes the stale timeout callback.
Repeated successful authentication attempts accumulate one retained closure each for up to five minutes.
What did you expect to happen?
Every terminal path should perform idempotent cleanup: clear the timeout, remove listeners, and close the callback server. No timer belonging to a completed flow should fire later.
Why this matters
Long-running CLI/SDK processes that perform repeated OAuth flows retain unnecessary server/controller/promise state. The stale callback also makes lifecycle behavior harder to reason about and can interfere with tests using fake timers.
Suggested direction
- Centralize settlement and cleanup in one idempotent helper.
- Call
clearTimeout(timeoutId) whenever the server/response settles.
- Ensure the request success, OAuth error, state error, parser error, server error, and timeout paths all use that helper.
- Add assertions that the timeout and listeners are removed for both successful and failed flows.
Client information
Found by source audit on current main, commit f47d6c6f7. This is platform-independent OAuth lifecycle logic; /about output is not applicable.
What happened?
The shared OAuth callback flow creates a five-minute timeout for every authentication attempt but does not clear it when the callback server closes after success, provider error, state rejection, or another terminal path.
In
packages/core/src/utils/oauth-flow.ts:226-243:timeoutIdis assigned a five-minute timer.closehandler removes the abort listener but never callsclearTimeout(timeoutId).As a result, a successfully completed flow retains the timer callback and its closure until the full timeout elapses. When it eventually fires, it aborts an already-settled flow.
unref()prevents the timer alone from keeping Node alive, but does not release the retained objects while the process continues running.The older Code Assist OAuth flow explicitly clears its corresponding timeout, and its tests assert that cleanup.
How can this be reproduced?
clearTimeout.startCallbackServer()and await its selected port.responsesuccessfully.clearTimeoutis never called.Repeated successful authentication attempts accumulate one retained closure each for up to five minutes.
What did you expect to happen?
Every terminal path should perform idempotent cleanup: clear the timeout, remove listeners, and close the callback server. No timer belonging to a completed flow should fire later.
Why this matters
Long-running CLI/SDK processes that perform repeated OAuth flows retain unnecessary server/controller/promise state. The stale callback also makes lifecycle behavior harder to reason about and can interfere with tests using fake timers.
Suggested direction
clearTimeout(timeoutId)whenever the server/response settles.Client information
Found by source audit on current
main, commitf47d6c6f7. This is platform-independent OAuth lifecycle logic;/aboutoutput is not applicable.