Skip to content

Commit 74867f3

Browse files
committed
Move retry classification upstream
1 parent 500103f commit 74867f3

4 files changed

Lines changed: 123 additions & 107 deletions

File tree

frontend/src/__tests__/App.test.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,13 @@ describe('App', () => {
478478
isLoading: false,
479479
error: undefined,
480480
});
481-
mockConvertFeed.mockRejectedValueOnce(new Error('Unauthorized'));
481+
mockConvertFeed.mockRejectedValueOnce(
482+
Object.assign(new Error('Unauthorized'), {
483+
code: 'UNAUTHORIZED',
484+
status: 401,
485+
kind: 'auth',
486+
})
487+
);
482488

483489
render(<App />);
484490

@@ -506,7 +512,13 @@ describe('App', () => {
506512
isLoading: false,
507513
error: undefined,
508514
});
509-
mockConvertFeed.mockRejectedValueOnce(new Error('Unauthorized'));
515+
mockConvertFeed.mockRejectedValueOnce(
516+
Object.assign(new Error('Unauthorized'), {
517+
code: 'UNAUTHORIZED',
518+
status: 401,
519+
kind: 'auth',
520+
})
521+
);
510522

511523
render(<App />);
512524

@@ -574,6 +586,9 @@ describe('App', () => {
574586
mockConvertFeed
575587
.mockRejectedValueOnce(
576588
Object.assign(new Error('Tried faraday first, then browserless. Browserless failed.'), {
589+
code: 'INTERNAL_SERVER_ERROR',
590+
status: 502,
591+
kind: 'server',
577592
retryAction: 'alternate',
578593
manualRetryStrategy: 'browserless',
579594
})
@@ -611,6 +626,9 @@ describe('App', () => {
611626
mockConvertFeed
612627
.mockRejectedValueOnce(
613628
Object.assign(new Error('Tried faraday first, then browserless. Browserless failed.'), {
629+
code: 'INTERNAL_SERVER_ERROR',
630+
status: 502,
631+
kind: 'server',
614632
retryAction: 'primary',
615633
})
616634
)
@@ -647,6 +665,9 @@ describe('App', () => {
647665
});
648666
mockConvertFeed.mockRejectedValueOnce(
649667
Object.assign(new Error('URL not allowed for this account'), {
668+
code: 'FORBIDDEN',
669+
status: 403,
670+
kind: 'server',
650671
retryAction: undefined,
651672
})
652673
);

frontend/src/__tests__/useFeedConversion.test.ts

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -171,13 +171,21 @@ describe('useFeedConversion', () => {
171171
fetchMock.mockRejectedValueOnce(new Error('Network error'));
172172

173173
const { result } = renderHook(() => useFeedConversion());
174-
let thrownError: (Error & { manualRetryStrategy?: string; retryAction?: string }) | undefined;
174+
let thrownError:
175+
| (Error & { manualRetryStrategy?: string; retryAction?: string; kind?: string; code?: string; status?: number })
176+
| undefined;
175177

176178
await act(async () => {
177179
try {
178180
await result.current.convertFeed('https://example.com', 'faraday', 'testtoken');
179181
} catch (error) {
180-
thrownError = error as Error & { manualRetryStrategy?: string; retryAction?: string };
182+
thrownError = error as Error & {
183+
manualRetryStrategy?: string;
184+
retryAction?: string;
185+
kind?: string;
186+
code?: string;
187+
status?: number;
188+
};
181189
}
182190
});
183191

@@ -187,6 +195,7 @@ describe('useFeedConversion', () => {
187195
expect(thrownError?.message).toBe('Network error');
188196
expect(thrownError?.manualRetryStrategy).toBe('browserless');
189197
expect(thrownError?.retryAction).toBe('alternate');
198+
expect(thrownError?.kind).toBe('network');
190199
});
191200

192201
it('preserves the created feed when preview loading fails after feed creation', async () => {
@@ -623,7 +632,7 @@ describe('useFeedConversion', () => {
623632
new Response(
624633
JSON.stringify({
625634
success: false,
626-
error: { message: 'Unauthorized' },
635+
error: { code: 'UNAUTHORIZED', message: 'Unauthorized' },
627636
}),
628637
{
629638
status: 401,
@@ -633,13 +642,21 @@ describe('useFeedConversion', () => {
633642
);
634643

635644
const { result } = renderHook(() => useFeedConversion());
636-
let thrownError: (Error & { manualRetryStrategy?: string; retryAction?: string }) | undefined;
645+
let thrownError:
646+
| (Error & { manualRetryStrategy?: string; retryAction?: string; kind?: string; code?: string; status?: number })
647+
| undefined;
637648

638649
await act(async () => {
639650
try {
640651
await result.current.convertFeed('https://example.com/articles', 'faraday', 'testtoken');
641652
} catch (error) {
642-
thrownError = error as Error & { manualRetryStrategy?: string; retryAction?: string };
653+
thrownError = error as Error & {
654+
manualRetryStrategy?: string;
655+
retryAction?: string;
656+
kind?: string;
657+
code?: string;
658+
status?: number;
659+
};
643660
}
644661
});
645662

@@ -649,6 +666,9 @@ describe('useFeedConversion', () => {
649666
expect(thrownError?.message).toBe('Unauthorized');
650667
expect(thrownError?.manualRetryStrategy).toBeUndefined();
651668
expect(thrownError?.retryAction).toBeUndefined();
669+
expect(thrownError?.kind).toBe('auth');
670+
expect(thrownError?.code).toBe('UNAUTHORIZED');
671+
expect(thrownError?.status).toBeUndefined();
652672
});
653673

654674
it('does not auto-retry when API returns a non-retryable BAD_REQUEST code', async () => {
@@ -666,13 +686,21 @@ describe('useFeedConversion', () => {
666686
);
667687

668688
const { result } = renderHook(() => useFeedConversion());
669-
let thrownError: (Error & { manualRetryStrategy?: string; retryAction?: string }) | undefined;
689+
let thrownError:
690+
| (Error & { manualRetryStrategy?: string; retryAction?: string; kind?: string; code?: string; status?: number })
691+
| undefined;
670692

671693
await act(async () => {
672694
try {
673695
await result.current.convertFeed('https://example.com/articles', 'faraday', 'testtoken');
674696
} catch (error) {
675-
thrownError = error as Error & { manualRetryStrategy?: string; retryAction?: string };
697+
thrownError = error as Error & {
698+
manualRetryStrategy?: string;
699+
retryAction?: string;
700+
kind?: string;
701+
code?: string;
702+
status?: number;
703+
};
676704
}
677705
});
678706

@@ -682,6 +710,9 @@ describe('useFeedConversion', () => {
682710
expect(thrownError?.message).toBe('Input rejected');
683711
expect(thrownError?.manualRetryStrategy).toBeUndefined();
684712
expect(thrownError?.retryAction).toBeUndefined();
713+
expect(thrownError?.kind).toBe('input');
714+
expect(thrownError?.code).toBe('BAD_REQUEST');
715+
expect(thrownError?.status).toBeUndefined();
685716
});
686717

687718
it('still auto-retries when API returns INTERNAL_SERVER_ERROR even if message contains a url', async () => {
@@ -781,12 +812,20 @@ describe('useFeedConversion', () => {
781812

782813
const { result } = renderHook(() => useFeedConversion());
783814

784-
let thrownError: (Error & { manualRetryStrategy?: string; retryAction?: string }) | undefined;
815+
let thrownError:
816+
| (Error & { manualRetryStrategy?: string; retryAction?: string; kind?: string; code?: string; status?: number })
817+
| undefined;
785818
await act(async () => {
786819
try {
787820
await result.current.convertFeed('https://example.com/articles', 'faraday', 'testtoken');
788821
} catch (error) {
789-
thrownError = error as Error & { manualRetryStrategy?: string; retryAction?: string };
822+
thrownError = error as Error & {
823+
manualRetryStrategy?: string;
824+
retryAction?: string;
825+
kind?: string;
826+
code?: string;
827+
status?: number;
828+
};
790829
}
791830
});
792831

@@ -795,6 +834,7 @@ describe('useFeedConversion', () => {
795834
);
796835
expect(thrownError?.manualRetryStrategy).toBeUndefined();
797836
expect(thrownError?.retryAction).toBe('primary');
837+
expect(thrownError?.kind).toBe('server');
798838
expect(result.current.result).toBeUndefined();
799839
expect(result.current.error).toBe(
800840
'Tried faraday first, then browserless. First attempt failed with: Upstream timeout. Second attempt failed with: Browserless also failed'

0 commit comments

Comments
 (0)