Skip to content

Commit c0dc544

Browse files
fix(dashboard): re-arm the 401 latch once the session recovers
unauthorizedHandled was set once per client and never cleared, so it collapsed every later 401 too, not just the concurrent ones it was meant to. Harmless for the shipped default handler, which navigates away and takes the client with it, but the onUnauthorized hook exists precisely so a caller can re-auth in place, and such a handler would fire exactly once for the client's lifetime and then go silent. Clear the latch on any 2xx from either fetch site: a success means the session is alive again, so the next 401 is a new episode. The storm guard still holds, since concurrent 401s have no success between them. Assisted-By: Claude <[email protected]> Signed-off-by: Myasnikov Daniil <[email protected]>
1 parent 5cbd30c commit c0dc544

2 files changed

Lines changed: 30 additions & 0 deletions

File tree

‎packages/system/dashboard/images/console/apps/console/src/__tests__/k8s-client/client.unauthorized.test.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,31 @@ describe("K8sClient onUnauthorized (401) handling", () => {
5252
expect(onUnauthorized).toHaveBeenCalledTimes(1)
5353
})
5454

55+
it("re-arms the latch after a success so a later 401 handles again", async () => {
56+
const onUnauthorized = vi.fn()
57+
const responses = [
58+
unauthorized(),
59+
fakeResponse({ body: JSON.stringify({ kind: "Pod" }) }),
60+
unauthorized(),
61+
]
62+
let call = 0
63+
vi.stubGlobal("fetch", vi.fn(async () => responses[call++]))
64+
const client = new K8sClient({ onUnauthorized })
65+
66+
// A 2xx in between means the session recovered, so the next expiry is a
67+
// new episode. Without the re-arm a custom in-page handler would fire
68+
// once for the client's whole lifetime.
69+
await expect(client.get("", "v1", "pods", "p", "ns")).rejects.toThrow(
70+
K8sApiError,
71+
)
72+
await client.get("", "v1", "pods", "p", "ns")
73+
await expect(client.get("", "v1", "pods", "p", "ns")).rejects.toThrow(
74+
K8sApiError,
75+
)
76+
77+
expect(onUnauthorized).toHaveBeenCalledTimes(2)
78+
})
79+
5580
it("does not invoke onUnauthorized on a non-401 error", async () => {
5681
const onUnauthorized = vi.fn()
5782
vi.stubGlobal(

‎packages/system/dashboard/images/console/packages/k8s-client/src/client.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ export class K8sClient {
8686
throw new K8sApiError(res.status, body)
8787
}
8888

89+
// A 2xx means the session is alive again, so re-arm the latch: the default
90+
// handler navigates away, but a custom one may re-auth in place.
91+
this.unauthorizedHandled = false
92+
8993
if (res.status === 204) return undefined as T
9094
// Some endpoints (e.g. KubeVirt action subresources like
9195
// virtualmachines/{name}/restart) return 2xx with an empty body;
@@ -282,6 +286,7 @@ export class K8sClient {
282286
await res.json().catch(() => res.statusText),
283287
)
284288
}
289+
this.unauthorizedHandled = false
285290
if (!res.body) throw new Error("No response body for watch")
286291

287292
const reader = res.body.getReader()

0 commit comments

Comments
 (0)