Skip to content

Commit fb8ed67

Browse files
authored
[FIX] Include locations for errors related to failed value completion. (#445)
Error locations were previously only being added due to async value resolution and not during value completion. This meant if an error was raised due to an incorrect response from a resolver function that it would not have gotten wrapped in a location-aware GraphQLError object. This includes a new intermediate phase which catches and assigns these locations before further propogating the error. This also assigns non-enumable property for `originalError` so it is not included during JSON.stringify.
1 parent 08a00aa commit fb8ed67

File tree

5 files changed

+99
-34
lines changed

5 files changed

+99
-34
lines changed

src/error/GraphQLError.js

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,21 @@ export class GraphQLError extends Error {
1717
message: string;
1818
stack: string;
1919
nodes: ?Array<Node>;
20-
source: Source;
21-
positions: Array<number>;
22-
locations: any;
23-
path: Array<string | number>;
20+
source: ?Source;
21+
positions: ?Array<number>;
22+
locations: ?Array<{ line: number, column: number }>;
23+
path: ?Array<string | number>;
2424
originalError: ?Error;
2525

2626
constructor(
2727
message: string,
2828
// A flow bug keeps us from declaring nodes as an array of Node
2929
nodes?: Array<any/* Node */>,
3030
stack?: ?string,
31-
source?: Source,
32-
positions?: Array<number>
31+
source?: ?Source,
32+
positions?: ?Array<number>,
33+
path?: ?Array<string|number>,
34+
originalError?: ?Error
3335
) {
3436
super(message);
3537

@@ -94,5 +96,17 @@ export class GraphQLError extends Error {
9496
// service adheres to the spec.
9597
enumerable: true,
9698
}: any));
99+
100+
Object.defineProperty(this, 'path', {
101+
value: path,
102+
// By being enumerable, JSON.stringify will include `path` in the
103+
// resulting output. This ensures that the simplist possible GraphQL
104+
// service adheres to the spec.
105+
enumerable: true
106+
});
107+
108+
Object.defineProperty(this, 'originalError', {
109+
value: originalError
110+
});
97111
}
98112
}

src/error/locatedError.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,23 @@ export function locatedError(
2121
nodes: Array<any>,
2222
path: Array<string | number>
2323
): GraphQLError {
24+
// Note: this uses a brand-check to support GraphQL errors originating from
25+
// other contexts.
26+
if (originalError && originalError.hasOwnProperty('locations')) {
27+
return (originalError: any);
28+
}
29+
2430
const message = originalError ?
2531
originalError.message || String(originalError) :
2632
'An unknown error occurred.';
2733
const stack = originalError ? originalError.stack : null;
28-
const error = new GraphQLError(message, nodes, stack);
29-
error.path = path;
30-
error.originalError = originalError;
31-
return error;
34+
return new GraphQLError(
35+
message,
36+
nodes,
37+
stack,
38+
null,
39+
null,
40+
path,
41+
originalError
42+
);
3243
}

src/execution/__tests__/executor-test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,6 @@ describe('Execute: Handles basic execution tasks', () => {
363363
locations: [ { line: 6, column: 7 } ] },
364364
{ message: 'Error getting syncReturnErrorList3',
365365
locations: [ { line: 6, column: 7 } ] },
366-
{ message: 'Error getting asyncReturnError',
367-
locations: [ { line: 13, column: 7 } ] },
368366
{ message: 'Error getting asyncReject',
369367
locations: [ { line: 8, column: 7 } ] },
370368
{ message: 'Error getting asyncRawReject',
@@ -375,6 +373,8 @@ describe('Execute: Handles basic execution tasks', () => {
375373
locations: [ { line: 11, column: 7 } ] },
376374
{ message: 'Error getting asyncRawError',
377375
locations: [ { line: 12, column: 7 } ] },
376+
{ message: 'Error getting asyncReturnError',
377+
locations: [ { line: 13, column: 7 } ] },
378378
]);
379379
});
380380

