Skip to content

Commit 5009d9f

Browse files
Fix crash in node when mixing sync/async resolvers (#3706)
Co-authored-by: Ivan Goncharov <[email protected]>
1 parent 90774d7 commit 5009d9f

File tree

2 files changed

+75
-14
lines changed

2 files changed

+75
-14
lines changed

Diff for: src/execution/__tests__/executor-test.ts

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

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

67
import { inspect } from '../../jsutils/inspect.js';
78

@@ -580,6 +581,56 @@ describe('Execute: Handles basic execution tasks', () => {
580581
});
581582
});
582583

584+
it('handles sync errors combined with rejections', async () => {
585+
let isAsyncResolverFinished = false;
586+
587+
const schema = new GraphQLSchema({
588+
query: new GraphQLObjectType({
589+
name: 'Query',
590+
fields: {
591+
syncNullError: {
592+
type: new GraphQLNonNull(GraphQLString),
593+
resolve: () => null,
594+
},
595+
asyncNullError: {
596+
type: new GraphQLNonNull(GraphQLString),
597+
async resolve() {
598+
await resolveOnNextTick();
599+
await resolveOnNextTick();
600+
await resolveOnNextTick();
601+
isAsyncResolverFinished = true;
602+
return null;
603+
},
604+
},
605+
},
606+
}),
607+
});
608+
609+
// Order is important here, as the promise has to be created before the synchronous error is thrown
610+
const document = parse(`
611+
{
612+
asyncNullError
613+
syncNullError
614+
}
615+
`);
616+
617+
const result = execute({ schema, document });
618+
619+
expect(isAsyncResolverFinished).to.equal(false);
620+
expectJSON(await result).toDeepEqual({
621+
data: null,
622+
errors: [
623+
{
624+
message:
625+
'Cannot return null for non-nullable field Query.syncNullError.',
626+
locations: [{ line: 4, column: 9 }],
627+
path: ['syncNullError'],
628+
},
629+
],
630+
});
631+
expect(isAsyncResolverFinished).to.equal(true);
632+
});
633+
583634
it('Full response path is included for non-nullable fields', () => {
584635
const A: GraphQLObjectType = new GraphQLObjectType({
585636
name: 'A',

Diff for: src/execution/execute.ts

+24-14
Original file line numberDiff line numberDiff line change
@@ -624,23 +624,33 @@ function executeFields(
624624
const results = Object.create(null);
625625
let containsPromise = false;
626626

627-
for (const [responseName, fieldNodes] of fields) {
628-
const fieldPath = addPath(path, responseName, parentType.name);
629-
const result = executeField(
630-
exeContext,
631-
parentType,
632-
sourceValue,
633-
fieldNodes,
634-
fieldPath,
635-
asyncPayloadRecord,
636-
);
627+
try {
628+
for (const [responseName, fieldNodes] of fields) {
629+
const fieldPath = addPath(path, responseName, parentType.name);
630+
const result = executeField(
631+
exeContext,
632+
parentType,
633+
sourceValue,
634+
fieldNodes,
635+
fieldPath,
636+
asyncPayloadRecord,
637+
);
637638

638-
if (result !== undefined) {
639-
results[responseName] = result;
640-
if (isPromise(result)) {
641-
containsPromise = true;
639+
if (result !== undefined) {
640+
results[responseName] = result;
641+
if (isPromise(result)) {
642+
containsPromise = true;
643+
}
642644
}
643645
}
646+
} catch (error) {
647+
if (containsPromise) {
648+
// Ensure that any promises returned by other fields are handled, as they may also reject.
649+
return promiseForObject(results).finally(() => {
650+
throw error;
651+
});
652+
}
653+
throw error;
644654
}
645655

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

0 commit comments

Comments
 (0)