Skip to content

Commit bb45f2c

Browse files
priosshrsthclaude
andcommitted
OUT-4109 | Catch debounced SWR revalidation rejections and drop token from fetcher errors
The unawaited mutate() fix missed the fire-and-forget debounceMutate(cacheKey) paths, which reject the same way when revalidation fails, and it made two non-async handlers await, so the branch did not typecheck. The enriched error also embedded the full URL, which carries the Copilot session token — that would have shipped a live credential to Sentry on every failed fetch. Log the path only and attach the status, which swr-config's onErrorRetry already reads to skip retrying 404s. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y4tF84tzJW7Bo1DehsDX22
1 parent 5ae9070 commit bb45f2c

5 files changed

Lines changed: 28 additions & 11 deletions

File tree

src/app/configure-tasks-app/ui/Subtemplates.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ export const Subtemplates = ({ template_id, token }: { template_id: string; toke
5353

5454
const { mutate } = useSWRConfig()
5555

56-
const _debounceMutate = async (cacheKey: string) => await mutate(cacheKey)
56+
const _debounceMutate = (cacheKey: string) =>
57+
mutate(cacheKey).catch((error) => console.error('Failed to revalidate subtemplates:', error))
5758
const debounceMutate = useDebounce(_debounceMutate, 200)
5859

5960
useEffect(() => {
@@ -68,7 +69,7 @@ export const Subtemplates = ({ template_id, token }: { template_id: string; toke
6869
debounceMutate(cacheKey)
6970
}, [activeTemplate?.subTaskTemplates])
7071

71-
const handleSubtemplateCreation = (payload: CreateTemplateRequest) => {
72+
const handleSubtemplateCreation = async (payload: CreateTemplateRequest) => {
7273
const tempId = generateRandomString('temp-template')
7374
setOptimisticUpdates((prev) => [
7475
...prev,

src/app/detail/ui/ActivityWrapper.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ export const ActivityWrapper = ({
5555

5656
useScrollToElement('commentId')
5757

58-
const _debounceMutate = async (cacheKey: string) => await mutate(cacheKey)
58+
const _debounceMutate = (cacheKey: string) =>
59+
mutate(cacheKey).catch((error) => console.error('Failed to revalidate activity logs:', error))
5960
const debounceMutate = useDebounce(_debounceMutate, 300)
6061

6162
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.

src/app/detail/ui/Subtasks.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ export const Subtasks = ({
6767

6868
const { mutate } = useSWRConfig()
6969

70-
const _debounceMutate = async (cacheKey: string) => await mutate(cacheKey)
70+
const _debounceMutate = (cacheKey: string) =>
71+
mutate(cacheKey).catch((error) => console.error('Failed to revalidate subtasks:', error))
7172
const debounceMutate = useDebounce(_debounceMutate, 200)
7273

7374
useEffect(() => {
@@ -84,7 +85,7 @@ export const Subtasks = ({
8485
setLastUpdated(activeTask?.lastSubtaskUpdated)
8586
}, [activeTask?.lastSubtaskUpdated])
8687

87-
const handleSubTaskCreation = (payload: CreateTaskRequest) => {
88+
const handleSubTaskCreation = async (payload: CreateTaskRequest) => {
8889
const tempId = generateRandomString('temp-task')
8990
setOptimisticUpdates((prev) => [
9091
...prev,

src/utils/fetcher.test.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,28 @@ describe('fetcher', () => {
1717
await expect(fetcher('/api/tasks/?token=test')).resolves.toEqual({ tasks: [] })
1818
})
1919

20-
it('throws an error with status, url, and response body for failed responses', async () => {
20+
it('throws an error with status, path, and response body, without the token', async () => {
2121
global.fetch = jest.fn().mockResolvedValue({
2222
ok: false,
2323
status: 500,
2424
text: async () => 'Internal server error',
2525
})
2626

27-
await expect(fetcher('/api/tasks/?token=test')).rejects.toThrow(
28-
'An error occurred while fetching the data. [500] /api/tasks/?token=test: Internal server error',
27+
await expect(fetcher('/api/tasks/?token=secret-token')).rejects.toThrow(
28+
'An error occurred while fetching the data. [500] /api/tasks/: Internal server error',
2929
)
3030
})
3131

32+
it('attaches the response status to the error so onErrorRetry can skip 404s', async () => {
33+
global.fetch = jest.fn().mockResolvedValue({
34+
ok: false,
35+
status: 404,
36+
text: async () => '',
37+
})
38+
39+
await expect(fetcher('/api/tasks/?token=test')).rejects.toMatchObject({ status: 404 })
40+
})
41+
3242
it('returns undefined when url is null', async () => {
3343
await expect(fetcher(null)).resolves.toBeUndefined()
3444
})

src/utils/fetcher.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ export const fetcher = async (url: string | null) => {
44
const res = await fetch(url)
55

66
if (!res.ok) {
7-
const responseText = await res.text().catch(() => '')
8-
const detail = responseText ? `: ${responseText.slice(0, 200)}` : ''
9-
throw new Error(`An error occurred while fetching the data. [${res.status}] ${url}${detail}`)
7+
const body = await res.text().catch(() => '')
8+
// url carries the Copilot session token in its query string — keep it out of error reporting
9+
const path = url.split('?')[0]
10+
const error = new Error(
11+
`An error occurred while fetching the data. [${res.status}] ${path}${body ? `: ${body.slice(0, 200)}` : ''}`,
12+
)
13+
throw Object.assign(error, { status: res.status })
1014
}
1115

1216
return res.json()

0 commit comments

Comments
 (0)