Skip to content

Commit

Permalink
Fix crash in node when mixing sync/async resolvers (#3706)
Browse files Browse the repository at this point in the history
Co-authored-by: Ivan Goncharov <ivan.goncharov.ua@gmail.com>
  • Loading branch information
chrskrchr and IvanGoncharov committed Oct 17, 2022
1 parent 90774d7 commit 5009d9f
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.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { assert, expect } from 'chai';
import { describe, it } from 'mocha';

import { expectJSON } from '../../__testUtils__/expectJSON.js';
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js';

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

Expand Down Expand Up @@ -580,6 +581,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);
expectJSON(await result).toDeepEqual({
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: GraphQLObjectType = new GraphQLObjectType({
name: 'A',
Expand Down
38 changes: 24 additions & 14 deletions src/execution/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,23 +624,33 @@ function executeFields(
const results = Object.create(null);
let containsPromise = false;

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

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 5009d9f

Please sign in to comment.