v5 (latest)FeaturesExecution Cancellation

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.

Execution Cancellation configuration
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.

Simple GraphQL schema
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.

Full Yoga Example
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.

GraphQL Query for fetching user with best friend
query UserWithBestFriend {
  user {
    id
    name
    bestFriend {
      id
      name
    }
  }
}

For your convenience, you can copy the following curl command to execute the query.

Curl Request for executing the operation
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:

Server logs
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.

Server logs for canceled request
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.

Altered server example with timer cleanup
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.

Server Logs after timer cleanup
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.

Resolver fetch cancellation example
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.

The execution cancelation is a feature of our GraphQL executor that is a fork of graphql-js.