src/execution/execute.js

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -620,9 +620,9 @@ function completeValueCatchingError(
620620
result: mixed
621621
): mixed {
622622
// If the field type is non-nullable, then it is resolved without any
623-
// protection from errors.
623+
// protection from errors, however it still properly locates the error.
624624
if (returnType instanceof GraphQLNonNull) {
625-
return completeValue(
625+
return completeValueWithLocatedError(
626626
exeContext,
627627
returnType,
628628
fieldASTs,
@@ -635,7 +635,7 @@ function completeValueCatchingError(
635635
// Otherwise, error protection is applied, logging the error and resolving
636636
// a null value for this field if one is encountered.
637637
try {
638-
const completed = completeValue(
638+
const completed = completeValueWithLocatedError(
639639
exeContext,
640640
returnType,
641641
fieldASTs,
@@ -644,8 +644,8 @@ function completeValueCatchingError(
644644
result
645645
);
646646
if (isThenable(completed)) {
647-
// If `completeValue` returned a rejected promise, log the rejection
648-
// error and resolve to null.
647+
// If `completeValueWithLocatedError` returned a rejected promise, log
648+
// the rejection error and resolve to null.
649649
// Note: we don't rely on a `catch` method, but we do expect "thenable"
650650
// to take a second callback for the error case.
651651
return ((completed: any): Promise<*>).then(undefined, error => {
@@ -655,13 +655,43 @@ function completeValueCatchingError(
655655
}
656656
return completed;
657657
} catch (error) {
658-
// If `completeValue` returned abruptly (threw an error), log the error
659-
// and return null.
658+
// If `completeValueWithLocatedError` returned abruptly (threw an error),
659+
// log the error and return null.
660660
exeContext.errors.push(error);
661661
return null;
662662
}
663663
}
664664

665+
// This is a small wrapper around completeValue which annotates errors with
666+
// location information.
667+
function completeValueWithLocatedError(
668+
exeContext: ExecutionContext,
669+
returnType: GraphQLType,
670+
fieldASTs: Array<Field>,
671+
info: GraphQLResolveInfo,
672+
path: Array<string | number>,
673+
result: mixed
674+
): mixed {
675+
try {
676+
const completed = completeValue(
677+
exeContext,
678+
returnType,
679+
fieldASTs,
680+
info,
681+
path,
682+
result
683+
);
684+
if (isThenable(completed)) {
685+
return ((completed: any): Promise<*>).catch(
686+
error => Promise.reject(locatedError(error, fieldASTs, path))
687+
);
688+
}
689+
return completed;
690+
} catch (error) {
691+
throw locatedError(error, fieldASTs, path);
692+
}
693+
}
694+
665695
/**
666696
* Implements the instructions for completeValue as defined in the
667697
* "Field entries" section of the spec.
@@ -694,23 +724,20 @@ function completeValue(
694724
// If result is a Promise, apply-lift over completeValue.
695725
if (isThenable(result)) {
696726
return ((result: any): Promise<*>).then(
697-
// Once resolved to a value, complete that value.
698727
resolved => completeValue(
699728
exeContext,
700729
returnType,
701730
fieldASTs,
702731
info,
703732
path,
704733
resolved
705-
),
706-
// If rejected, create a located error, and continue to reject.
707-
error => Promise.reject(locatedError(error, fieldASTs, path))
734+
)
708735
);
709736
}
710737

711738
// If result is an Error, throw a located error.
712739
if (result instanceof Error) {
713-
throw locatedError(result, fieldASTs, path);
740+
throw result;
714741
}
715742

716743
// If field type is NonNull, complete for inner type, and throw field error
@@ -725,10 +752,9 @@ function completeValue(
725752
result
726753
);
727754
if (completed === null) {
728-
throw new GraphQLError(
755+
throw new Error(
729756
`Cannot return null for non-nullable field ${
730-
info.parentType.name}.${info.fieldName}.`,
731-
fieldASTs
757+
info.parentType.name}.${info.fieldName}.`
732758
);
733759
}
734760
return completed;
@@ -785,8 +811,7 @@ function completeValue(
785811
}
786812

787813
// Not reachable. All possible output types have been considered.
788-
invariant(
789-
false,
814+
throw new Error(
790815
`Cannot complete value of unexpected type "${String(returnType)}".`
791816
);
792817
}
@@ -845,7 +870,13 @@ function completeLeafValue(
845870
): mixed {
846871
invariant(returnType.serialize, 'Missing serialize method on type');
847872
const serializedResult = returnType.serialize(result);
848-
return isNullish(serializedResult) ? null : serializedResult;
873+
if (isNullish(serializedResult)) {
874+
throw new Error(
875+
`Expected a value of type "${String(returnType)}" but ` +
876+
`received: ${String(result)}`
877+
);
878+
}
879+
return serializedResult;
849880
}
850881

851882
/**

src/type/__tests__/enumType-test.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,14 @@ describe('Type System: Enum Values', () => {
174174
it('does not accept incorrect internal value', async () => {
175175
expect(
176176
await graphql(schema, '{ colorEnum(fromString: "GREEN") }')
177-
).to.jsonEqual({
177+
).to.containSubset({
178178
data: {
179179
colorEnum: null
180-
}
180+
},
181+
errors: [
182+
{ message: 'Expected a value of type "Color" but received: GREEN',
183+
locations: [ { line: 1, column: 3 } ] }
184+
]
181185
});
182186
});
183187

@@ -367,13 +371,18 @@ describe('Type System: Enum Values', () => {
367371
good: complexEnum(provideGoodValue: true)
368372
bad: complexEnum(provideBadValue: true)
369373
}`)
370-
).to.jsonEqual({
374+
).to.containSubset({
371375
data: {
372376
first: 'ONE',
373377
second: 'TWO',
374378
good: 'TWO',
375379
bad: null
376-
}
380+
},
381+
errors: [ {
382+
message:
383+
'Expected a value of type "Complex" but received: [object Object]',
384+
locations: [ { line: 5, column: 9 } ]
385+
} ]
377386
});
378387
});
379388

0 commit comments

Comments
 (0)