Skip to content

Commit 660b30c

Browse files
committed
Catch unhandled exception in abstract resolution
1 parent 3de02c1 commit 660b30c

File tree

2 files changed

+116
-1
lines changed

2 files changed

+116
-1
lines changed

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

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
import { GraphQLBoolean, GraphQLString } from '../../type/scalars';
1313
import { GraphQLSchema } from '../../type/schema';
1414

15-
import { executeSync } from '../execute';
15+
import { execute, executeSync } from '../execute';
1616

1717
class Dog {
1818
name: string;
@@ -154,6 +154,72 @@ odie.mother.progeny = [odie];
154154
const liz = new Person('Liz');
155155
const john = new Person('John', [garfield, odie], [liz, odie]);
156156

157+
// New types for the unhandled rejection test
158+
const SearchableInterface = new GraphQLInterfaceType({
159+
name: 'Searchable',
160+
fields: {
161+
id: { type: GraphQLString },
162+
},
163+
// Deliberately no resolveType, to use isTypeOf from concrete types
164+
});
165+
166+
const TypeA = new GraphQLObjectType({
167+
name: 'TypeA',
168+
interfaces: [SearchableInterface],
169+
fields: () => ({
170+
id: { type: GraphQLString },
171+
nameA: { type: GraphQLString },
172+
}),
173+
isTypeOf: (_value, _context, _info) => {
174+
return new Promise((_resolve, reject) => {
175+
setTimeout(() => {
176+
reject(new Error('TypeA_isTypeOf_rejected'));
177+
}, 10);
178+
});
179+
},
180+
});
181+
182+
const TypeB = new GraphQLObjectType({
183+
name: 'TypeB',
184+
interfaces: [SearchableInterface],
185+
fields: () => ({
186+
id: { type: GraphQLString },
187+
nameB: { type: GraphQLString },
188+
}),
189+
isTypeOf: (value: any, _context, _info) => {
190+
return value.id === 'b';
191+
},
192+
});
193+
194+
const queryTypeWithSearchable = new GraphQLObjectType({
195+
name: 'Query',
196+
fields: {
197+
person: {
198+
type: PersonType,
199+
resolve: () => john,
200+
},
201+
search: {
202+
type: SearchableInterface,
203+
args: { id: { type: GraphQLString } },
204+
resolve: (_source, { id }) => {
205+
/* c8 ignore start */
206+
if (id === 'a') {
207+
return { id: 'a', nameA: 'Object A' };
208+
/* c8 ignore end */
209+
} else if (id === 'b') {
210+
return { id: 'b', nameB: 'Object B' };
211+
}
212+
},
213+
},
214+
},
215+
});
216+
217+
const schemaWithSearchable = new GraphQLSchema({
218+
query: queryTypeWithSearchable,
219+
types: [PetType, TypeA, TypeB, SearchableInterface, PersonType, DogType, CatType], // Added new types
220+
});
221+
// End of new types
222+
157223
describe('Execute: Union and intersection types', () => {
158224
it('can introspect on union and intersection types', () => {
159225
const document = parse(`
@@ -545,4 +611,50 @@ describe('Execute: Union and intersection types', () => {
545611
expect(encounteredRootValue).to.equal(rootValue);
546612
expect(encounteredContext).to.equal(contextValue);
547613
});
614+
615+
it('handles promises from isTypeOf correctly when a later type matches synchronously', async () => {
616+
const document = parse(`
617+
query TestSearch {
618+
search(id: "b") {
619+
__typename
620+
id
621+
... on TypeA {
622+
nameA
623+
}
624+
... on TypeB {
625+
nameB
626+
}
627+
}
628+
}
629+
`);
630+
631+
let unhandledRejection: any = null;
632+
/* c8 ignore start */
633+
const unhandledRejectionListener = (reason: any) => {
634+
unhandledRejection = reason;
635+
};
636+
process.on('unhandledRejection', unhandledRejectionListener);
637+
/* c8 ignore end */
638+
639+
const result = await execute({
640+
schema: schemaWithSearchable,
641+
document,
642+
});
643+
644+
expect(result.errors).to.be.undefined;
645+
expect(result.data).to.deep.equal({
646+
search: {
647+
__typename: 'TypeB',
648+
id: 'b',
649+
nameB: 'Object B',
650+
},
651+
});
652+
653+
// Give the TypeA promise a chance to reject and the listener to fire
654+
await new Promise((resolve) => setTimeout(resolve, 20));
655+
656+
process.removeListener('unhandledRejection', unhandledRejectionListener);
657+
658+
expect(unhandledRejection).to.be.null;
659+
});
548660
});

src/execution/execute.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,9 @@ export const defaultTypeResolver: GraphQLTypeResolver<unknown, unknown> =
10021002
if (isPromise(isTypeOfResult)) {
10031003
promisedIsTypeOfResults[i] = isTypeOfResult;
10041004
} else if (isTypeOfResult) {
1005+
if (promisedIsTypeOfResults.length) {
1006+
Promise.allSettled(promisedIsTypeOfResults);
1007+
}
10051008
return type.name;
10061009
}
10071010
}

0 commit comments

Comments
 (0)