Skip to content

Commit 6223245

Browse files
Slavaleebyron
authored andcommitted
Errors thrown from resolvers have the execution path (#396)
* Errors thrown from resolvers have the execution path This path is also passed in the `info` object to resolvers. This information is useful for ease of debugging and more detailed logging. * Remove PathedError * rename property executionPath to path * remove an unnecessary block * info.executionPath -> info.path * a minor tweak to make the body of executeFields look closer to executeFieldsSerially * remove the unnecessary clone of info * Add a test for a path with non-nullable fields * stylistic changes * remove stray property
1 parent 572bbda commit 6223245

File tree

7 files changed

+272
-14
lines changed

7 files changed

+272
-14
lines changed

src/__tests__/starWarsIntrospection-test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,13 @@ describe('Star Wars Introspection Tests', () => {
205205
kind: 'LIST'
206206
}
207207
},
208+
{
209+
name: 'secretBackstory',
210+
type: {
211+
name: 'String',
212+
kind: 'SCALAR'
213+
}
214+
},
208215
{
209216
name: 'primaryFunction',
210217
type: {
@@ -284,6 +291,14 @@ describe('Star Wars Introspection Tests', () => {
284291
}
285292
}
286293
},
294+
{
295+
name: 'secretBackstory',
296+
type: {
297+
name: 'String',
298+
kind: 'SCALAR',
299+
ofType: null
300+
}
301+
},
287302
{
288303
name: 'primaryFunction',
289304
type: {

src/__tests__/starWarsQuery-test.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ import { expect } from 'chai';
1111
import { describe, it } from 'mocha';
1212
import { StarWarsSchema } from './starWarsSchema.js';
1313
import { graphql } from '../graphql';
14+
import {
15+
GraphQLObjectType,
16+
GraphQLNonNull,
17+
GraphQLSchema,
18+
GraphQLString,
19+
} from '../type';
1420

1521
// 80+ char lines are useful in describe/it, so ignore in this file.
1622
/* eslint-disable max-len */
@@ -364,4 +370,162 @@ describe('Star Wars Query Tests', () => {
364370
expect(result).to.deep.equal({ data: expected });
365371
});
366372
});
373+
374+
describe('Reporting errors raised in resolvers', () => {
375+
it('Correctly reports error on accessing secretBackstory', async () => {
376+
const query = `
377+
query HeroNameQuery {
378+
hero {
379+
name
380+
secretBackstory
381+
}
382+
}
383+
`;
384+
const expected = {
385+
hero: {
386+
name: 'R2-D2',
387+
secretBackstory: null
388+
}
389+
};
390+
const expectedErrors = [ 'secretBackstory is secret.' ];
391+
const result = await graphql(StarWarsSchema, query);
392+
expect(result.data).to.deep.equal(expected);
393+
expect(result.errors.map(e => e.message)).to.deep.equal(expectedErrors);
394+
expect(
395+
result.errors.map(e => e.path)).to.deep.equal(
396+
[ [ 'hero', 'secretBackstory' ] ]);
397+
});
398+
399+
it('Correctly reports error on accessing secretBackstory in a list', async () => {
400+
const query = `
401+
query HeroNameQuery {
402+
hero {
403+
name
404+
friends {
405+
name
406+
secretBackstory
407+
}
408+
}
409+
}
410+
`;
411+
const expected = {
412+
hero: {
413+
name: 'R2-D2',
414+
friends: [
415+
{
416+
name: 'Luke Skywalker',
417+
secretBackstory: null,
418+
},
419+
{
420+
name: 'Han Solo',
421+
secretBackstory: null,
422+
},
423+
{
424+
name: 'Leia Organa',
425+
secretBackstory: null,
426+
},
427+
]
428+
}
429+
};
430+
const expectedErrors = [
431+
'secretBackstory is secret.',
432+
'secretBackstory is secret.',
433+
'secretBackstory is secret.',
434+
];
435+
const result = await graphql(StarWarsSchema, query);
436+
expect(result.data).to.deep.equal(expected);
437+
expect(result.errors.map(e => e.message)).to.deep.equal(expectedErrors);
438+
expect(
439+
result.errors.map(e => e.path)
440+
).to.deep.equal(
441+
[
442+
[ 'hero', 'friends', 0, 'secretBackstory' ],
443+
[ 'hero', 'friends', 1, 'secretBackstory' ],
444+
[ 'hero', 'friends', 2, 'secretBackstory' ],
445+
]);
446+
});
447+
448+
it('Correctly reports error on accessing through an alias', async () => {
449+
const query = `
450+
query HeroNameQuery {
451+
mainHero: hero {
452+
name
453+
story: secretBackstory
454+
}
455+
}
456+
`;
457+
const expected = {
458+
mainHero: {
459+
name: 'R2-D2',
460+
story: null,
461+
}
462+
};
463+
const expectedErrors = [
464+
'secretBackstory is secret.',
465+
];
466+
const result = await graphql(StarWarsSchema, query);
467+
expect(result.data).to.deep.equal(expected);
468+
expect(result.errors.map(e => e.message)).to.deep.equal(expectedErrors);
469+
expect(
470+
result.errors.map(e => e.path)
471+
).to.deep.equal([ [ 'mainHero', 'story' ] ]);
472+
});
473+
474+
it('Full response path is included when fields are non-nullable', async () => {
475+
const A = new GraphQLObjectType({
476+
name: 'A',
477+
fields: () => ({
478+
nullableA: {
479+
type: A,
480+
resolve: () => ({}),
481+
},
482+
nonNullA: {
483+
type: new GraphQLNonNull(A),
484+
resolve: () => ({}),
485+
},
486+
throws: {
487+
type: new GraphQLNonNull(GraphQLString),
488+
resolve: () => { throw new Error('Catch me if you can'); },
489+
},
490+
}),
491+
});
492+
const queryType = new GraphQLObjectType({
493+
name: 'query',
494+
fields: () => ({
495+
nullableA: {
496+
type: A,
497+
resolve: () => ({})
498+
}
499+
}),
500+
});
501+
const schema = new GraphQLSchema({
502+
query: queryType,
503+
});
504+
505+
const query = `
506+
query {
507+
nullableA {
508+
nullableA {
509+
nonNullA {
510+
nonNullA {
511+
throws
512+
}
513+
}
514+
}
515+
}
516+
}
517+
`;
518+
519+
const result = await graphql(schema, query);
520+
const expected = {
521+
nullableA: {
522+
nullableA: null
523+
}
524+
};
525+
expect(result.data).to.deep.equal(expected);
526+
expect(
527+
result.errors.map(e => e.path)).to.deep.equal(
528+
[ [ 'nullableA', 'nullableA', 'nonNullA', 'nonNullA', 'throws' ] ]);
529+
});
530+
});
367531
});

