fix(web): open the monitoring sidebar when a connect fails during OAuth (#2108)
The failure column was gated on the `"error"` connection status, which an OAuth failure never reaches: the parent drives the auth legs itself and tears the client down on failure, so the session settles at `"disconnected"`. The sidebar therefore stayed shut on exactly the failure the Network tab exists to explain — the case #2048 was closed on. Gate it on `erroredServerId` instead, the parent's canonical "this server's last connect attempt failed" flag, and set that flag on every OAuth connect-attempt failure: the pre-redirect discovery/DCR leg, the callback leg's token exchange, an OAuth-storage load failure, and a provider that redirects back with an error instead of a code. Nothing else was needed to make the requests reachable. `auth` fetches are already tracked into the Network store, which does not clear on disconnect and is restored from the session on the callback leg, so discovery, DCR and the token exchange are all present. Tab selection stays content-gated rather than hard-coded, so Network leads because it is the one that captured something. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
9d0448b644
commit
f7224144ec
2 changed files with 211 additions and 3 deletions
|
|
@ -1689,6 +1689,87 @@ describe("InspectorView", () => {
|
|||
expect(screen.queryByRole("radio", { name: "Console" })).toBeNull();
|
||||
});
|
||||
|
||||
// An OAuth failure is torn down by the parent, so the session rests at
|
||||
// "disconnected" rather than "error" — the status gate the failure column
|
||||
// used to carry never saw it, and the requests explaining the failure were
|
||||
// unreachable (#2108). The parent's `erroredServerId` is the signal instead.
|
||||
it("opens the monitoring sidebar to Network when OAuth fails without an error status (#2108)", async () => {
|
||||
const { rerender } = renderWithMantine(
|
||||
<StatefulInspectorViewHost
|
||||
{...makeProps({
|
||||
servers: [httpServer],
|
||||
activeServer: "beta",
|
||||
connectionStatus: "connecting",
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Close monitoring sidebar" }),
|
||||
).toBeNull();
|
||||
|
||||
rerender(
|
||||
<StatefulInspectorViewHost
|
||||
{...makeProps({
|
||||
servers: [httpServer],
|
||||
// The failure's teardown clears the active server and settles the
|
||||
// session at "disconnected" — never "error".
|
||||
activeServer: undefined,
|
||||
erroredServerId: "beta",
|
||||
connectionStatus: "disconnected",
|
||||
initializeResult: undefined,
|
||||
network: [
|
||||
{
|
||||
id: "a1",
|
||||
timestamp: new Date(),
|
||||
method: "GET",
|
||||
url: "https://as.example.com/.well-known/oauth-authorization-server",
|
||||
requestHeaders: {},
|
||||
responseStatus: 404,
|
||||
responseStatusText: "Not Found",
|
||||
category: "auth",
|
||||
},
|
||||
],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByRole("button", { name: "Close monitoring sidebar" }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("radio", { name: "Network" })).toBeChecked();
|
||||
});
|
||||
|
||||
it("does not re-open the sidebar on a mount that starts already flagged (#2108)", () => {
|
||||
// Same props as the failure above, but as the *initial* render: there is
|
||||
// no transition, so a user who closed the column isn't fought on remount.
|
||||
renderWithMantine(
|
||||
<StatefulInspectorViewHost
|
||||
{...makeProps({
|
||||
servers: [httpServer],
|
||||
activeServer: undefined,
|
||||
erroredServerId: "beta",
|
||||
connectionStatus: "disconnected",
|
||||
initializeResult: undefined,
|
||||
network: [
|
||||
{
|
||||
id: "a1",
|
||||
timestamp: new Date(),
|
||||
method: "GET",
|
||||
url: "https://as.example.com/.well-known/oauth-authorization-server",
|
||||
requestHeaders: {},
|
||||
responseStatus: 404,
|
||||
responseStatusText: "Not Found",
|
||||
category: "auth",
|
||||
},
|
||||
],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Close monitoring sidebar" }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("surfaces Console (stderr), not Network, in the failure column for a stdio server (#1621)", async () => {
|
||||
const stdioErr: ServerEntry = {
|
||||
id: "beta",
|
||||
|
|
|
|||
|
|
@ -388,6 +388,13 @@ export interface InspectorViewProps {
|
|||
* the Servers screen draws a red border until another server is connected or
|
||||
* a new connection is attempted. Independent of `activeServer`, which the
|
||||
* parent clears on the failure's `disconnect` event.
|
||||
*
|
||||
* It is also the sole signal that opens the monitoring sidebar onto the
|
||||
* failure's diagnostics (#2108). That used to be gated on the `"error"`
|
||||
* connection status, which an OAuth failure never reaches — the parent drives
|
||||
* that leg and tears the client down itself, settling the session at
|
||||
* `"disconnected"`. So the parent must set this for *every* connect-attempt
|
||||
* failure, auth legs included, and clear it as each new attempt starts.
|
||||
*/
|
||||
erroredServerId?: string;
|
||||
/**
|
||||
|
|
@ -817,18 +824,33 @@ export function InspectorView({
|
|||
// connecting/disconnected → error, never connected → error. This keeps the
|
||||
// auto-open aligned with the red-border (`erroredServerId`), which the parent
|
||||
// also sets only for connect-attempt failures.
|
||||
//
|
||||
// A connect attempt that fails *during OAuth* (#2108) never reaches the
|
||||
// `"error"` status at all: the auth leg is driven from the parent, which
|
||||
// tears the client down on failure, so the session settles at
|
||||
// `"disconnected"`. So the failure arm also fires on the parent raising
|
||||
// `erroredServerId` — the canonical "this server's last connect attempt
|
||||
// failed" flag, which the parent clears at the start of every new attempt.
|
||||
// Tracked through a ref for the same reason as the status: a mount that
|
||||
// starts already flagged must not re-open a column the user closed.
|
||||
const prevStatusRef = useRef(connectionStatus);
|
||||
const prevErroredServerIdRef = useRef(erroredServerId);
|
||||
useEffect(() => {
|
||||
const prev = prevStatusRef.current;
|
||||
prevStatusRef.current = connectionStatus;
|
||||
const prevErrored = prevErroredServerIdRef.current;
|
||||
prevErroredServerIdRef.current = erroredServerId;
|
||||
const becameConnected =
|
||||
connectionStatus === "connected" && prev !== "connected";
|
||||
const becameError =
|
||||
connectionStatus === "error" && prev !== "error" && prev !== "connected";
|
||||
(connectionStatus === "error" &&
|
||||
prev !== "error" &&
|
||||
prev !== "connected") ||
|
||||
(erroredServerId !== undefined && prevErrored !== erroredServerId);
|
||||
if (becameConnected || becameError) {
|
||||
setMonitorPinned(true);
|
||||
}
|
||||
}, [connectionStatus, setMonitorPinned]);
|
||||
}, [connectionStatus, erroredServerId, setMonitorPinned]);
|
||||
|
||||
const appTools = useMemo<Tool[]>(() => {
|
||||
return tools.filter((tool) => {
|
||||
|
|
@ -924,7 +946,14 @@ export function InspectorView({
|
|||
// (set by the parent only for connect-attempt failures, not mid-session
|
||||
// crashes) so a crash of a previously-connected server doesn't reorganize the
|
||||
// column into the failure tab set — matching the auto-open effect above.
|
||||
const failed = connectionStatus === "error" && erroredServerId !== undefined;
|
||||
//
|
||||
// Keyed on `erroredServerId` alone rather than on the `"error"` status
|
||||
// (#2108): an OAuth leg that fails is torn down by the parent, so the session
|
||||
// rests at `"disconnected"` and a status gate would never see the failure —
|
||||
// which is exactly the case a server developer needs the Network tab for.
|
||||
// `!connected` is the only status condition left, and it holds vacuously
|
||||
// whenever the flag is set (the parent clears it as each new attempt starts).
|
||||
const failed = !connected && erroredServerId !== undefined;
|
||||
const monitorAvailable = useMemo<string[]>(() => {
|
||||
if (connected) return availableTabs.filter((t) => MONITOR_TABS.includes(t));
|
||||
if (failed) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue