Skip to content

Commit a78e156

Browse files
committed
Fix unhandled error when parsing custom scalar literals.
Fixes #910 This factors out the enum value validation from scalar value validation and ensures the same try/catch is used in isValidLiteralValue as isValidJSValue and protecting from errors in valueFromAST.
1 parent 007407d commit a78e156

File tree

8 files changed

+101
-52
lines changed

8 files changed

+101
-52
lines changed

src/execution/__tests__/variables-test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -613,7 +613,7 @@ describe('Execute: Handles inputs', () => {
613613
{
614614
message:
615615
'Variable "$value" got invalid value [1,2,3].\nExpected type ' +
616-
'"String", found [1,2,3]: String cannot represent an array value: [1,2,3]',
616+
'"String", found [1,2,3]; String cannot represent an array value: [1,2,3]',
617617
locations: [{ line: 2, column: 31 }],
618618
path: undefined,
619619
},

src/subscription/__tests__/subscribe-test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ describe('Subscription Initialization Phase', () => {
475475
{
476476
message:
477477
'Variable "$priority" got invalid value "meow".\nExpected ' +
478-
'type "Int", found "meow": Int cannot represent non 32-bit signed ' +
478+
'type "Int", found "meow"; Int cannot represent non 32-bit signed ' +
479479
'integer value: meow',
480480
locations: [{ line: 2, column: 21 }],
481481
path: undefined,

src/type/definition.js

-24
Original file line numberDiff line numberDiff line change
@@ -332,11 +332,6 @@ export class GraphQLScalarType {
332332
return serializer(value);
333333
}
334334

335-
// Determines if an internal value is valid for this type.
336-
isValidValue(value: mixed): boolean {
337-
return !isInvalid(this.parseValue(value));
338-
}
339-
340335
// Parses an externally provided value to use as an input.
341336
parseValue(value: mixed): mixed {
342337
const parser = this._scalarConfig.parseValue;
@@ -346,11 +341,6 @@ export class GraphQLScalarType {
346341
return parser ? parser(value) : value;
347342
}
348343

349-
// Determines if an internal value is valid for this type.
350-
isValidLiteral(valueNode: ValueNode, variables: ?ObjMap<mixed>): boolean {
351-
return !isInvalid(this.parseLiteral(valueNode, variables));
352-
}
353-
354344
// Parses an externally provided literal value to use as an input.
355345
parseLiteral(valueNode: ValueNode, variables: ?ObjMap<mixed>): mixed {
356346
const parser = this._scalarConfig.parseLiteral;
@@ -919,12 +909,6 @@ export class GraphQLEnumType /* <T> */ {
919909
}
920910
}
921911

922-
isValidValue(value: mixed): boolean {
923-
return (
924-
typeof value === 'string' && this._getNameLookup()[value] !== undefined
925-
);
926-
}
927-
928912
parseValue(value: mixed): ?any /* T */ {
929913
if (typeof value === 'string') {
930914
const enumValue = this._getNameLookup()[value];
@@ -934,14 +918,6 @@ export class GraphQLEnumType /* <T> */ {
934918
}
935919
}
936920

937-
isValidLiteral(valueNode: ValueNode, _variables: ?ObjMap<mixed>): boolean {
938-
// Note: variables will be resolved to a value before calling this function.
939-
return (
940-
valueNode.kind === Kind.ENUM &&
941-
this._getNameLookup()[valueNode.value] !== undefined
942-
);
943-
}
944-
945921
parseLiteral(valueNode: ValueNode, _variables: ?ObjMap<mixed>): ?any /* T */ {
946922
// Note: variables will be resolved to a value before calling this function.
947923
if (valueNode.kind === Kind.ENUM) {

src/utilities/isValidJSValue.js

+17-11
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import { forEach, isCollection } from 'iterall';
1111

1212
import invariant from '../jsutils/invariant';
13+
import isInvalid from '../jsutils/isInvalid';
1314
import isNullish from '../jsutils/isNullish';
1415
import {
1516
GraphQLScalarType,
@@ -89,23 +90,28 @@ export function isValidJSValue(
8990
return errors;
9091
}
9192

92-
invariant(
93-
type instanceof GraphQLScalarType || type instanceof GraphQLEnumType,
94-
'Must be input type',
95-
);
93+
// Enum types only accept certain values
94+
if (type instanceof GraphQLEnumType) {
95+
if (typeof value !== 'string' || !type.getValue(value)) {
96+
const printed = JSON.stringify(value);
97+
return [`Expected type "${type.name}", found ${printed}.`];
98+
}
99+
100+
return [];
101+
}
102+
103+
invariant(type instanceof GraphQLScalarType, 'Must be scalar type');
96104

97-
// Scalar/Enum input checks to ensure the type can parse the value to
98-
// a non-null value.
105+
// Scalars determine if a value is valid via parseValue().
99106
try {
100107
const parseResult = type.parseValue(value);
101-
if (isNullish(parseResult) && !type.isValidValue(value)) {
108+
if (isInvalid(parseResult)) {
102109
return [`Expected type "${type.name}", found ${JSON.stringify(value)}.`];
103110
}
104111
} catch (error) {
105-
return [
106-
`Expected type "${type.name}", found ${JSON.stringify(value)}: ` +
107-
error.message,
108-
];
112+
const printed = JSON.stringify(value);
113+
const message = error.message;
114+
return [`Expected type "${type.name}", found ${printed}; ${message}`];
109115
}
110116

111117
return [];

src/utilities/isValidLiteralValue.js

+20-7
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
} from '../type/definition';
2424
import type { GraphQLInputType } from '../type/definition';
2525
import invariant from '../jsutils/invariant';
26+
import isInvalid from '../jsutils/isInvalid';
2627
import keyMap from '../jsutils/keyMap';
2728

2829
/**
@@ -100,14 +101,26 @@ export function isValidLiteralValue(
100101
return errors;
101102
}
102103

103-
invariant(
104-
type instanceof GraphQLScalarType || type instanceof GraphQLEnumType,
105-
'Must be input type',
106-
);
104+
if (type instanceof GraphQLEnumType) {
105+
if (valueNode.kind !== Kind.ENUM || !type.getValue(valueNode.value)) {
106+
return [`Expected type "${type.name}", found ${print(valueNode)}.`];
107+
}
108+
109+
return [];
110+
}
111+
112+
invariant(type instanceof GraphQLScalarType, 'Must be a scalar type');
107113

108-
// Scalars determine if a literal values is valid.
109-
if (!type.isValidLiteral(valueNode, null)) {
110-
return [`Expected type "${type.name}", found ${print(valueNode)}.`];
114+
// Scalars determine if a literal value is valid via parseLiteral().
115+
try {
116+
const parseResult = type.parseLiteral(valueNode, null);
117+
if (isInvalid(parseResult)) {
118+
return [`Expected type "${type.name}", found ${print(valueNode)}.`];
119+
}
120+
} catch (error) {
121+
const printed = print(valueNode);
122+
const message = error.message;
123+
return [`Expected type "${type.name}", found ${printed}; ${message}`];
111124
}
112125

113126
return [];

src/utilities/valueFromAST.js

+22-8
Original file line numberDiff line numberDiff line change
@@ -144,18 +144,32 @@ export function valueFromAST(
144144
return coercedObj;
145145
}
146146

147-
invariant(
148-
type instanceof GraphQLScalarType || type instanceof GraphQLEnumType,
149-
'Must be input type',
150-
);
151-
152-
// Scalar and Enum values implement methods which perform this translation.
153-
if (type.isValidLiteral(valueNode, variables)) {
154-
return type.parseLiteral(valueNode, variables);
147+
if (type instanceof GraphQLEnumType) {
148+
if (valueNode.kind !== Kind.ENUM) {
149+
return; // Invalid: intentionally return no value.
150+
}
151+
const enumValue = type.getValue(valueNode.value);
152+
if (!enumValue) {
153+
return; // Invalid: intentionally return no value.
154+
}
155+
return enumValue.value;
155156
}
156157

158+
invariant(type instanceof GraphQLScalarType, 'Must be scalar type');
159+
160+
// Scalars fulfill parsing a literal value via parseLiteral().
157161
// Invalid values represent a failure to parse correctly, in which case
158162
// no value is returned.
163+
let result;
164+
try {
165+
result = type.parseLiteral(valueNode, variables);
166+
} catch (_error) {
167+
return; // Invalid: intentionally return no value.
168+
}
169+
if (isInvalid(result)) {
170+
return; // Invalid: intentionally return no value.
171+
}
172+
return result;
159173
}
160174

161175
// Returns true if the provided valueNode is a variable which is not defined

src/validation/__tests__/harness.js

+20
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
GraphQLIncludeDirective,
3030
GraphQLSkipDirective,
3131
} from '../../type/directives';
32+
import { GraphQLScalarType } from '../../type/definition';
3233

3334
const Being = new GraphQLInterfaceType({
3435
name: 'Being',
@@ -269,6 +270,19 @@ const ComplicatedArgs = new GraphQLObjectType({
269270
}),
270271
});
271272

273+
const InvalidScalar = new GraphQLScalarType({
274+
name: 'Invalid',
275+
serialize(value) {
276+
return value;
277+
},
278+
parseLiteral(node) {
279+
throw new Error('Invalid scalar is always invalid: ' + node.value);
280+
},
281+
parseValue(node) {
282+
throw new Error('Invalid scalar is always invalid: ' + node);
283+
},
284+
});
285+
272286
const QueryRoot = new GraphQLObjectType({
273287
name: 'QueryRoot',
274288
fields: () => ({
@@ -284,6 +298,12 @@ const QueryRoot = new GraphQLObjectType({
284298
dogOrHuman: { type: DogOrHuman },
285299
humanOrAlien: { type: HumanOrAlien },
286300
complicatedArgs: { type: ComplicatedArgs },
301+
invalidArg: {
302+
args: {
303+
arg: { type: InvalidScalar },
304+
},
305+
type: GraphQLString,
306+
},
287307
}),
288308
});
289309

src/validation/__tests__/validation-test.js

+20
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,26 @@ describe('Validate: Supports full validation', () => {
3636
);
3737
});
3838

39+
it('detects bad scalar parse', () => {
40+
const doc = `
41+
query {
42+
invalidArg(arg: "bad value")
43+
}
44+
`;
45+
46+
const errors = validate(testSchema, parse(doc));
47+
expect(errors).to.deep.equal([
48+
{
49+
locations: [{ line: 3, column: 25 }],
50+
message:
51+
'Argument "arg" has invalid value "bad value".\n' +
52+
'Expected type "Invalid", found "bad value"; ' +
53+
'Invalid scalar is always invalid: bad value',
54+
path: undefined,
55+
},
56+
]);
57+
});
58+
3959
// NOTE: experimental
4060
it('validates using a custom TypeInfo', () => {
4161
// This TypeInfo will never return a valid field.

0 commit comments

Comments
 (0)