diff --git a/src/execution/__tests__/cancellation-test.ts b/src/execution/__tests__/cancellation-test.ts index 3c2f41553f..19a6dc35f8 100644 --- a/src/execution/__tests__/cancellation-test.ts +++ b/src/execution/__tests__/cancellation-test.ts @@ -130,9 +130,19 @@ describe('Execute: Cancellation', () => { } `); + let aborted = false; const cancellableAsyncFn = async (abortSignal: AbortSignal) => { + if (abortSignal.aborted) { + aborted = true; + } else { + abortSignal.addEventListener('abort', () => { + aborted = true; + }); + } + // We are in an async function so it gets cancelled and the field ends up + // resolving with the abort signal's error. await resolveOnNextTick(); - abortSignal.throwIfAborted(); + throw Error('some random other error that does not show up in response'); }; const resultPromise = execute({ @@ -141,8 +151,8 @@ describe('Execute: Cancellation', () => { abortSignal: abortController.signal, rootValue: { todo: { - id: (_args: any, _context: any, _info: any, signal: AbortSignal) => - cancellableAsyncFn(signal), + id: (_args: any, _context: any, info: { abortSignal: AbortSignal }) => + cancellableAsyncFn(info.abortSignal), }, }, }); @@ -165,6 +175,8 @@ describe('Execute: Cancellation', () => { }, ], }); + + expect(aborted).to.equal(true); }); it('should stop the execution when aborted during object field completion with a custom error', async () => { diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 05e1c293f9..e5d5eedee7 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -223,6 +223,7 @@ describe('Execute: Handles basic execution tasks', () => { 'rootValue', 'operation', 'variableValues', + 'abortSignal', ); const operation = document.definitions[0]; diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 70edfeb8a8..bcf21e9737 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -875,6 +875,7 @@ function executeField( toNodes(fieldDetailsList), parentType, path, + abortSignal, ); // Get the resolve function, regardless of if its result is normal or abrupt (error). @@ -893,7 +894,7 @@ function executeField( // The resolve function's optional third argument is a context value that // is provided to every resolve function within an execution. It is commonly // used to represent an authenticated user, or request-specific caches. - const result = resolveFn(source, args, contextValue, info, abortSignal); + const result = resolveFn(source, args, contextValue, info); if (isPromise(result)) { return completePromisedValue( @@ -960,6 +961,7 @@ export function buildResolveInfo( fieldNodes: ReadonlyArray, parentType: GraphQLObjectType, path: Path, + abortSignal: AbortSignal | undefined, ): GraphQLResolveInfo { const { schema, fragmentDefinitions, rootValue, operation, variableValues } = validatedExecutionArgs; @@ -976,6 +978,7 @@ export function buildResolveInfo( rootValue, operation, variableValues, + abortSignal, }; } @@ -2079,12 +2082,12 @@ export const defaultTypeResolver: GraphQLTypeResolver = * of calling that function while passing along args and context value. */ export const defaultFieldResolver: GraphQLFieldResolver = - function (source: any, args, contextValue, info, abortSignal) { + function (source: any, args, contextValue, info) { // ensure source is a value for which property access is acceptable. if (isObjectLike(source) || typeof source === 'function') { const property = source[info.fieldName]; if (typeof property === 'function') { - return source[info.fieldName](args, contextValue, info, abortSignal); + return source[info.fieldName](args, contextValue, info); } return property; } @@ -2293,6 +2296,7 @@ function executeSubscription( fieldNodes, rootType, path, + abortSignal, ); try { @@ -2317,7 +2321,7 @@ function executeSubscription( // The resolve function's optional third argument is a context value that // is provided to every resolve function within an execution. It is commonly // used to represent an authenticated user, or request-specific caches. - const result = resolveFn(rootValue, args, contextValue, info, abortSignal); + const result = resolveFn(rootValue, args, contextValue, info); if (isPromise(result)) { const abortSignalListener = abortSignal diff --git a/src/type/definition.ts b/src/type/definition.ts index ea96be5153..4cda4de25a 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -997,7 +997,6 @@ export type GraphQLFieldResolver< args: TArgs, context: TContext, info: GraphQLResolveInfo, - abortSignal: AbortSignal | undefined, ) => TResult; export interface GraphQLResolveInfo { @@ -1011,6 +1010,7 @@ export interface GraphQLResolveInfo { readonly rootValue: unknown; readonly operation: OperationDefinitionNode; readonly variableValues: VariableValues; + readonly abortSignal: AbortSignal | undefined; } /** diff --git a/website/pages/upgrade-guides/v16-v17.mdx b/website/pages/upgrade-guides/v16-v17.mdx index 00b8a27343..df97e69606 100644 --- a/website/pages/upgrade-guides/v16-v17.mdx +++ b/website/pages/upgrade-guides/v16-v17.mdx @@ -178,7 +178,7 @@ Use the `validateInputValue` helper to retrieve the actual errors. - Added `hideSuggestions` option to `execute`/`validate`/`subscribe`/... to hide schema-suggestions in error messages - Added `abortSignal` option to `graphql()`, `execute()`, and `subscribe()` allows cancellation of these methods; - the `abortSignal` can also be passed to field resolvers to cancel asynchronous work that they initiate. + `info.abortSignal` can also be used in field resolvers to cancel asynchronous work that they initiate. - `extensions` support `symbol` keys, in addition to the normal string keys. - Added ability for resolver functions to return async iterables. - Added `perEventExecutor` execution option to allows specifying a custom executor for subscription source stream events, which can be useful for preparing a per event execution context argument.