Skip to content

Commit b43ccb1

Browse files
authored
docs: refresh upgrade guide and v17 topics (#4737)
1 parent 46b4ae9 commit b43ccb1

22 files changed

Lines changed: 2432 additions & 198 deletions

website/pages/docs/_meta.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,31 @@ const meta = {
2323
nullability: '',
2424
'abstract-types': '',
2525
'custom-scalars': '',
26+
'constructing-types': '',
27+
'oneof-input-objects': '',
28+
'schema-coordinates': '',
29+
'schema-evolution': '',
30+
subscriptions: '',
2631
'-- 3': {
2732
type: 'separator',
28-
title: 'Advanced Guides',
33+
title: 'Experimental Specification Features',
2934
},
30-
'constructing-types': '',
31-
'oneof-input-objects': '',
35+
'experimental-specification-features': '',
3236
'defer-stream': '',
33-
subscriptions: '',
37+
'fragment-arguments': '',
38+
'directives-on-directives': '',
39+
'-- 4': {
40+
type: 'separator',
41+
title: 'GraphQL.js Runtime Features',
42+
},
43+
'graphql-harness': '',
44+
'advanced-execution-pipelines': '',
45+
'abort-signals': '',
46+
'execution-hooks': '',
47+
'-- 5': {
48+
type: 'separator',
49+
title: 'Advanced Guides',
50+
},
3451
'type-generation': '',
3552
'cursor-based-pagination': '',
3653
'advanced-custom-scalars': '',
@@ -41,7 +58,7 @@ const meta = {
4158
'graphql-errors': '',
4259
'using-directives': '',
4360
'authorization-strategies': '',
44-
'-- 4': {
61+
'-- 6': {
4562
type: 'separator',
4663
title: 'Testing',
4764
},
@@ -50,7 +67,7 @@ const meta = {
5067
'testing-operations': '',
5168
'testing-resolvers': '',
5269
'testing-best-practices': '',
53-
'-- 5': {
70+
'-- 7': {
5471
type: 'separator',
5572
title: 'Production & Scaling',
5673
},
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
---
2+
title: Handling Abort Signals
3+
sidebarTitle: Abort Signals
4+
---
5+
6+
import { Callout } from 'nextra/components';
7+
8+
# Handling Abort Signals
9+
10+
<Callout type="info">
11+
Abort signal support is available in GraphQL.js v17 and newer. It is a
12+
GraphQL.js runtime API, not GraphQL syntax or a transport protocol.
13+
</Callout>
14+
15+
## What the specification says
16+
17+
The GraphQL specification mentions cancellation in a narrow execution case.
18+
During ordinary execution, when a non-null execution error propagates to a
19+
parent response position, sibling response positions that have not executed or
20+
yielded a value
21+
[may be cancelled](https://spec.graphql.org/draft/#sec-Errors-and-Non-Null-Types)
22+
to avoid unnecessary work. The
23+
[conformance appendix](https://spec.graphql.org/draft/#sec-Appendix-Conformance)
24+
makes lowercase key words in normative portions of the specification carry
25+
their RFC 2119 meaning, so this is a normative `MAY`. It permits cancellation;
26+
it does not require every implementation to cancel work in that case.
27+
28+
The subscription algorithms also describe cancellation of response streams and
29+
source streams. Those algorithms are still about GraphQL execution and stream
30+
lifecycle, not about a user or host cancelling a request that is already in
31+
flight.
32+
33+
The specification does not define a cancellation primitive or a transport
34+
cancellation protocol.
35+
36+
## What GraphQL.js v17 adds
37+
38+
GraphQL.js v17 exposes cancellation for two related situations:
39+
40+
- Internally, GraphQL.js can signal work that no longer contributes to the
41+
returned result, such as work from response positions cancelled under the
42+
specification's execution rules.
43+
- Externally, a host can pass `abortSignal` to `execute()`, `subscribe()`,
44+
`graphql()`, or `experimentalExecuteIncrementally()` to cancel an issued
45+
request while it is in flight.
46+
47+
Both cases matter because long-running GraphQL operations often start other
48+
asynchronous work: database queries, HTTP requests, async iterators, loaders,
49+
and subscription streams. In v16, GraphQL.js had no standard way to ask that
50+
work to stop. In v17, GraphQL.js uses `AbortSignal` as its JavaScript runtime
51+
API for cancellation.
52+
53+
GraphQL.js v17 uses the same resolver-scoped signal for internal cancellation
54+
and external request cancellation. Resolvers read that shared signal with
55+
`info.getAbortSignal()` and pass it to downstream APIs that support
56+
cancellation.
57+
58+
Abort signals are cooperative. They let GraphQL.js and resolvers pass a
59+
cancellation request to downstream work, but JavaScript cannot force an
60+
arbitrary promise, database driver, HTTP client, or async iterator to stop. The
61+
downstream API has to accept the signal and honor it.
62+
63+
GraphQL.js does not currently provide fine-grained per-field or per-branch
64+
abort signals. Resolvers in an operation share one resolver `AbortSignal`. For
65+
internally cancelled portions of an operation, GraphQL.js aborts that shared
66+
signal when the result that will actually be returned has finished, notifying
67+
pending resolver work together. An external abort also aborts the same shared
68+
resolver signal. This coarseness may change in future versions.
69+
70+
## Using the signal in resolvers
71+
72+
Resolvers obtain the abort signal via `info.getAbortSignal()`. Pass that
73+
signal to downstream APIs that support cancellation.
74+
75+
```js
76+
const Query = new GraphQLObjectType({
77+
name: 'Query',
78+
fields: {
79+
user: {
80+
type: User,
81+
args: { id: { type: new GraphQLNonNull(GraphQLID) } },
82+
async resolve(_source, args, _context, info) {
83+
const abortSignal = info.getAbortSignal();
84+
85+
const response = await fetch(`https://users.example/${args.id}`, {
86+
signal: abortSignal,
87+
});
88+
89+
return response.json();
90+
},
91+
},
92+
},
93+
});
94+
```
95+
96+
As noted, this signal can be triggered by GraphQL.js itself. For example,
97+
suppose a query selects `user { profile recommendations }`, `profile` is
98+
non-null, and the `recommendations` resolver starts a slow downstream request.
99+
If `profile` throws and the error bubbles so that `user` becomes `null`, the
100+
`recommendations` result can no longer appear in the response. GraphQL.js does
101+
not currently create a separate signal for that one sibling branch. Instead,
102+
when the response that will actually be returned has finished, GraphQL.js
103+
aborts the shared resolver signal, so any still-pending resolver work that
104+
honors the signal can stop together.
105+
106+
For work that does not accept `AbortSignal`, check the signal before starting
107+
and again between expensive steps. This is useful even for synchronous chunks:
108+
JavaScript cannot interrupt code that is already running, but the next check can
109+
avoid starting more work.
110+
111+
```js
112+
async function loadReport(info) {
113+
const abortSignal = info.getAbortSignal();
114+
115+
if (abortSignal?.aborted) {
116+
throw abortSignal.reason;
117+
}
118+
119+
const rows = await loadReportRows();
120+
121+
if (abortSignal?.aborted) {
122+
throw abortSignal.reason;
123+
}
124+
125+
const totals = calculateTotals(rows);
126+
127+
if (abortSignal?.aborted) {
128+
throw abortSignal.reason;
129+
}
130+
131+
return formatReport(totals);
132+
}
133+
```
134+
135+
If the API exposes its own cancellation method, connect the abort event to that
136+
method:
137+
138+
```js
139+
async function loadReportFromJob(info) {
140+
const abortSignal = info.getAbortSignal();
141+
142+
if (abortSignal?.aborted) {
143+
throw abortSignal.reason;
144+
}
145+
146+
const job = startReportJob();
147+
abortSignal?.addEventListener(
148+
'abort',
149+
() => {
150+
job.cancel();
151+
},
152+
{ once: true },
153+
);
154+
155+
return job.result;
156+
}
157+
```
158+
159+
Here `startReportJob()` represents an async API that does not accept
160+
`AbortSignal` directly, but does return an object with a `result` promise and a
161+
`cancel()` method.
162+
163+
## Passing an external signal to execution
164+
165+
GraphQL.js also lets a host cancel an issued request while it is in flight.
166+
Pass `abortSignal` to `graphql()`, `execute()`, `subscribe()`, or
167+
`experimentalExecuteIncrementally()`. If that external signal is aborted,
168+
GraphQL.js aborts the same resolver signal exposed through
169+
`info.getAbortSignal()`.
170+
171+
```js
172+
import { execute, parse } from 'graphql';
173+
174+
const controller = new AbortController();
175+
const document = parse(`
176+
query User($id: ID!) {
177+
user(id: $id) {
178+
id
179+
name
180+
}
181+
}
182+
`);
183+
184+
const resultPromise = execute({
185+
schema,
186+
document,
187+
variableValues: { id: '123' },
188+
abortSignal: controller.signal,
189+
});
190+
191+
setTimeout(() => {
192+
controller.abort(new Error('Request timed out'));
193+
}, 500);
194+
195+
const result = await resultPromise;
196+
```
197+
198+
If the signal is aborted before execution finishes, asynchronous execution
199+
rejects. The abort reason becomes the rejection cause when possible.
200+
201+
## Wiring HTTP request life cycles
202+
203+
Most servers already know when a request is no longer useful: the client
204+
disconnects, a gateway timeout fires, or a framework cancellation token is
205+
triggered. Bridge that lifecycle to GraphQL.js so resolver cancellation follows
206+
request cancellation.
207+
208+
```js
209+
const controller = new AbortController();
210+
211+
req.on('close', () => {
212+
controller.abort(new Error('Client disconnected'));
213+
});
214+
215+
const result = await execute({
216+
schema,
217+
document,
218+
variableValues,
219+
contextValue,
220+
abortSignal: controller.signal,
221+
});
222+
```
223+
224+
This helps avoid expensive resolver work after the client is already gone.
225+
226+
## Handling aborted execution
227+
228+
GraphQL.js rejects with `AbortedGraphQLExecutionError` when execution is
229+
aborted after it has started. The error exposes the best partial result
230+
GraphQL.js can still produce.
231+
232+
```js
233+
import { AbortedGraphQLExecutionError, execute } from 'graphql';
234+
235+
try {
236+
const result = await execute({
237+
schema,
238+
document,
239+
abortSignal,
240+
});
241+
return result;
242+
} catch (error) {
243+
if (error instanceof AbortedGraphQLExecutionError) {
244+
const partialResult = await error.abortedResult;
245+
logger.info({ partialResult }, 'GraphQL execution aborted');
246+
}
247+
248+
throw error;
249+
}
250+
```
251+
252+
`abortedResult` may be either a result or a promise for a result. For
253+
incremental delivery, it may contain the initial incremental result if that was
254+
already available.
255+
256+
## Observing async cleanup
257+
258+
An abort can stop GraphQL.js from producing more response data before every
259+
tracked async task has settled. Cleanup can continue after the response
260+
boundary, especially when resolvers use async iterators or when downstream
261+
libraries perform their own shutdown work.
262+
263+
Use the experimental `asyncWorkFinished` execution hook when a host needs to
264+
observe that boundary. See [Execution Hooks](/docs/execution-hooks) for
265+
examples of logging cleanup completion, delaying response delivery until
266+
tracked work settles, and tracking resolver-started async work.
267+
268+
## Practical guidance
269+
270+
- Treat abort as cooperative cancellation. A resolver or downstream client that
271+
ignores the signal may keep doing work outside GraphQL.js.
272+
- Pass the signal to downstream clients early, before starting expensive work.
273+
- Avoid swallowing abort errors in resolvers. Let GraphQL.js stop the operation.
274+
- Keep request timeouts at the server or transport layer, and connect those
275+
timeouts to an `AbortController`.
276+
- Do not expose abort signals in the GraphQL schema. They are a JavaScript
277+
runtime concern, not client query syntax.

0 commit comments

Comments
 (0)