Skip to content

Commit e3b2a5d

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

File tree

2 files changed

+124
-1
lines changed

2 files changed

+124
-1
lines changed

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

Lines changed: 121 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,80 @@ 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: [
220+
PetType,
221+
TypeA,
222+
TypeB,
223+
SearchableInterface,
224+
PersonType,
225+
DogType,
226+
CatType,
227+
], // Added new types
228+
});
229+
// End of new types
230+
157231
describe('Execute: Union and intersection types', () => {
158232
it('can introspect on union and intersection types', () => {
159233
const document = parse(`
@@ -545,4 +619,50 @@ describe('Execute: Union and intersection types', () => {
545619
expect(encounteredRootValue).to.equal(rootValue);
546620
expect(encounteredContext).to.equal(contextValue);
547621
});
622+
623+
it('handles promises from isTypeOf correctly when a later type matches synchronously', async () => {
624+
const document = parse(`
625+
query TestSearch {
626+
search(id: "b") {
627+
__typename
628+
id
629+
... on TypeA {
630+
nameA
631+
}
632+
... on TypeB {
633+
nameB
634+
}
635+
}
636+
}
637+
`);
638+
639+
let unhandledRejection: any = null;
640+
/* c8 ignore start */
641+
const unhandledRejectionListener = (reason: any) => {
642+
unhandledRejection = reason;
643+
};
644+
process.on('unhandledRejection', unhandledRejectionListener);
645+
/* c8 ignore end */
646+
647+
const result = await execute({
648+
schema: schemaWithSearchable,
649+
document,
650+
});
651+
652+
expect(result.errors).to.be.undefined;
653+
expect(result.data).to.deep.equal({
654+
search: {
655+
__typename: 'TypeB',
656+
id: 'b',
657+
nameB: 'Object B',
658+
},
659+
});
660+
661+
// Give the TypeA promise a chance to reject and the listener to fire
662+
await new Promise((resolve) => setTimeout(resolve, 20));
663+
664+
process.removeListener('unhandledRejection', unhandledRejectionListener);
665+
666+
expect(unhandledRejection).to.be.null;
667+
});
548668
});

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)