Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions src/app/(app)/mingo/hooks/use-mingo-dialog-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,22 @@ interface FetchArchivedResult {
}

async function runDialogMutation(query: string, variables: Record<string, unknown>, key: string): Promise<void> {
const response = await apiClient.post<{ data: Record<string, DialogMutationPayload> }>('/chat/graphql', {
const response = await apiClient.post<{
data: Record<string, DialogMutationPayload> | 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);
}
Expand Down Expand Up @@ -118,7 +126,7 @@ export function useMingoDialogActions() {
const fetchArchivedDialogs = useCallback(
async (params: FetchArchivedParams): Promise<FetchArchivedResult> => {
const runFetch = async (): Promise<FetchArchivedResult> => {
const response = await apiClient.post<DialogsResponse>('/chat/graphql', {
const response = await apiClient.post<DialogsResponse & { errors?: { message: string }[] }>('/chat/graphql', {
query: GET_MINGO_DIALOGS_QUERY,
variables: {
filter: { agentTypes: ['ADMIN'], statuses: ['ARCHIVED'] },
Expand All @@ -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 => ({
Expand Down
11 changes: 11 additions & 0 deletions src/lib/auth-api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T = any> {
data?: T;
error?: string;
Expand Down Expand Up @@ -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);
}
}
}
Expand Down