Skip to content

Commit e609156

Browse files
Fix a potential crash when calling clearStore while a query was running. (#11989)
* Fix a potential crash when calling `clearStore` while a query was running. * size-limits * Update src/react/hooks/__tests__/useLazyQuery.test.tsx Co-authored-by: Jerel Miller <[email protected]> * fix up merge * Clean up Prettier, Size-limit, and Api-Extractor --------- Co-authored-by: Jerel Miller <[email protected]> Co-authored-by: phryneas <[email protected]>
1 parent 8aa627f commit e609156

File tree

6 files changed

+133
-4
lines changed

6 files changed

+133
-4
lines changed

.changeset/pink-guests-vanish.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@apollo/client": patch
3+
---
4+
5+
Fix a potential crash when calling `clearStore` while a query was running.
6+
7+
Previously, calling `client.clearStore()` while a query was running had one of these results:
8+
* `useQuery` would stay in a `loading: true` state.
9+
* `useLazyQuery` would stay in a `loading: true` state, but also crash with a `"Cannot read property 'data' of undefined"` error.
10+
11+
Now, in both cases, the hook will enter an error state with a `networkError`, and the promise returned by the `useLazyQuery` `execute` function will return a result in an error state.

.size-limits.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"dist/apollo-client.min.cjs": 40252,
3-
"import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 33052
2+
"dist/apollo-client.min.cjs": 40271,
3+
"import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 33058
44
}

