From 3f4b80bcdc756bcb67bae27bd34b96d82bb492d8 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 19:53:32 +0000 Subject: [PATCH] fix: unblock hung Log Out and Mingo archive TypeError Two confirmation modals never settled. Log Out: `logoutAsync` fired a `fetch` with no timeout, and `performLogout` awaited it before it cleared local state or redirected. A hung request never rejected, so the modal spinner stayed forever and the session stayed live. Bound the call with an AbortController timeout so sign-out always proceeds. Mingo dialog helper: `runDialogMutation` and the archived-list fetch read `response.data.data[key]` without checking the GraphQL `errors` array or a null `data`. A failed mutation returns HTTP 200 with `data: null`, so the code threw a raw TypeError. Check `errors` first and guard `data` before indexing it. Generated-By: PostHog Desktop Task-Id: 11ac13fe-6342-49cb-8ee3-48119bbee96e --- .../mingo/hooks/use-mingo-dialog-actions.ts | 20 ++++++++++++++++--- src/lib/auth-api-client.ts | 11 ++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/app/(app)/mingo/hooks/use-mingo-dialog-actions.ts b/src/app/(app)/mingo/hooks/use-mingo-dialog-actions.ts index 1d763d84..119c7801 100644 --- a/src/app/(app)/mingo/hooks/use-mingo-dialog-actions.ts +++ b/src/app/(app)/mingo/hooks/use-mingo-dialog-actions.ts @@ -29,14 +29,22 @@ interface FetchArchivedResult { } async function runDialogMutation(query: string, variables: Record, key: string): Promise { - const response = await apiClient.post<{ data: Record }>('/chat/graphql', { + const response = await apiClient.post<{ + data: Record | null; + errors?: { message: string }[]; + }>('/chat/graphql', { query, variables, }); if (!response.ok || !response.data) { throw new Error(response.error || 'Request failed'); } - const payload = response.data.data[key]; + // A GraphQL-level failure returns HTTP 200 with `data: null` and an `errors` + // array. Surface the server's message instead of dereferencing null `data`. + if (response.data.errors?.length) { + throw new Error(response.data.errors[0].message); + } + const payload = response.data.data?.[key]; if (payload?.userErrors?.length) { throw new Error(payload.userErrors[0].message); } @@ -118,7 +126,7 @@ export function useMingoDialogActions() { const fetchArchivedDialogs = useCallback( async (params: FetchArchivedParams): Promise => { const runFetch = async (): Promise => { - const response = await apiClient.post('/chat/graphql', { + const response = await apiClient.post('/chat/graphql', { query: GET_MINGO_DIALOGS_QUERY, variables: { filter: { agentTypes: ['ADMIN'], statuses: ['ARCHIVED'] }, @@ -129,6 +137,12 @@ export function useMingoDialogActions() { if (!response.ok || !response.data) { throw new Error(response.error || 'Failed to fetch archived chats'); } + // A GraphQL-level failure returns HTTP 200 with `data: null` and an + // `errors` array. Surface the server's message instead of dereferencing + // null `data`. + if (response.data.errors?.length) { + throw new Error(response.data.errors[0].message); + } const { edges, pageInfo } = response.data.data.dialogs; return { dialogs: edges.map(edge => ({ diff --git a/src/lib/auth-api-client.ts b/src/lib/auth-api-client.ts index ce16000f..995519ca 100644 --- a/src/lib/auth-api-client.ts +++ b/src/lib/auth-api-client.ts @@ -39,6 +39,9 @@ function getDomainSuffix(): string { export const SAAS_DOMAIN_SUFFIX = getDomainSuffix(); +/** Upper bound on the server-side logout call before sign-out proceeds anyway. */ +const LOGOUT_TIMEOUT_MS = 5000; + export interface AuthApiResponse { data?: T; error?: string; @@ -350,16 +353,24 @@ class AuthApiClient { } } + // Bound the server call: a hung request must not block the caller, which + // waits on this before it clears local state and redirects. On timeout the + // abort rejects the fetch, this returns false, and sign-out still finishes. + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), LOGOUT_TIMEOUT_MS); try { await fetch(logoutUrl, { method: 'GET', credentials: 'include', redirect: 'manual', headers, + signal: controller.signal, }); return true; } catch { return false; + } finally { + clearTimeout(timeout); } } }