diff --git a/packages/query-core/src/__tests__/infiniteQueryBehavior.test.tsx b/packages/query-core/src/__tests__/infiniteQueryBehavior.test.tsx index 29367cad4f..1ca35fdfcf 100644 --- a/packages/query-core/src/__tests__/infiniteQueryBehavior.test.tsx +++ b/packages/query-core/src/__tests__/infiniteQueryBehavior.test.tsx @@ -2,7 +2,12 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { waitFor } from '@testing-library/react' import { CancelledError, InfiniteQueryObserver } from '..' import { createQueryClient, queryKey, sleep } from './utils' -import type { InfiniteQueryObserverResult, QueryCache, QueryClient } from '..' +import type { + InfiniteData, + InfiniteQueryObserverResult, + QueryCache, + QueryClient, +} from '..' describe('InfiniteQueryBehavior', () => { let queryClient: QueryClient @@ -323,4 +328,72 @@ describe('InfiniteQueryBehavior', () => { unsubscribe() }) + + test('InfiniteQueryBehavior should not enter an infinite loop when a page errors while retry is on #8046', async () => { + let errorCount = 0 + const key = queryKey() + + interface TestResponse { + data: Array<{ id: string }> + nextToken?: number + } + + const fakeData = [ + { data: [{ id: 'item-1' }], nextToken: 1 }, + { data: [{ id: 'item-2' }], nextToken: 2 }, + { data: [{ id: 'item-3' }], nextToken: 3 }, + { data: [{ id: 'item-4' }] }, + ] + + const fetchData = async ({ nextToken = 0 }: { nextToken?: number }) => + new Promise((resolve, reject) => { + setTimeout(() => { + if (nextToken == 2 && errorCount < 3) { + errorCount += 1 + reject({ statusCode: 429 }) + return + } + resolve(fakeData[nextToken] as TestResponse) + }, 10) + }) + + const observer = new InfiniteQueryObserver< + TestResponse, + Error, + InfiniteData, + TestResponse, + typeof key, + number + >(queryClient, { + retry: 5, + staleTime: 0, + retryDelay: 10, + + queryKey: key, + initialPageParam: 1, + getNextPageParam: (lastPage) => lastPage.nextToken, + queryFn: ({ pageParam }) => fetchData({ nextToken: pageParam }), + }) + + // Fetch Page 1 + const page1Data = await observer.fetchNextPage() + expect(page1Data.data?.pageParams).toEqual([1]) + + // Fetch Page 2, as per the queryFn, this will reject 2 times then resolves + const page2Data = await observer.fetchNextPage() + expect(page2Data.data?.pageParams).toEqual([1, 2]) + + // Fetch Page 3 + const page3Data = await observer.fetchNextPage() + expect(page3Data.data?.pageParams).toEqual([1, 2, 3]) + + // Now the real deal; re-fetching this query **should not** stamp into an + // infinite loop where the retryer every time restarts from page 1 + // once it reaches the page where it errors. + // For this to work, we'd need to reset the error count so we actually retry + errorCount = 0 + const reFetchedData = await observer.refetch() + + expect(reFetchedData.data?.pageParams).toEqual([1, 2, 3]) + }) }) diff --git a/packages/query-core/src/infiniteQueryBehavior.ts b/packages/query-core/src/infiniteQueryBehavior.ts index 5db6e34ba1..04de4f7a30 100644 --- a/packages/query-core/src/infiniteQueryBehavior.ts +++ b/packages/query-core/src/infiniteQueryBehavior.ts @@ -13,14 +13,15 @@ export function infiniteQueryBehavior( ): QueryBehavior> { return { onFetch: (context, query) => { + const options = context.options as InfiniteQueryPageParamsOptions + const direction = context.fetchOptions?.meta?.fetchMore?.direction + const oldPages = context.state.data?.pages || [] + const oldPageParams = context.state.data?.pageParams || [] + let result: InfiniteData = { pages: [], pageParams: [] } + let currentPage = 0 + const fetchFn = async () => { - const options = context.options as InfiniteQueryPageParamsOptions - const direction = context.fetchOptions?.meta?.fetchMore?.direction - const oldPages = context.state.data?.pages || [] - const oldPageParams = context.state.data?.pageParams || [] - const empty = { pages: [], pageParams: [] } let cancelled = false - const addSignalProperty = (object: unknown) => { Object.defineProperty(object, 'signal', { enumerable: true, @@ -78,8 +79,6 @@ export function infiniteQueryBehavior( } } - let result: InfiniteData - // fetch next / previous page? if (direction && oldPages.length) { const previous = direction === 'backward' @@ -92,22 +91,20 @@ export function infiniteQueryBehavior( result = await fetchPage(oldData, param, previous) } else { - // Fetch first page - result = await fetchPage( - empty, - oldPageParams[0] ?? options.initialPageParam, - ) - const remainingPages = pages ?? oldPages.length - // Fetch remaining pages - for (let i = 1; i < remainingPages; i++) { - const param = getNextPageParam(options, result) + // Fetch all pages + do { + const param = + currentPage === 0 + ? (oldPageParams[0] ?? options.initialPageParam) + : getNextPageParam(options, result) if (param == null) { break } result = await fetchPage(result, param) - } + currentPage++ + } while (currentPage < remainingPages) } return result