Execution Cancellation
In the real world, a lot of HTTP requests are dropped or canceled. This can happen due to a flakey internet connection, navigation to a new view or page within a web or native app or the user simply closing the app. In this case, the server can stop processing the request and save resources.
That is why Yoga comes with experimental support for canceling the GraphQL execution upon request cancellation.
Getting started
To enable execution cancellation, you need to add the useExecutionCancellation
plugin to your Yoga
instance.
import { createYoga, useExecutionCancellation } from 'graphql-yoga'
import { schema } from './schema'
// Provide your schema
const yoga = createYoga({
plugins: [useExecutionCancellation()],
schema
})
// Start the server and explore http://localhost:4000/graphql
const server = createServer(yoga)
server.listen(4000, () => {
console.info('Server is running on http://localhost:4000/graphql')
})
That is all you need to do to enable execution cancellation in your Yoga server. Theoretically, you can enable this and immediately benefit from it without making any other adjustments within your GraphQL schema implementation.
If you want to understand how it works and how you can adjust your resolvers to properly cancel pending promises (e.g. database reads or HTTP requests), you can continue with the next section.
How it works
The following example demonstrates how the execution cancellation works.
type Query {
user: User
}
type User {
id: ID!
name: String!
bestFriend: User
}
The Query.user
resolver has a artificial delay of 10 seconds to simulate a long-running operation
e.g. a slow read from a database.
import { createServer } from 'node:http'
import { setTimeout as setTimeout$ } from 'node:timers/promises'
import { createLogger, createSchema, createYoga, useExecutionCancellation } from 'graphql-yoga'
const logger = createLogger('debug')
const schema = createSchema({
typeDefs: /* GraphQL */ `
type Query {
user: User
}
type User {
id: ID!
name: String!
bestFriend: User
}
`,
resolvers: {
Query: {
async user(_, __, { request }) {
logger.info('resolving user')
await setTimeout$(5000)
logger.info('resolved user')
return {
id: '1',
name: 'Chewie'
}
}
},
User: {
bestFriend() {
logger.info('resolving user best friend')
return {
id: '2',
name: 'Han Solo'
}
}
}
}
})
const yoga = createYoga({
plugins: [useExecutionCancellation()],
schema,
logging: logger
})
const server = createServer(yoga)
server.listen(4000, () => {
console.info('Server is running on http://localhost:4000/graphql')
})
With this server setup we are going to execute the following GraphQL query.
query UserWithBestFriend {
user {
id
name
bestFriend {
id
name
}
}
}
For your convenience, you can copy the following curl
command to execute the query.
curl -g \
-X POST \
-H "content-type: application/json" \
-d '{"query":"{ user { id name bestFriend { id name } } }"}' \
"http://localhost:4000/graphql"
Execute this will give us the following server log output:
DEBUG Parsing request to extract GraphQL parameters
DEBUG Processing GraphQL Parameters
INFO resolving user
INFO resolved user
INFO resolving user best friend
DEBUG Processing GraphQL Parameters done.
Now, let’s cancel the request by closing the terminal or pressing Ctrl + C
after the server shows
the INFO resolving user
log.
This will now log the following.
DEBUG Parsing request to extract GraphQL parameters
DEBUG Processing GraphQL Parameters
INFO resolving user
DEBUG Request aborted
INFO resolved user
The interesting part is that now DEBUG Request aborted
is shown. At this point, any further
GraphQL execution will be stopped and no other GraphQL resolvers will be invoked.
However, the INFO resolved user
log is still showing up.
We can further adjust our server to cancel the artificial delay promise when the request is aborted to avoid any further processing.
import { createServer } from 'node:http'
import { createLogger, createSchema, createYoga, useExecutionCancellation } from 'graphql-yoga'
const logger = createLogger('debug')
const schema = createSchema({
typeDefs: /* GraphQL */ `
type Query {
user: User
}
type User {
id: ID!
name: String!
bestFriend: User
}
`,
resolvers: {
Query: {
async user(_, __, { request }) {
logger.info('resolving user')
await new Promise((resolve, reject) => {
const timeout = setTimeout(resolve, 5000)
request.signal.addEventListener('abort', () => {
clearTimeout(timeout)
reject(request.signal.reason)
})
})
logger.info('resolved user')
return {
id: '1',
name: 'Chewie'
}
}
},
User: {
bestFriend() {
logger.info('resolving user best friend')
return {
id: '2',
name: 'Han Solo'
}
}
}
}
})
// Provide your schema
const yoga = createYoga({
plugins: [useExecutionCancellation()],
schema,
logging: logger
})
// Start the server and explore http://localhost:4000/graphql
const server = createServer(yoga)
server.listen(4000, () => {
console.info('Server is running on http://localhost:4000/graphql')
})
After these adjustments, the timer will be cleaned up properly, and INFO resolved user
, will no
longer show up in the logs.
DEBUG Parsing request to extract GraphQL parameters
DEBUG Processing GraphQL Parameters
INFO resolving user
DEBUG Request aborted
Propagate cancellation in resolvers
As shown in the previous Selection, you can utilize the Yoga context object request.signal
to
cancel pending asynchronous operations in your resolvers.
fetch
example
In this example we just pass context.request.signal
to the fetch call. This will cancel the fetch
request when the GraphQL request is canceled.
import { createSchema, createYoga } from 'graphql-yoga'
const yoga = createYoga({
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query {
greeting: String!
}
`,
resolvers: {
Query: {
greeting: async (_, args, context) => {
// This service does not exist
const greeting = await fetch('http://localhost:9876/greeting', {
signal: context.request.signal
}).then(res => res.text())
return greeting
}
}
}
})
})
const server = createServer(yoga)
server.listen(4000, () => {
console.info('Server is running on http://localhost:4000/graphql')
})
Further Resources
The execution cancelation API is built on top of the AbortController
and AbortSignal
APIs.
- MDN AbortController and
- MDN AbortSignal APIs
The execution cancelation is a feature of our GraphQL executor that is a fork of graphql-js
.