Skip to content

Commit 4a82557

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

File tree

2 files changed

+74
-13
lines changed

2 files changed

+74
-13
lines changed

src/execution/__tests__/executor-test.ts

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

44
import { expectJSON } from '../../__testUtils__/expectJSON';
5+
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick';
56

67
import { inspect } from '../../jsutils/inspect';
78
import { invariant } from '../../jsutils/invariant';
@@ -625,6 +626,56 @@ describe('Execute: Handles basic execution tasks', () => {
625626
});
626627
});
627628

629+
it('handles sync errors combined with rejections', async () => {
630+
let isAsyncResolverFinished = false;
631+
632+
const schema = new GraphQLSchema({
633+
query: new GraphQLObjectType({
634+
name: 'Query',
635+
fields: {
636+
syncNullError: {
637+
type: new GraphQLNonNull(GraphQLString),
638+
resolve: () => null,
639+
},
640+
asyncNullError: {
641+
type: new GraphQLNonNull(GraphQLString),
642+
async resolve() {
643+
await resolveOnNextTick();
644+
await resolveOnNextTick();
645+
await resolveOnNextTick();
646+
isAsyncResolverFinished = true;
647+
return null;
648+
},
649+
},
650+
},
651+
}),
652+
});
653+
654+
// Order is important here, as the promise has to be created before the synchronous error is thrown
655+
const document = parse(`
656+
{
657+
asyncNullError
658+
syncNullError
659+
}
660+
`);
661+
662+
const result = execute({ schema, document });
663+
664+
expect(isAsyncResolverFinished).to.equal(false);
665+
expectJSON(await result).toDeepEqual({
666+
data: null,
667+
errors: [
668+
{
669+
message:
670+
'Cannot return null for non-nullable field Query.syncNullError.',
671+
locations: [{ line: 4, column: 9 }],
672+
path: ['syncNullError'],
673+
},
674+
],
675+
});
676+
expect(isAsyncResolverFinished).to.equal(true);
677+
});
678+
628679
it('Full response path is included for non-nullable fields', () => {
629680
const A: GraphQLObjectType = new GraphQLObjectType({
630681
name: 'A',

src/execution/execute.ts

+23-13
Original file line numberDiff line numberDiff line change
@@ -445,22 +445,32 @@ function executeFields(
445445
const results = Object.create(null);
446446
let containsPromise = false;
447447

448-
for (const [responseName, fieldNodes] of fields.entries()) {
449-
const fieldPath = addPath(path, responseName, parentType.name);
450-
const result = executeField(
451-
exeContext,
452-
parentType,
453-
sourceValue,
454-
fieldNodes,
455-
fieldPath,
456-
);
448+
try {
449+
for (const [responseName, fieldNodes] of fields.entries()) {
450+
const fieldPath = addPath(path, responseName, parentType.name);
451+
const result = executeField(
452+
exeContext,
453+
parentType,
454+
sourceValue,
455+
fieldNodes,
456+
fieldPath,
457+
);
457458

458-
if (result !== undefined) {
459-
results[responseName] = result;
460-
if (isPromise(result)) {
461-
containsPromise = true;
459+
if (result !== undefined) {
460+
results[responseName] = result;
461+
if (isPromise(result)) {
462+
containsPromise = true;
463+
}
462464
}
463465
}
466+
} catch (error) {
467+
if (containsPromise) {
468+
// Ensure that any promises returned by other fields are handled, as they may also reject.
469+
return promiseForObject(results).finally(() => {
470+
throw error;
471+
});
472+
}
473+
throw error;
464474
}
465475

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

0 commit comments

Comments
 (0)