[WIP] repro `hydrate()` causes unhandled promise rejection with a throwing `prefetchQuery`
Debugging an issue where I see A query that was dehydrated as pending ended up rejecting. [${query?.queryHash}]: Error: server error; The error will be redacted in production builds logged, but my SSR render also returned a status code of 500.
Upon digging into the code, I think I found that hydrate() can cause an unhandled promise rejection from here when dehydrateQuery catches an error and returns a Promise.reject because a queryFn threw.
It seems like no tests are covering this, due to them ending before the rejection happens, but when I run the tests with the change in this PR, I get the following:
Test run
β nx run @tanstack/query-core:build [existing outputs match the cache, left as is] β nx run @tanstack/query-persist-client-core:build [existing outputs match the cache, left as is] β nx run @tanstack/react-query:build [existing outputs match the cache, left as is] β nx run @tanstack/query-devtools:build [existing outputs match the cache, left as is] β nx run @tanstack/react-query-persist-client:test:lib [existing outputs match the cache, left as is] β nx run @tanstack/react-query-devtools:test:lib [existing outputs match the cache, left as is]βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β nx run @tanstack/react-query:test:lib > @tanstack/[email protected] test:lib /Users/alvar/Code/query/packages/react-query > vitest --retry=3
Testing types with tsc and vue-tsc is an experimental feature.
Breaking changes might not follow SemVer, please pin Vitest's version when using it.
RUN v2.0.5 /Users/alvar/Code/query/packages/react-query
Coverage enabled with istanbul
β |@tanstack/react-query| src/__tests__/queryOptions.test-d.tsx (18 tests)
β |@tanstack/react-query| src/__tests__/infiniteQueryOptions.test-d.tsx (10 tests)
β |@tanstack/react-query| src/__tests__/useQueries.test-d.tsx (6 tests)
β |@tanstack/react-query| src/__tests__/useInfiniteQuery.test-d.tsx (7 tests)
β |@tanstack/react-query| src/__tests__/useQuery.test-d.tsx (9 tests)
β |@tanstack/react-query| src/__tests__/useSuspenseQueries.test-d.tsx (6 tests)
β |@tanstack/react-query| src/__tests__/suspense.test-d.tsx (9 tests)
β |@tanstack/react-query| src/__tests__/prefetch.test-d.tsx (5 tests)
stderr | src/__tests__/prefetch.test.tsx > usePrefetchQuery > should let errors fall through and not refetch failed queries
Error: Oops! Server error!
at /Users/alvar/Code/query/packages/react-query/src/__tests__/prefetch.test.tsx:137:13 {
[stack]: 'Error: Oops! Server error!\n' +
' at /Users/alvar/Code/query/packages/react-query/src/__tests__/prefetch.test.tsx:137:13',
[message]: 'Oops! Server error!'
}
The above error occurred in the <Suspended> component:
at Suspended (/Users/alvar/Code/query/packages/react-query/src/__tests__/prefetch.test.tsx:39:64)
at Suspense
at ErrorBoundary (/Users/alvar/Code/query/node_modules/.pnpm/[email protected][email protected]/node_modules/react-error-boundary/dist/react-error-boundary.development.esm.js:14:5)
at App (/Users/alvar/Code/query/packages/react-query/src/__tests__/prefetch.test.tsx:126:29)
at QueryClientProvider (/Users/alvar/Code/query/packages/react-query/src/QueryClientProvider.tsx:400:3)
React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary.
stderr | src/__tests__/prefetch.test.tsx > usePrefetchQuery > should be able to recover from errors and try fetching again
Error: Oops! Server error!
at /Users/alvar/Code/query/packages/react-query/src/__tests__/prefetch.test.tsx:200:13 {
[stack]: 'Error: Oops! Server error!\n' +
' at /Users/alvar/Code/query/packages/react-query/src/__tests__/prefetch.test.tsx:200:13',
[message]: 'Oops! Server error!'
}
The above error occurred in the <Suspended> component:
at Suspended (/Users/alvar/Code/query/packages/react-query/src/__tests__/prefetch.test.tsx:39:64)
at Suspense
at ErrorBoundary (/Users/alvar/Code/query/node_modules/.pnpm/[email protected][email protected]/node_modules/react-error-boundary/dist/react-error-boundary.development.esm.js:14:5)
at App (/Users/alvar/Code/query/packages/react-query/src/__tests__/prefetch.test.tsx:204:47)
at QueryClientProvider (/Users/alvar/Code/query/packages/react-query/src/QueryClientProvider.tsx:400:3)
React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary.
β |@tanstack/react-query| src/__tests__/useQueries.test.tsx (19 tests) 673ms
β |@tanstack/react-query| src/__tests__/useMutation.test.tsx (23 tests) 869ms
stderr | src/__tests__/prefetch.test.tsx > usePrefetchInfiniteQuery > should prefetch an infinite query if query state does not exist
Warning: Each child in a list should have a unique "key" prop.
Check the render method of `Suspended`. See https://react.dev/link/warning-keys for more information.
at div
at Suspended (/Users/alvar/Code/query/packages/react-query/src/__tests__/prefetch.test.tsx:337:72)
at Suspense
at App (/Users/alvar/Code/query/packages/react-query/src/__tests__/prefetch.test.tsx:366:29)
at QueryClientProvider (/Users/alvar/Code/query/packages/react-query/src/QueryClientProvider.tsx:400:3)
β |@tanstack/react-query| src/__tests__/HydrationBoundary.test.tsx (7 tests) 180ms
β |@tanstack/react-query| src/__tests__/ssr-hydration.test.tsx (3 tests) 127ms
β |@tanstack/react-query| src/__tests__/QueryResetErrorBoundary.test.tsx (13 tests) 1842ms
β |@tanstack/react-query| src/__tests__/prefetch.test.tsx (9 tests) 1971ms
β |@tanstack/react-query| src/__tests__/useInfiniteQuery.test.tsx (26 tests) 2448ms
β |@tanstack/react-query| src/__tests__/useSuspenseQueries.test.tsx (6 tests) 37ms
β |@tanstack/react-query| src/__tests__/useIsFetching.test.tsx (5 tests) 350ms
β |@tanstack/react-query| src/__tests__/fine-grained-persister.test.tsx (3 tests) 46ms
β |@tanstack/react-query| src/__tests__/useMutationState.test.tsx (7 tests) 732ms
β |@tanstack/react-query| src/__tests__/ssr.test.tsx (5 tests) 15ms
β |@tanstack/react-query| src/__tests__/QueryClientProvider.test.tsx (4 tests) 62ms
β |@tanstack/react-query| src/__tests__/suspense.test.tsx (22 tests) 10490ms
β― |@tanstack/react-query| src/__tests__/useQuery.test.tsx (139 tests | 1 failed) 10864ms
Γ useQuery > should retry failed initialPromise on the client
β Unable to find an element with the text: failure: redacted. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, script, style
<body>
<div>
<div>
<div>
failure:
</div>
<div>
data:
client
</div>
</div>
</div>
</body>
Ignored nodes: comments, script, style
<html>
<head />
<body>
<div>
<div>
<div>
failure:
</div>
<div>
data:
client
</div>
</div>
</div>
</body>
</html>
β Unable to find an element with the text: failure: redacted. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, script, style
<body>
<div>
<div>
<div>
failure:
</div>
<div>
data:
client
</div>
</div>
</div>
</body>
Ignored nodes: comments, script, style
<html>
<head />
<body>
<div>
<div>
<div>
failure:
</div>
<div>
data:
client
</div>
</div>
</div>
</body>
</html>
β Unable to find an element with the text: failure: redacted. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, script, style
<body>
<div>
<div>
<div>
failure:
</div>
<div>
data:
client
</div>
</div>
</div>
</body>
Ignored nodes: comments, script, style
<html>
<head />
<body>
<div>
<div>
<div>
failure:
</div>
<div>
data:
client
</div>
</div>
</div>
</body>
</html>
β Unable to find an element with the text: failure: redacted. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, script, style
<body>
<div>
<div>
<div>
failure:
</div>
<div>
data:
client
</div>
</div>
</div>
</body>
Ignored nodes: comments, script, style
<html>
<head />
<body>
<div>
<div>
<div>
failure:
</div>
<div>
data:
client
</div>
</div>
</div>
</body>
</html>
β―β―β―β―β―β―β― Failed Tests 1 β―β―β―β―β―β―β―
FAIL |@tanstack/react-query| src/__tests__/useQuery.test.tsx > useQuery > should retry failed initialPromise on the client
FAIL |@tanstack/react-query| src/__tests__/useQuery.test.tsx > useQuery > should retry failed initialPromise on the client
FAIL |@tanstack/react-query| src/__tests__/useQuery.test.tsx > useQuery > should retry failed initialPromise on the client
FAIL |@tanstack/react-query| src/__tests__/useQuery.test.tsx > useQuery > should retry failed initialPromise on the client
TestingLibraryElementError: Unable to find an element with the text: failure: redacted. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, script, style
<body>
<div>
<div>
<div>
failure:
</div>
<div>
data:
client
</div>
</div>
</div>
</body>
Ignored nodes: comments, script, style
<html>
<head />
<body>
<div>
<div>
<div>
failure:
</div>
<div>
data:
client
</div>
</div>
</div>
</body>
</html>
β― Proxy.waitForWrapper ../../node_modules/.pnpm/@[email protected]/node_modules/@testing-library/dom/dist/wait-for.js:163:27
β― src/__tests__/useQuery.test.tsx:6587:11
6585|
6586| const rendered = renderWithClient(clientQueryClient, <Page />)
6587| await waitFor(() => rendered.getByText('failure: redacted'))
| ^
6588| await waitFor(() => rendered.getByText('data: client'))
6589| expect(count).toBe(1)
β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―[1/4]β―
β―β―β―β―β―β― Unhandled Errors β―β―β―β―β―β―
Vitest caught 4 unhandled errors during the test run.
This might cause false positive tests. Resolve unhandled errors to make sure your tests are not affected.
β―β―β―β― Unhandled Rejection β―β―β―β―β―
Error: redacted
β― ../query-core/src/hydration.ts:90:31
This error originated in "src/__tests__/useQuery.test.tsx" test file. It doesn't mean the error was thrown inside the file itself, but while it was running.
The latest test that might've caused the error is "should retry failed initialPromise on the client". It might mean one of the following:
- The error was thrown, while Vitest was running this test.
- If the error occurred after the test had been completed, this was the last documented test before it was thrown.
β―β―β―β― Unhandled Rejection β―β―β―β―β―
Error: redacted
β― ../query-core/src/hydration.ts:90:31
This error originated in "src/__tests__/useQuery.test.tsx" test file. It doesn't mean the error was thrown inside the file itself, but while it was running.
The latest test that might've caused the error is "should retry failed initialPromise on the client". It might mean one of the following:
- The error was thrown, while Vitest was running this test.
- If the error occurred after the test had been completed, this was the last documented test before it was thrown.
β―β―β―β― Unhandled Rejection β―β―β―β―β―
Error: redacted
β― ../query-core/src/hydration.ts:90:31
This error originated in "src/__tests__/useQuery.test.tsx" test file. It doesn't mean the error was thrown inside the file itself, but while it was running.
The latest test that might've caused the error is "should retry failed initialPromise on the client". It might mean one of the following:
- The error was thrown, while Vitest was running this test.
- If the error occurred after the test had been completed, this was the last documented test before it was thrown.
β―β―β―β― Unhandled Rejection β―β―β―β―β―
Error: redacted
β― ../query-core/src/hydration.ts:90:31
This error originated in "src/__tests__/useQuery.test.tsx" test file. It doesn't mean the error was thrown inside the file itself, but while it was running.
The latest test that might've caused the error is "should retry failed initialPromise on the client". It might mean one of the following:
- The error was thrown, while Vitest was running this test.
- If the error occurred after the test had been completed, this was the last documented test before it was thrown.
β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―
Test Files 1 failed | 22 passed (23)
Tests 1 failed | 360 passed (361)
Type Errors no errors
Errors 4 errors
Start at 16:52:31
Duration 15.39s (transform 1.15s, setup 9.32s, collect 4.87s, tests 30.71s, environment 18.06s, prepare 1.41s, typecheck 2.72s)
βELIFECYCLEβ Command failed with exit code 1.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
NX Ran target test:lib for 3 projects and 4 tasks they depend on (19s)
β 6/7 succeeded [6 read from cache]
β 1/7 targets failed, including the following:
- nx run @tanstack/react-query:test:lib
View structured, searchable error logs at https://nx.app/runs/yruFhd2Ecu
βELIFECYCLEβ Command failed with exit code 1.
@alvarlagerlof is this happening because of the .then chain?
const initialPromise = Promise.resolve(promise).then(deserializeData)
Ideally, what we wanted to achieve is if the promise from the server fails, it would get retried on the client (since we pass it to the retryer). I'm wondering if adding a .catch(noop) here would stop those retries from happening ...
@alvarlagerlof is this happening because of the
.thenchain?
Yes the lack of .catch causes an unhandled promise rejection.
Ideally, what we wanted to achieve is if the promise from the server fails, it would get retried on the client (since we pass it to the retryer). I'm wondering if adding a
.catch(noop)here would stop those retries from happening ...
I realised the same about the retries. I tried adding .catch(() => {}) that does ofc fix the unhandled promise, but as you suspected it breaks retries.
Test log
```
β―β―β―β―β―β―β― Failed Tests 1 β―β―β―β―β―β―β―
FAIL |@tanstack/react-query| src/__tests__/useQuery.test.tsx > useQuery > should retry failed initialPromise on the client
TestingLibraryElementError: Unable to find an element with the text: failure: redacted. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, script, style
<body>
<div>
<div>
<div>
failure:
["query_136"] data is undefined
</div>
<div>
data:
</div>
</div>
</div>
</body>
Ignored nodes: comments, script, style
<html>
<head />
<body>
<div>
<div>
<div>
failure:
["query_136"] data is undefined
</div>
<div>
data:
</div>
</div>
</div>
</body>
</html>
β― Proxy.waitForWrapper ../../node_modules/.pnpm/@[email protected]/node_modules/@testing-library/dom/dist/wait-for.js:163:27
β― src/__tests__/useQuery.test.tsx:6586:11
6584|
6585| const rendered = renderWithClient(clientQueryClient, <Page />)
6586| await waitFor(() => rendered.getByText('failure: redacted'))
| ^
6587| await waitFor(() => rendered.getByText('data: client'))
6588| expect(count).toBe(1)
β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―[1/4]β―
FAIL |@tanstack/react-query| src/__tests__/useQuery.test.tsx > useQuery > should retry failed initialPromise on the client
TestingLibraryElementError: Unable to find an element with the text: failure: redacted. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, script, style
<body>
<div>
<div>
<div>
failure:
["query_137"] data is undefined
</div>
<div>
data:
</div>
</div>
</div>
</body>
Ignored nodes: comments, script, style
<html>
<head />
<body>
<div>
<div>
<div>
failure:
["query_137"] data is undefined
</div>
<div>
data:
</div>
</div>
</div>
</body>
</html>
β― Proxy.waitForWrapper ../../node_modules/.pnpm/@[email protected]/node_modules/@testing-library/dom/dist/wait-for.js:163:27
β― src/__tests__/useQuery.test.tsx:6586:11
6584|
6585| const rendered = renderWithClient(clientQueryClient, <Page />)
6586| await waitFor(() => rendered.getByText('failure: redacted'))
| ^
6587| await waitFor(() => rendered.getByText('data: client'))
6588| expect(count).toBe(1)
β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―[2/4]β―
FAIL |@tanstack/react-query| src/__tests__/useQuery.test.tsx > useQuery > should retry failed initialPromise on the client
TestingLibraryElementError: Unable to find an element with the text: failure: redacted. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, script, style
<body>
<div>
<div>
<div>
failure:
["query_138"] data is undefined
</div>
<div>
data:
</div>
</div>
</div>
</body>
Ignored nodes: comments, script, style
<html>
<head />
<body>
<div>
<div>
<div>
failure:
["query_138"] data is undefined
</div>
<div>
data:
</div>
</div>
</div>
</body>
</html>
β― Proxy.waitForWrapper ../../node_modules/.pnpm/@[email protected]/node_modules/@testing-library/dom/dist/wait-for.js:163:27
β― src/__tests__/useQuery.test.tsx:6586:11
6584|
6585| const rendered = renderWithClient(clientQueryClient, <Page />)
6586| await waitFor(() => rendered.getByText('failure: redacted'))
| ^
6587| await waitFor(() => rendered.getByText('data: client'))
6588| expect(count).toBe(1)
β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―[3/4]β―
FAIL |@tanstack/react-query| src/__tests__/useQuery.test.tsx > useQuery > should retry failed initialPromise on the client
TestingLibraryElementError: Unable to find an element with the text: failure: redacted. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, script, style
<body>
<div>
<div>
<div>
failure:
["query_139"] data is undefined
</div>
<div>
data:
</div>
</div>
</div>
</body>
Ignored nodes: comments, script, style
<html>
<head />
<body>
<div>
<div>
<div>
failure:
["query_139"] data is undefined
</div>
<div>
data:
</div>
</div>
</div>
</body>
</html>
β― Proxy.waitForWrapper ../../node_modules/.pnpm/@[email protected]/node_modules/@testing-library/dom/dist/wait-for.js:163:27
β― src/__tests__/useQuery.test.tsx:6586:11
6584|
6585| const rendered = renderWithClient(clientQueryClient, <Page />)
6586| await waitFor(() => rendered.getByText('failure: redacted'))
| ^
6587| await waitFor(() => rendered.getByText('data: client'))
6588| expect(count).toBe(1)
β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―[4/4]β―
Test Files 1 failed | 22 passed (23)
Tests 1 failed | 360 passed (361)
Type Errors no errors
Start at 12:22:17
Duration 19.78s (transform 6.78s, setup 11.99s, collect 22.31s, tests 34.73s, environment 22.41s, prepare 6.71s, typecheck 18.40s)
βELIFECYCLEβ Command failed with exit code 1.
</p>
</details>
Not sure why it's "unhandled" then. The retryer does handle it π - on the client
Not sure why it's "unhandled" then. The retryer does handle it π - on the client
I just tried this:
// Note: `Promise.resolve` required cause
// RSC transformed promises are not thenable
const initialPromise = Promise.resolve(promise).then(deserializeData)
// this doesn't actually fetch - it just creates a retryer
// which will re-use the passed `initialPromise`
void query.fetch(undefined, { initialPromise }).catch(() => {
/* noop */
}
It also resolved the unhandled promise rejection. So maybe something in the fetch is actually where the issue lies.
But the added sleep seems to cause a test fail regardless:
Test log
FAIL |@tanstack/react-query| src/__tests__/useQuery.test.tsx > useQuery > should retry failed initialPromise on the client
TestingLibraryElementError: Unable to find an element with the text: failure: redacted. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, script, style
<body>
<div>
<div>
<div>
failure:
</div>
<div>
data:
client
</div>
</div>
</div>
</body>
Ignored nodes: comments, script, style
<html>
<head />
<body>
<div>
<div>
<div>
failure:
</div>
<div>
data:
client
</div>
</div>
</div>
</body>
</html>
β― Proxy.waitForWrapper ../../node_modules/.pnpm/@[email protected]/node_modules/@testing-library/dom/dist/wait-for.js:163:27
β― src/__tests__/useQuery.test.tsx:6587:11
6585|
6586| const rendered = renderWithClient(clientQueryClient, <Page />)
6587| await waitFor(() => rendered.getByText('failure: redacted'))
| ^
6588| await waitFor(() => rendered.getByText('data: client'))
6589| expect(count).toBe(1)
β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―β―[1/4]β―
Test Files 1 failed | 22 passed (23)
Tests 1 failed | 360 passed (361)
Type Errors no errors
Start at 14:23:31
Duration 16.57s (transform 4.25s, setup 6.11s, collect 12.24s, tests 36.34s, environment 12.65s, prepare 3.76s, typecheck 10.56s)
βELIFECYCLEβ Command failed with exit code 1.