Skip to content

Commit b47e306

Browse files
Fix crash in node when mixing sync/async resolvers (backport of #3706) (#3708)
Co-authored-by: Ivan Goncharov <[email protected]>
1 parent ef6688d commit b47e306

File tree

2 files changed

+75
-14
lines changed

2 files changed

+75
-14
lines changed

src/execution/__tests__/executor-test.js

+51
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { describe, it } from 'mocha';
33

44
import inspect from '../../jsutils/inspect';
55
import invariant from '../../jsutils/invariant';
6+
import resolveOnNextTick from '../../__testUtils__/resolveOnNextTick';
67

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

650+
it('handles sync errors combined with rejections', async () => {
651+
let isAsyncResolverFinished = false;
652+
653+
const schema = new GraphQLSchema({
654+
query: new GraphQLObjectType({
655+
name: 'Query',
656+
fields: {
657+
syncNullError: {
658+
type: new GraphQLNonNull(GraphQLString),
659+
resolve: () => null,
660+
},
661+
asyncNullError: {
662+
type: new GraphQLNonNull(GraphQLString),
663+
async resolve() {
664+
await resolveOnNextTick();
665+
await resolveOnNextTick();
666+
await resolveOnNextTick();
667+
isAsyncResolverFinished = true;
668+
return null;
669+
},
670+
},
671+
},
672+
}),
673+
});
674+
675+
// Order is important here, as the promise has to be created before the synchronous error is thrown
676+
const document = parse(`
677+
{
678+
asyncNullError
679+
syncNullError
680+
}
681+
`);
682+
683+
const result = execute({ schema, document });
684+
685+
expect(isAsyncResolverFinished).to.equal(false);
686+
expect(await result).to.deep.equal({
687+
data: null,
688+
errors: [
689+
{
690+
message:
691+
'Cannot return null for non-nullable field Query.syncNullError.',
692+
locations: [{ line: 4, column: 9 }],
693+
path: ['syncNullError'],
694+
},
695+
],
696+
});
697+
expect(isAsyncResolverFinished).to.equal(true);
698+
});
699+
649700
it('Full response path is included for non-nullable fields', () => {
650701
const A = new GraphQLObjectType({
651702
name: 'A',

src/execution/execute.js

+24-14
Original file line numberDiff line numberDiff line change
@@ -456,23 +456,33 @@ function executeFields(
456456
const results = Object.create(null);
457457
let containsPromise = false;
458458

459-
for (const responseName of Object.keys(fields)) {
460-
const fieldNodes = fields[responseName];
461-
const fieldPath = addPath(path, responseName, parentType.name);
462-
const result = resolveField(
463-
exeContext,
464-
parentType,
465-
sourceValue,
466-
fieldNodes,
467-
fieldPath,
468-
);
459+
try {
460+
for (const responseName of Object.keys(fields)) {
461+
const fieldNodes = fields[responseName];
462+
const fieldPath = addPath(path, responseName, parentType.name);
463+
const result = resolveField(
464+
exeContext,
465+
parentType,
466+
sourceValue,
467+
fieldNodes,
468+
fieldPath,
469+
);
469470

470-
if (result !== undefined) {
471-
results[responseName] = result;
472-
if (isPromise(result)) {
473-
containsPromise = true;
471+
if (result !== undefined) {
472+
results[responseName] = result;
473+
if (isPromise(result)) {
474+
containsPromise = true;
475+
}
474476
}
475477
}
478+
} catch (error) {
479+
if (containsPromise) {
480+
// Ensure that any promises returned by other fields are handled, as they may also reject.
481+
return promiseForObject(results).finally(() => {
482+
throw error;
483+
});
484+
}
485+
throw error;
476486
}
477487

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

0 commit comments

Comments
 (0)