src/__tests__/starWarsSchema.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ const episodeEnum = new GraphQLEnumType({
102102
* name: String
103103
* friends: [Character]
104104
* appearsIn: [Episode]
105+
* secretBackstory: String
105106
* }
106107
*/
107108
const characterInterface = new GraphQLInterfaceType({
@@ -125,6 +126,10 @@ const characterInterface = new GraphQLInterfaceType({
125126
type: new GraphQLList(episodeEnum),
126127
description: 'Which movies they appear in.',
127128
},
129+
secretBackstory: {
130+
type: GraphQLString,
131+
description: 'All secrets about their past.',
132+
},
128133
}),
129134
resolveType: character => {
130135
return getHuman(character.id) ? humanType : droidType;
@@ -140,6 +145,7 @@ const characterInterface = new GraphQLInterfaceType({
140145
* name: String
141146
* friends: [Character]
142147
* appearsIn: [Episode]
148+
* secretBackstory: String
143149
* }
144150
*/
145151
const humanType = new GraphQLObjectType({
@@ -168,6 +174,13 @@ const humanType = new GraphQLObjectType({
168174
type: GraphQLString,
169175
description: 'The home planet of the human, or null if unknown.',
170176
},
177+
secretBackstory: {
178+
type: GraphQLString,
179+
description: 'Where are they from and how they came to be who they are.',
180+
resolve: () => {
181+
throw new Error('secretBackstory is secret.');
182+
},
183+
},
171184
}),
172185
interfaces: [ characterInterface ]
173186
});
@@ -181,6 +194,7 @@ const humanType = new GraphQLObjectType({
181194
* name: String
182195
* friends: [Character]
183196
* appearsIn: [Episode]
197+
* secretBackstory: String
184198
* primaryFunction: String
185199
* }
186200
*/
@@ -206,6 +220,13 @@ const droidType = new GraphQLObjectType({
206220
type: new GraphQLList(episodeEnum),
207221
description: 'Which movies they appear in.',
208222
},
223+
secretBackstory: {
224+
type: GraphQLString,
225+
description: 'Construction date and the name of the designer.',
226+
resolve: () => {
227+
throw new Error('secretBackstory is secret.');
228+
},
229+
},
209230
primaryFunction: {
210231
type: GraphQLString,
211232
description: 'The primary function of the droid.',

src/error/GraphQLError.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export class GraphQLError extends Error {
2020
source: Source;
2121
positions: Array<number>;
2222
locations: any;
23+
path: Array<string | number>;
2324
originalError: ?Error;
2425

2526
constructor(

src/error/locatedError.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ import { GraphQLError } from './GraphQLError';
1818
*/
1919
export function locatedError(
2020
originalError: ?Error,
21-
nodes: Array<any>
21+
nodes: Array<any>,
22+
path: Array<string | number>
2223
): GraphQLError {
2324
const message = originalError ?
2425
originalError.message || String(originalError) :
2526
'An unknown error occurred.';
2627
const stack = originalError ? originalError.stack : null;
2728
const error = new GraphQLError(message, nodes, stack);
2829
error.originalError = originalError;
30+
error.path = path;
2931
return error;
3032
}

0 commit comments

Comments
 (0)