Skip to content

Commit a7db60b

Browse files
JoviDeCroockbenjie
andauthored
Catch unhandled exception in abstract resolution (#4392)
Co-authored-by: Benjie <benjie@jemjie.com>
1 parent dd53d99 commit a7db60b

File tree

3 files changed

+123
-3
lines changed

3 files changed

+123
-3
lines changed

.c8rc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"src/jsutils/ObjMap.ts",
99
"src/jsutils/PromiseOrValue.ts",
1010
"src/utilities/assertValidName.ts",
11-
"src/utilities/typedQueryDocumentNode.ts"
11+
"src/utilities/typedQueryDocumentNode.ts",
12+
"src/**/__tests__/**/*.ts"
1213
],
1314
"clean": true,
1415
"temp-directory": "coverage",

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

Lines changed: 113 additions & 2 deletions
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;
@@ -118,7 +118,6 @@ const PetType = new GraphQLUnionType({
118118
if (value instanceof Cat) {
119119
return CatType.name;
120120
}
121-
/* c8 ignore next 3 */
122121
// Not reachable, all possible types have been considered.
123122
expect.fail('Not reachable');
124123
},
@@ -154,6 +153,71 @@ odie.mother.progeny = [odie];
154153
const liz = new Person('Liz');
155154
const john = new Person('John', [garfield, odie], [liz, odie]);
156155

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

src/execution/execute.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,14 @@ export const defaultTypeResolver: GraphQLTypeResolver<unknown, unknown> =
10021002
if (isPromise(isTypeOfResult)) {
10031003
promisedIsTypeOfResults[i] = isTypeOfResult;
10041004
} else if (isTypeOfResult) {
1005+
if (promisedIsTypeOfResults.length) {
1006+
// Explicitly ignore any promise rejections
1007+
Promise.allSettled(promisedIsTypeOfResults)
1008+
/* c8 ignore next 3 */
1009+
.catch(() => {
1010+
// Do nothing
1011+
});
1012+
}
10051013
return type.name;
10061014
}
10071015
}

0 commit comments

Comments
 (0)