diff --git a/src/app/configure-tasks-app/ui/Subtemplates.tsx b/src/app/configure-tasks-app/ui/Subtemplates.tsx index f1742287a..dfae06283 100644 --- a/src/app/configure-tasks-app/ui/Subtemplates.tsx +++ b/src/app/configure-tasks-app/ui/Subtemplates.tsx @@ -53,7 +53,8 @@ export const Subtemplates = ({ template_id, token }: { template_id: string; toke const { mutate } = useSWRConfig() - const _debounceMutate = async (cacheKey: string) => await mutate(cacheKey) + const _debounceMutate = (cacheKey: string) => + mutate(cacheKey).catch((error) => console.error('Failed to revalidate subtemplates:', error)) const debounceMutate = useDebounce(_debounceMutate, 200) useEffect(() => { @@ -68,7 +69,7 @@ export const Subtemplates = ({ template_id, token }: { template_id: string; toke debounceMutate(cacheKey) }, [activeTemplate?.subTaskTemplates]) - const handleSubtemplateCreation = (payload: CreateTemplateRequest) => { + const handleSubtemplateCreation = async (payload: CreateTemplateRequest) => { const tempId = generateRandomString('temp-template') setOptimisticUpdates((prev) => [ ...prev, @@ -90,7 +91,7 @@ export const Subtemplates = ({ template_id, token }: { template_id: string; toke const optimisticData = sortTaskByDescendingOrder([...currentSubtemplates, tempSubtemplate]) try { - mutate( + await mutate( cacheKey, async () => { const subTask = await createSubTemplate(token, template_id, payload) diff --git a/src/app/detail/ui/ActivityWrapper.tsx b/src/app/detail/ui/ActivityWrapper.tsx index 8bd1b1ff9..672c2c2a9 100644 --- a/src/app/detail/ui/ActivityWrapper.tsx +++ b/src/app/detail/ui/ActivityWrapper.tsx @@ -55,7 +55,8 @@ export const ActivityWrapper = ({ useScrollToElement('commentId') - const _debounceMutate = async (cacheKey: string) => await mutate(cacheKey) + const _debounceMutate = (cacheKey: string) => + mutate(cacheKey).catch((error) => console.error('Failed to revalidate activity logs:', error)) const debounceMutate = useDebounce(_debounceMutate, 300) const shouldRefetchRef = useRef(true) //preventing double fetching from comment apis. Due to optimistic update revalidation, we are already fetching logs there. So no need to refetch in case for comment creation and deletion. @@ -102,7 +103,7 @@ export const ActivityWrapper = ({ const optimisticData = getOptimisticData(postCommentPayload, activities.data, tempLog) try { - mutate( + await mutate( cacheKey, async () => { shouldRefetchRef.current = false diff --git a/src/app/detail/ui/Subtasks.tsx b/src/app/detail/ui/Subtasks.tsx index 51c45cf86..579a5311e 100644 --- a/src/app/detail/ui/Subtasks.tsx +++ b/src/app/detail/ui/Subtasks.tsx @@ -67,7 +67,8 @@ export const Subtasks = ({ const { mutate } = useSWRConfig() - const _debounceMutate = async (cacheKey: string) => await mutate(cacheKey) + const _debounceMutate = (cacheKey: string) => + mutate(cacheKey).catch((error) => console.error('Failed to revalidate subtasks:', error)) const debounceMutate = useDebounce(_debounceMutate, 200) useEffect(() => { @@ -84,7 +85,7 @@ export const Subtasks = ({ setLastUpdated(activeTask?.lastSubtaskUpdated) }, [activeTask?.lastSubtaskUpdated]) - const handleSubTaskCreation = (payload: CreateTaskRequest) => { + const handleSubTaskCreation = async (payload: CreateTaskRequest) => { const tempId = generateRandomString('temp-task') setOptimisticUpdates((prev) => [ ...prev, @@ -104,7 +105,7 @@ export const Subtasks = ({ ) const optimisticData = subTasks?.tasks ? sortSubtasksByPriority([...subTasks.tasks, tempSubtask]) : [tempSubtask] try { - mutate( + await mutate( cacheKey, async () => { const subTask = await handleCreate(token, payload, { disableSubtaskTemplates: true }) diff --git a/src/utils/fetcher.test.ts b/src/utils/fetcher.test.ts new file mode 100644 index 000000000..d86f9b44d --- /dev/null +++ b/src/utils/fetcher.test.ts @@ -0,0 +1,45 @@ +import { fetcher } from '@/utils/fetcher' + +describe('fetcher', () => { + const originalFetch = global.fetch + + afterEach(() => { + global.fetch = originalFetch + jest.restoreAllMocks() + }) + + it('returns parsed JSON for successful responses', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: async () => ({ tasks: [] }), + }) + + await expect(fetcher('/api/tasks/?token=test')).resolves.toEqual({ tasks: [] }) + }) + + it('throws an error with status, path, and response body, without the token', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: false, + status: 500, + text: async () => 'Internal server error', + }) + + await expect(fetcher('/api/tasks/?token=secret-token')).rejects.toThrow( + 'An error occurred while fetching the data. [500] /api/tasks/: Internal server error', + ) + }) + + it('attaches the response status to the error so onErrorRetry can skip 404s', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: false, + status: 404, + text: async () => '', + }) + + await expect(fetcher('/api/tasks/?token=test')).rejects.toMatchObject({ status: 404 }) + }) + + it('returns undefined when url is null', async () => { + await expect(fetcher(null)).resolves.toBeUndefined() + }) +}) diff --git a/src/utils/fetcher.ts b/src/utils/fetcher.ts index 99f9dfe43..a9494d186 100644 --- a/src/utils/fetcher.ts +++ b/src/utils/fetcher.ts @@ -4,8 +4,13 @@ export const fetcher = async (url: string | null) => { const res = await fetch(url) if (!res.ok) { - const error = new Error('An error occurred while fetching the data.') - throw error + const body = await res.text().catch(() => '') + // url carries the Copilot session token in its query string — keep it out of error reporting + const path = url.split('?')[0] + const error = new Error( + `An error occurred while fetching the data. [${res.status}] ${path}${body ? `: ${body.slice(0, 200)}` : ''}`, + ) + throw Object.assign(error, { status: res.status }) } return res.json()