Skip to content

Commit 7445c67

Browse files
chrskrchrIvanGoncharov
authored andcommitted
Fix crash in node when mixing sync/async resolvers
1 parent 80325b5 commit 7445c67

File tree

2 files changed

+74
-14
lines changed

2 files changed

+74
-14
lines changed

src/execution/__tests__/executor-test.ts

Lines changed: 50 additions & 0 deletions
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,55 @@ describe('Execute: Handles basic execution tasks', () => {
580581
});
581582
});
582583

584+
it('handles sync errors combined with rejections', async () => {
585+
let isAsyncResolverCalled = 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+
isAsyncResolverCalled = true;
602+
return Promise.resolve(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 = await execute({ schema, document });
618+
619+
expect(isAsyncResolverCalled).to.equal(true);
620+
expectJSON(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+
});
632+
583633
it('Full response path is included for non-nullable fields', () => {
584634
const A: GraphQLObjectType = new GraphQLObjectType({
585635
name: 'A',

src/execution/execute.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -623,23 +623,33 @@ function executeFields(
623623
const results = Object.create(null);
624624
let containsPromise = false;
625625

626-
for (const [responseName, fieldNodes] of fields) {
627-
const fieldPath = addPath(path, responseName, parentType.name);
628-
const result = executeField(
629-
exeContext,
630-
parentType,
631-
sourceValue,
632-
fieldNodes,
633-
fieldPath,
634-
asyncPayloadRecord,
635-
);
626+
try {
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+
);
636637

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

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

0 commit comments

Comments
 (0)