Skip to content

Commit

Permalink
Fix crash in node when mixing sync/async resolvers (backport of #3706) (
Browse files Browse the repository at this point in the history
#3708)

Co-authored-by: Ivan Goncharov <[email protected]>
  • Loading branch information
chrskrchr and IvanGoncharov authored Oct 17, 2022
1 parent ef6688d commit b47e306
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 14 deletions.
51 changes: 51 additions & 0 deletions src/execution/__tests__/executor-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, it } from 'mocha';

import inspect from '../../jsutils/inspect';
import invariant from '../../jsutils/invariant';
import resolveOnNextTick from '../../__testUtils__/resolveOnNextTick';

import { Kind } from '../../language/kinds';
import { parse } from '../../language/parser';
Expand Down Expand Up @@ -646,6 +647,56 @@ describe('Execute: Handles basic execution tasks', () => {
});
});

it('handles sync errors combined with rejections', async () => {
let isAsyncResolverFinished = false;

const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
syncNullError: {
type: new GraphQLNonNull(GraphQLString),
resolve: () => null,
},
asyncNullError: {
type: new GraphQLNonNull(GraphQLString),
async resolve() {
await resolveOnNextTick();
await resolveOnNextTick();
await resolveOnNextTick();
isAsyncResolverFinished = true;
return null;
},
},
},
}),
});

// Order is important here, as the promise has to be created before the synchronous error is thrown
const document = parse(`
{
asyncNullError
syncNullError
}
`);

const result = execute({ schema, document });

expect(isAsyncResolverFinished).to.equal(false);
expect(await result).to.deep.equal({
data: null,
errors: [
{
message:
'Cannot return null for non-nullable field Query.syncNullError.',
locations: [{ line: 4, column: 9 }],
path: ['syncNullError'],
},
],
});
expect(isAsyncResolverFinished).to.equal(true);
});

it('Full response path is included for non-nullable fields', () => {
const A = new GraphQLObjectType({
name: 'A',
Expand Down
38 changes: 24 additions & 14 deletions src/execution/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -456,23 +456,33 @@ function executeFields(
const results = Object.create(null);
let containsPromise = false;

for (const responseName of Object.keys(fields)) {
const fieldNodes = fields[responseName];
const fieldPath = addPath(path, responseName, parentType.name);
const result = resolveField(
exeContext,
parentType,
sourceValue,
fieldNodes,
fieldPath,
);
try {
for (const responseName of Object.keys(fields)) {
const fieldNodes = fields[responseName];
const fieldPath = addPath(path, responseName, parentType.name);
const result = resolveField(
exeContext,
parentType,
sourceValue,
fieldNodes,
fieldPath,
);

if (result !== undefined) {
results[responseName] = result;
if (isPromise(result)) {
containsPromise = true;
if (result !== undefined) {
results[responseName] = result;
if (isPromise(result)) {
containsPromise = true;
}
}
}
} catch (error) {
if (containsPromise) {
// Ensure that any promises returned by other fields are handled, as they may also reject.
return promiseForObject(results).finally(() => {
throw error;
});
}
throw error;
}

// If there are no promises, we can just return the object
Expand Down

0 comments on commit b47e306

Please sign in to comment.