Skip to content

Commit 2a87e39

Browse files
authored
fix(execute): handle defaultTypeResolver promise rejections (#4646)
1 parent c9ac696 commit 2a87e39

File tree

2 files changed

+143
-41
lines changed

2 files changed

+143
-41
lines changed

src/execution/__tests__/union-interface-test.ts

Lines changed: 117 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -672,30 +672,37 @@ describe('Execute: Union and intersection types', () => {
672672
const rootValue = new Person('John', [], [liz], [garfield]);
673673
const contextValue = { authToken: '123abc' };
674674

675-
/* c8 ignore next 4 */
675+
let unhandledRejection: unknown = null;
676+
const unhandledRejectionListener = (reason: unknown) => {
677+
unhandledRejection = reason;
678+
};
676679
// eslint-disable-next-line no-undef
677-
process.on('unhandledRejection', () => {
678-
expect.fail('Unhandled rejection');
679-
});
680-
681-
const result = await execute({
682-
schema,
683-
document,
684-
rootValue,
685-
contextValue,
686-
});
680+
process.on('unhandledRejection', unhandledRejectionListener);
687681

688-
expectJSON(result).toDeepEqual({
689-
data: {
690-
responsibilities: [
691-
{
692-
__typename: 'Cat',
693-
meows: false,
694-
name: 'Garfield',
695-
},
696-
],
697-
},
698-
});
682+
try {
683+
const result = await execute({
684+
schema,
685+
document,
686+
rootValue,
687+
contextValue,
688+
});
689+
690+
expectJSON(result).toDeepEqual({
691+
data: {
692+
responsibilities: [
693+
{
694+
__typename: 'Cat',
695+
meows: false,
696+
name: 'Garfield',
697+
},
698+
],
699+
},
700+
});
701+
} finally {
702+
// eslint-disable-next-line no-undef
703+
process.removeListener('unhandledRejection', unhandledRejectionListener);
704+
}
705+
expect(unhandledRejection).to.equal(null);
699706
});
700707

701708
it('handles promises from isTypeOf correctly when a later type matches synchronously', async () => {
@@ -718,7 +725,7 @@ describe('Execute: Union and intersection types', () => {
718725
const unhandledRejectionListener = (reason: any) => {
719726
unhandledRejection = reason;
720727
};
721-
// eslint-disable-next-line
728+
// eslint-disable-next-line no-undef
722729
process.on('unhandledRejection', unhandledRejectionListener);
723730

724731
const result = await execute({
@@ -739,7 +746,93 @@ describe('Execute: Union and intersection types', () => {
739746

740747
await new Promise((resolve) => setTimeout(resolve, 20));
741748

742-
// eslint-disable-next-line
749+
// eslint-disable-next-line no-undef
750+
process.removeListener('unhandledRejection', unhandledRejectionListener);
751+
752+
expect(unhandledRejection).to.equal(null);
753+
});
754+
755+
it('handles pending isTypeOf rejections when a later isTypeOf throws synchronously', async () => {
756+
const ThrowingSearchableInterface = new GraphQLInterfaceType({
757+
name: 'ThrowingSearchable',
758+
fields: {
759+
id: { type: GraphQLString },
760+
},
761+
});
762+
763+
const TypeAsyncReject = new GraphQLObjectType({
764+
name: 'TypeAsyncReject',
765+
interfaces: [ThrowingSearchableInterface],
766+
fields: () => ({
767+
id: { type: GraphQLString },
768+
nameAsyncReject: { type: GraphQLString },
769+
}),
770+
isTypeOf: (_value, _context, _info) =>
771+
new Promise((_resolve, reject) =>
772+
setTimeout(
773+
() => reject(new Error('TypeAsyncReject_isTypeOf_rejected')),
774+
10,
775+
),
776+
),
777+
});
778+
779+
const TypeThrowing = new GraphQLObjectType({
780+
name: 'TypeThrowing',
781+
interfaces: [ThrowingSearchableInterface],
782+
fields: () => ({
783+
id: { type: GraphQLString },
784+
nameThrowing: { type: GraphQLString },
785+
}),
786+
isTypeOf: () => {
787+
throw new Error('TypeThrowing_isTypeOf_threw');
788+
},
789+
});
790+
791+
const schemaWithThrowingIsTypeOf = new GraphQLSchema({
792+
query: new GraphQLObjectType({
793+
name: 'Query',
794+
fields: {
795+
search: {
796+
type: ThrowingSearchableInterface,
797+
resolve: () => ({ id: 'x', nameThrowing: 'Object X' }),
798+
},
799+
},
800+
}),
801+
types: [TypeAsyncReject, TypeThrowing],
802+
});
803+
804+
const document = parse(`
805+
{
806+
search {
807+
__typename
808+
id
809+
... on TypeThrowing {
810+
nameThrowing
811+
}
812+
}
813+
}
814+
`);
815+
816+
let unhandledRejection: unknown = null;
817+
const unhandledRejectionListener = (reason: unknown) => {
818+
unhandledRejection = reason;
819+
};
820+
// eslint-disable-next-line no-undef
821+
process.on('unhandledRejection', unhandledRejectionListener);
822+
823+
const result = await execute({
824+
schema: schemaWithThrowingIsTypeOf,
825+
document,
826+
});
827+
828+
expect(result.data).to.deep.equal({
829+
search: null,
830+
});
831+
expect(result.errors?.[0].message).to.equal('TypeThrowing_isTypeOf_threw');
832+
833+
await new Promise((resolve) => setTimeout(resolve, 20));
834+
835+
// eslint-disable-next-line no-undef
743836
process.removeListener('unhandledRejection', unhandledRejectionListener);
744837

745838
expect(unhandledRejection).to.equal(null);

src/execution/execute.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -441,26 +441,35 @@ export const defaultTypeResolver: GraphQLTypeResolver<unknown, unknown> =
441441
const possibleTypes = info.schema.getPossibleTypes(abstractType);
442442
const promisedIsTypeOfResults = [];
443443

444-
for (let i = 0; i < possibleTypes.length; i++) {
445-
const type = possibleTypes[i];
446-
447-
if (type.isTypeOf) {
448-
const isTypeOfResult = type.isTypeOf(value, contextValue, info);
449-
450-
if (isPromise(isTypeOfResult)) {
451-
promisedIsTypeOfResults[i] = isTypeOfResult;
452-
} else if (isTypeOfResult) {
453-
if (promisedIsTypeOfResults.length) {
454-
// Explicitly ignore any promise rejections
455-
Promise.allSettled(promisedIsTypeOfResults)
456-
/* c8 ignore next 3 */
457-
.catch(() => {
458-
// Do nothing
459-
});
444+
try {
445+
for (let i = 0; i < possibleTypes.length; i++) {
446+
const type = possibleTypes[i];
447+
448+
if (type.isTypeOf) {
449+
const isTypeOfResult = type.isTypeOf(value, contextValue, info);
450+
451+
if (isPromise(isTypeOfResult)) {
452+
promisedIsTypeOfResults[i] = isTypeOfResult;
453+
} else if (isTypeOfResult) {
454+
if (promisedIsTypeOfResults.length) {
455+
// Explicitly ignore any promise rejections
456+
Promise.allSettled(promisedIsTypeOfResults)
457+
/* c8 ignore next 3 */
458+
.catch(() => {
459+
// Do nothing
460+
});
461+
}
462+
return type.name;
460463
}
461-
return type.name;
462464
}
463465
}
466+
} catch (error) {
467+
if (promisedIsTypeOfResults.length) {
468+
return Promise.allSettled(promisedIsTypeOfResults).then(() => {
469+
throw error;
470+
});
471+
}
472+
throw error;
464473
}
465474

466475
if (promisedIsTypeOfResults.length) {

0 commit comments

Comments
 (0)