src/core/ObservableQuery.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
fixObservableSubclass,
1818
getQueryDefinition,
1919
} from "../utilities/index.js";
20-
import type { ApolloError } from "../errors/index.js";
20+
import { ApolloError, isApolloError } from "../errors/index.js";
2121
import type { QueryManager } from "./QueryManager.js";
2222
import type {
2323
ApolloQueryResult,
@@ -974,6 +974,12 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`,
974974
},
975975
error: (error) => {
976976
if (equal(this.variables, variables)) {
977+
// Coming from `getResultsFromLink`, `error` here should always be an `ApolloError`.
978+
// However, calling `concast.cancel` can inject another type of error, so we have to
979+
// wrap it again here.
980+
if (!isApolloError(error)) {
981+
error = new ApolloError({ networkError: error });
982+
}
977983
finishWaitingForOwnResult();
978984
this.reportError(error, variables);
979985
}

src/react/hooks/__tests__/useLazyQuery.test.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import { useLazyQuery } from "../useLazyQuery";
2626
import { QueryResult } from "../../types/types";
2727
import { profileHook } from "../../../testing/internal";
28+
import { InvariantError } from "../../../utilities/globals";
2829

2930
describe("useLazyQuery Hook", () => {
3031
const helloQuery: TypedDocumentNode<{
@@ -1922,6 +1923,69 @@ describe("useLazyQuery Hook", () => {
19221923
expect(options.fetchPolicy).toBe(defaultFetchPolicy);
19231924
});
19241925
});
1926+
1927+
// regression for https://github.com/apollographql/apollo-client/issues/11988
1928+
test("calling `clearStore` while a lazy query is running puts the hook into an error state and resolves the promise with an error result", async () => {
1929+
const link = new MockSubscriptionLink();
1930+
let requests = 0;
1931+
link.onSetup(() => requests++);
1932+
const client = new ApolloClient({
1933+
link,
1934+
cache: new InMemoryCache(),
1935+
});
1936+
const ProfiledHook = profileHook(() => useLazyQuery(helloQuery));
1937+
render(<ProfiledHook />, {
1938+
wrapper: ({ children }) => (
1939+
<ApolloProvider client={client}>{children}</ApolloProvider>
1940+
),
1941+
});
1942+
1943+
{
1944+
const [, result] = await ProfiledHook.takeSnapshot();
1945+
expect(result.loading).toBe(false);
1946+
expect(result.data).toBeUndefined();
1947+
}
1948+
const execute = ProfiledHook.getCurrentSnapshot()[0];
1949+
1950+
const promise = execute();
1951+
expect(requests).toBe(1);
1952+
1953+
{
1954+
const [, result] = await ProfiledHook.takeSnapshot();
1955+
expect(result.loading).toBe(true);
1956+
expect(result.data).toBeUndefined();
1957+
}
1958+
1959+
client.clearStore();
1960+
1961+
const executionResult = await promise;
1962+
expect(executionResult.data).toBeUndefined();
1963+
expect(executionResult.loading).toBe(true);
1964+
expect(executionResult.error).toEqual(
1965+
new ApolloError({
1966+
networkError: new InvariantError(
1967+
"Store reset while query was in flight (not completed in link chain)"
1968+
),
1969+
})
1970+
);
1971+
1972+
{
1973+
const [, result] = await ProfiledHook.takeSnapshot();
1974+
expect(result.loading).toBe(false);
1975+
expect(result.data).toBeUndefined();
1976+
expect(result.error).toEqual(
1977+
new ApolloError({
1978+
networkError: new InvariantError(
1979+
"Store reset while query was in flight (not completed in link chain)"
1980+
),
1981+
})
1982+
);
1983+
}
1984+
1985+
link.simulateResult({ result: { data: { hello: "Greetings" } } }, true);
1986+
await expect(ProfiledHook).not.toRerender({ timeout: 50 });
1987+
expect(requests).toBe(1);
1988+
});
19251989
});
19261990

19271991
describe.skip("Type Tests", () => {

src/react/hooks/__tests__/useQuery.test.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10082,6 +10082,54 @@ describe("useQuery Hook", () => {
1008210082
);
1008310083
});
1008410084

10085+
test("calling `clearStore` while a query is running puts the hook into an error state", async () => {
10086+
const query = gql`
10087+
query {
10088+
hello
10089+
}
10090+
`;
10091+
10092+
const link = new MockSubscriptionLink();
10093+
let requests = 0;
10094+
link.onSetup(() => requests++);
10095+
const client = new ApolloClient({
10096+
link,
10097+
cache: new InMemoryCache(),
10098+
});
10099+
const ProfiledHook = profileHook(() => useQuery(query));
10100+
render(<ProfiledHook />, {
10101+
wrapper: ({ children }) => (
10102+
<ApolloProvider client={client}>{children}</ApolloProvider>
10103+
),
10104+
});
10105+
10106+
expect(requests).toBe(1);
10107+
{
10108+
const result = await ProfiledHook.takeSnapshot();
10109+
expect(result.loading).toBe(true);
10110+
expect(result.data).toBeUndefined();
10111+
}
10112+
10113+
client.clearStore();
10114+
10115+
{
10116+
const result = await ProfiledHook.takeSnapshot();
10117+
expect(result.loading).toBe(false);
10118+
expect(result.data).toBeUndefined();
10119+
expect(result.error).toEqual(
10120+
new ApolloError({
10121+
networkError: new InvariantError(
10122+
"Store reset while query was in flight (not completed in link chain)"
10123+
),
10124+
})
10125+
);
10126+
}
10127+
10128+
link.simulateResult({ result: { data: { hello: "Greetings" } } }, true);
10129+
await expect(ProfiledHook).not.toRerender({ timeout: 50 });
10130+
expect(requests).toBe(1);
10131+
});
10132+
1008510133
// https://github.com/apollographql/apollo-client/issues/11938
1008610134
it("does not emit `data` on previous fetch when a 2nd fetch is kicked off and the result returns an error when errorPolicy is none", async () => {
1008710135
const query = gql`

src/utilities/observables/Concast.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ export class Concast<T> extends Observable<T> {
256256
public cancel = (reason: any) => {
257257
this.reject(reason);
258258
this.sources = [];
259-
this.handlers.complete();
259+
this.handlers.error(reason);
260260
};
261261
}
262262

0 commit comments

Comments
 (0)