fix(web): surface a failed OAuth clear instead of floating its rejection (#2144)

Copilot review round 10.

Both Clear entry points discarded the promise with `void`, but
`clearServerOAuthAndDisconnect` does not own its failures — the store write or
the disconnect can reject. That produced an unhandled rejection and left the
user with a control that had silently done nothing.

A shared `runClear` terminates it with `.catch` and raises a red, non-expiring
notification: the tokens may still be on disk and the session may still be up,
which is not a notice to let time out. An RFC 7009 failure is not this case —
those come back as an outcome and are already reported in the success toast.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-28 04:53:47 -04:00
commit 1661f09d0a

View file

@ -1320,19 +1320,42 @@ function App() {
// target isn't resolvable.
const settingsModalIsStdio = settingsModalServerType === "stdio";
/**
* Run a clear from a key/click handler, which cannot await.
*
* `clearServerOAuthAndDisconnect` does **not** own its failures — the store
* write or the disconnect can reject — so a bare `void` would produce an
* unhandled rejection and leave the user with a control that silently did
* nothing. (An RFC 7009 failure is not this: those come back as an outcome
* and are already reported in the success toast.)
*/
const runClear = useCallback(
(server: Parameters<typeof clearServerOAuthAndDisconnect>[0]) => {
clearServerOAuthAndDisconnect(server).catch((err: unknown) => {
notifications.show({
title: "Could not clear the stored OAuth state",
message: err instanceof Error ? err.message : String(err),
color: "red",
// The tokens may still be on disk and the session may still be up,
// so this is not a notice to let time out.
autoClose: false,
});
});
},
[clearServerOAuthAndDisconnect],
);
const handleClearConnectionOAuth = useCallback(() => {
if (!activeServer) return;
void clearServerOAuthAndDisconnect(activeServer);
}, [activeServer, clearServerOAuthAndDisconnect]);
runClear(activeServer);
}, [activeServer, runClear]);
const handleClearStoredOAuthFromSettings = useCallback(() => {
if (!settingsModalTarget) return;
// Clear from *inside* the settings modal, so the draft is what the user is
// looking at rather than what the debounced save has persisted (#2144).
void clearServerOAuthAndDisconnect(
serverWithDraftSettings(settingsModalTarget, settingsDraft),
);
}, [settingsModalTarget, settingsDraft, clearServerOAuthAndDisconnect]);
runClear(serverWithDraftSettings(settingsModalTarget, settingsDraft));
}, [settingsModalTarget, settingsDraft, runClear]);
const onSettingsModalClose = useCallback(() => {
flushSettingsDraft();