Skip to content

Commit 4245355

Browse files
authored
Merge pull request #280 from datum-cloud/fix/272-cursor-error
fix(data-table): Auto-handle expired cursor tokens (410 errors)
2 parents a6ad7f6 + 25d68ff commit 4245355

File tree

5 files changed

+96
-7
lines changed

5 files changed

+96
-7
lines changed

app/modules/axios/axios.client.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isExpiredCursorError } from '@/modules/datum-ui/data-table/lib/data-table';
12
import { logger } from '@/utils/logger';
23
import { captureApiError } from '@/utils/logger';
34
import { toast } from '@datum-ui/toast';
@@ -102,9 +103,14 @@ const onResponseError = (error: AxiosError): Promise<AxiosError> => {
102103
responseData: error.response?.data,
103104
});
104105

105-
// For all other errors, show toast with meaningful info
106-
const title = errorInfo.requestId ? `Request ID: ${errorInfo.requestId}` : 'Error';
107-
toast.error(title, { description: errorInfo.message });
106+
// Check if this is an expired cursor error (410) - these are handled automatically by data tables
107+
const isExpiredCursor = isExpiredCursorError(error);
108+
// Skip toast for expired cursor errors - they're handled automatically by data tables
109+
if (!isExpiredCursor) {
110+
// For all other errors, show toast with meaningful info
111+
const title = errorInfo.requestId ? `Request ID: ${errorInfo.requestId}` : 'Error';
112+
toast.error(title, { description: errorInfo.message });
113+
}
108114

109115
return Promise.reject(error);
110116
};

app/modules/datum-ui/data-table/components/data-table.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getCommonPinningStyles } from '../lib/data-table';
1+
import { getCommonPinningStyles, isExpiredCursorError } from '../lib/data-table';
22
import { useDataTableInstance } from '../providers/data-table.provider';
33
import { DataTableLoading } from './data-table-loading';
44
import { DataTablePagination } from './data-table-pagination';
@@ -28,16 +28,18 @@ export function DataTable<TData>({
2828
}: DataTableProps<TData>) {
2929
const { table, query } = useDataTableInstance<TData>();
3030

31-
// Show loading state when query is loading
32-
if (query.isLoading) {
31+
// Show loading state when query is loading or when we're handling an expired cursor error
32+
// (expired cursor errors are automatically handled by clearing the cursor and refetching)
33+
const isExpiredCursor = query.isError && isExpiredCursorError(query.error);
34+
if (query.isLoading || isExpiredCursor) {
3335
return (
3436
<DataTableLoading<TData> rows={5} actionBar={actionBar} className={className} {...props}>
3537
{children}
3638
</DataTableLoading>
3739
);
3840
}
3941

40-
// Show error state when query has error
42+
// Show error state when query has error (but not expired cursor errors, which are handled automatically)
4143
if (query.isError) {
4244
return (
4345
<div className={cn('flex w-full flex-col gap-2.5 overflow-auto', className)} {...props}>

app/modules/datum-ui/data-table/hooks/useDataTableQuery.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isExpiredCursorError } from '../lib/data-table';
12
import { ListQueryParams } from '@/resources/schemas';
23
import { keepPreviousData, useQuery } from '@tanstack/react-query';
34
import {
@@ -245,8 +246,26 @@ export function useDataTableQuery<T>({
245246
}),
246247
placeholderData: keepPreviousData,
247248
enabled,
249+
retry: (failureCount, error) => {
250+
if (isExpiredCursorError(error)) {
251+
return false;
252+
}
253+
return failureCount < 3;
254+
},
248255
});
249256

257+
// Auto-handle expired cursor tokens
258+
useEffect(() => {
259+
if (!cursor || !query.isError) {
260+
return;
261+
}
262+
263+
const error = query.error;
264+
if (isExpiredCursorError(error)) {
265+
setCursor('');
266+
}
267+
}, [query.isError, query.error, cursor, setCursor]);
268+
250269
// --- Memoized Setters ---
251270
const setSorting = useMemo(() => {
252271
if (!useSorting) return undefined;

app/modules/datum-ui/data-table/lib/data-table.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,57 @@ export function getValidFilters<TData>(
6464
: filter.value !== '' && filter.value !== null && filter.value !== undefined)
6565
);
6666
}
67+
68+
/**
69+
* Checks if an error is a 410 ResourceExpired error (expired cursor token).
70+
* This is used to automatically handle expired pagination tokens by clearing
71+
* the cursor and refetching from the beginning.
72+
*/
73+
export function isExpiredCursorError(error: unknown): boolean {
74+
// Check for AxiosError with 410 status
75+
if (error && typeof error === 'object' && 'response' in error) {
76+
const axiosError = error as { response?: { status?: number; data?: any } };
77+
if (axiosError.response?.status === 410) {
78+
return true;
79+
}
80+
// Also check error message in response data
81+
if (axiosError.response?.data) {
82+
const data = axiosError.response.data;
83+
const errorMessage =
84+
typeof data === 'string' ? data : (data as any)?.error || (data as any)?.message || '';
85+
if (errorMessage) {
86+
const message = String(errorMessage).toLowerCase();
87+
if (
88+
message.includes('continue') ||
89+
message.includes('token') ||
90+
message.includes('too old') ||
91+
message.includes('expired') ||
92+
message.includes('provided continue parameter')
93+
) {
94+
return true;
95+
}
96+
}
97+
}
98+
}
99+
100+
// Check for error message containing ResourceExpired or 410
101+
if (error instanceof Error) {
102+
const message = error.message.toLowerCase();
103+
return (
104+
message.includes('410') ||
105+
message.includes('resourceexpired') ||
106+
message.includes('continue token') ||
107+
message.includes('continue parameter') ||
108+
message.includes('provided continue parameter') ||
109+
message.includes('too old') ||
110+
message.includes('inconsistent list')
111+
);
112+
}
113+
114+
// Check for error object with status code
115+
if (error && typeof error === 'object' && 'status' in error) {
116+
return (error as { status?: number }).status === 410;
117+
}
118+
119+
return false;
120+
}

app/modules/datum-ui/data-table/providers/data-table.provider.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ export function DataTableProvider<TData, TQuery = DataTableQuery<TData>>({
141141
}
142142
}, [sorting, filters, limit]);
143143

144+
// Reset pagination when cursor is cleared (e.g., expired token)
145+
useEffect(() => {
146+
if (!cursor && currentPage > 0) {
147+
setCursorHistory(['']);
148+
setCurrentPage(0);
149+
}
150+
}, [cursor, currentPage]);
151+
144152
// Add selection to first column (embedded) and actions column (separate)
145153
const enhancedColumns = useMemo(() => {
146154
const hasActions = actions && actions.length > 0;

0 commit comments

Comments
 (0)