Skip to content

Commit 03d8d2a

Browse files
committed
SPEC/BUG: Ambiguity with null variable values and default values
This change corresponds to a spec proposal which solves an ambiguity in how variable values and default values behave with explicit null values. Otherwise, this ambiguity allows for null values to appear in non-null argument values, which may result in unforseen null-pointer-errors. This appears in three distinct but related issues: **VariablesInAllowedPosition validation rule** The explicit value `null` may be used as a default value for a variable, however `VariablesInAllowedPositions` allowed a nullable type with a default value to flow into an argument expecting a non-null type. This validation rule must explicitly not allow `null` default values to flow in this manner. **Coercing Variable Values** coerceVariableValues allows the explicit `null` value to be used over a default value, which can result in flowing a null value to a non-null argument when paired with the validation rule mentioned above. Instead a default value must be used even when an explicit `null` value is provided. **Coercing Argument Values** coerceArgumentValues allows the explicit `null` default value to be used before checking for a non-null type. This could inadvertently allow a null value into a non-null typed argument.
1 parent 8913bf0 commit 03d8d2a

File tree

5 files changed

+359
-27
lines changed

5 files changed

+359
-27
lines changed

src/execution/__tests__/nonnull-test.js

+188
Original file line numberDiff line numberDiff line change
@@ -486,4 +486,192 @@ describe('Execute: handles non-nullable types', () => {
486486
],
487487
},
488488
);
489+
490+
describe('Handles non-null argument', () => {
491+
const schemaWithNonNullArg = new GraphQLSchema({
492+
query: new GraphQLObjectType({
493+
name: 'Query',
494+
fields: {
495+
withNonNullArg: {
496+
type: GraphQLString,
497+
args: {
498+
cannotBeNull: {
499+
type: GraphQLNonNull(GraphQLString),
500+
defaultValue: null,
501+
},
502+
},
503+
resolve: async (_, args) => {
504+
if (typeof args.cannotBeNull === 'string') {
505+
return 'Passed: ' + args.cannotBeNull;
506+
}
507+
},
508+
},
509+
},
510+
}),
511+
});
512+
513+
it('succeeds when passed non-null literal value', async () => {
514+
const result = await execute({
515+
schema: schemaWithNonNullArg,
516+
document: parse(`
517+
query {
518+
withNonNullArg (cannotBeNull: "literal value")
519+
}
520+
`),
521+
});
522+
523+
expect(result).to.deep.equal({
524+
data: {
525+
withNonNullArg: 'Passed: literal value',
526+
},
527+
});
528+
});
529+
530+
it('succeeds when passed non-null variable value', async () => {
531+
const result = await execute({
532+
schema: schemaWithNonNullArg,
533+
document: parse(`
534+
query ($testVar: String!) {
535+
withNonNullArg (cannotBeNull: $testVar)
536+
}
537+
`),
538+
variableValues: {
539+
testVar: 'variable value',
540+
},
541+
});
542+
543+
expect(result).to.deep.equal({
544+
data: {
545+
withNonNullArg: 'Passed: variable value',
546+
},
547+
});
548+
});
549+
550+
it('succeeds when missing variable has default value', async () => {
551+
const result = await execute({
552+
schema: schemaWithNonNullArg,
553+
document: parse(`
554+
query ($testVar: String = "default value") {
555+
withNonNullArg (cannotBeNull: $testVar)
556+
}
557+
`),
558+
variableValues: {
559+
// Intentionally missing variable
560+
},
561+
});
562+
563+
expect(result).to.deep.equal({
564+
data: {
565+
withNonNullArg: 'Passed: default value',
566+
},
567+
});
568+
});
569+
570+
it('succeeds when null variable has default value', async () => {
571+
const result = await execute({
572+
schema: schemaWithNonNullArg,
573+
document: parse(`
574+
query ($testVar: String = "default value") {
575+
withNonNullArg (cannotBeNull: $testVar)
576+
}
577+
`),
578+
variableValues: {
579+
testVar: null,
580+
},
581+
});
582+
583+
expect(result).to.deep.equal({
584+
data: {
585+
withNonNullArg: 'Passed: default value',
586+
},
587+
});
588+
});
589+
590+
it('field error when missing non-null arg', async () => {
591+
// Note: validation should identify this issue first (missing args rule)
592+
// however execution should still protect against this.
593+
const result = await execute({
594+
schema: schemaWithNonNullArg,
595+
document: parse(`
596+
query {
597+
withNonNullArg
598+
}
599+
`),
600+
});
601+
602+
expect(result).to.deep.equal({
603+
data: {
604+
withNonNullArg: null,
605+
},
606+
errors: [
607+
{
608+
message:
609+
'Argument "cannotBeNull" of required type "String!" was not provided.',
610+
locations: [{ line: 3, column: 13 }],
611+
path: ['withNonNullArg'],
612+
},
613+
],
614+
});
615+
});
616+
617+
it('field error when non-null arg provided null', async () => {
618+
// Note: validation should identify this issue first (values of correct
619+
// type rule) however execution should still protect against this.
620+
const result = await execute({
621+
schema: schemaWithNonNullArg,
622+
document: parse(`
623+
query {
624+
withNonNullArg(cannotBeNull: null)
625+
}
626+
`),
627+
});
628+
629+
expect(result).to.deep.equal({
630+
data: {
631+
withNonNullArg: null,
632+
},
633+
errors: [
634+
{
635+
message:
636+
'Argument "cannotBeNull" of non-null type "String!" must ' +
637+
'not be null.',
638+
locations: [{ line: 3, column: 42 }],
639+
path: ['withNonNullArg'],
640+
},
641+
],
642+
});
643+
});
644+
645+
it('field error when non-null arg not provided variable value', async () => {
646+
// Note: validation should identify this issue first (variables in allowed
647+
// position rule) however execution should still protect against this.
648+
const result = await execute({
649+
schema: schemaWithNonNullArg,
650+
document: parse(`
651+
query ($testVar: String) {
652+
withNonNullArg(cannotBeNull: $testVar)
653+
}
654+
`),
655+
variableValues: {
656+
// Intentionally missing variable
657+
},
658+
});
659+
660+
expect(result).to.deep.equal({
661+
data: {
662+
withNonNullArg: null,
663+
},
664+
errors: [
665+
{
666+
message:
667+
'Argument "cannotBeNull" of required type "String!" was ' +
668+
'provided the variable "$testVar" which was not provided a ' +
669+
'runtime value.',
670+
locations: [{ line: 3, column: 42 }],
671+
path: ['withNonNullArg'],
672+
},
673+
],
674+
});
675+
});
676+
});
489677
});

src/execution/__tests__/variables-test.js

+100-6
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ const TestType = new GraphQLObjectType({
8282
args: { input: { type: GraphQLString, defaultValue: 'Hello World' } },
8383
resolve: (_, { input }) => input && JSON.stringify(input),
8484
},
85+
fieldWithNonNullableStringInputAndDefaultArgumentValue: {
86+
type: GraphQLString,
87+
args: {
88+
input: {
89+
type: GraphQLNonNull(GraphQLString),
90+
defaultValue: 'Unreachable',
91+
},
92+
},
93+
resolve: (_, { input }) => input && JSON.stringify(input),
94+
},
8595
fieldWithNestedInputObject: {
8696
type: GraphQLString,
8797
args: {
@@ -264,6 +274,72 @@ describe('Execute: Handles inputs', () => {
264274
});
265275
});
266276

277+
it('does not use default value when provided', async () => {
278+
const withDefaultsAST = parse(`
279+
query q($input: String = "Default value") {
280+
fieldWithNullableStringInput(input: $input)
281+
}
282+
`);
283+
284+
const result = await execute({
285+
schema,
286+
document: withDefaultsAST,
287+
variableValues: {
288+
input: 'Variable value',
289+
},
290+
});
291+
292+
expect(result).to.deep.equal({
293+
data: {
294+
fieldWithNullableStringInput: '"Variable value"',
295+
},
296+
});
297+
});
298+
299+
it('uses default value when explicit null value provided', async () => {
300+
const withDefaultsAST = parse(`
301+
query q($input: String = "Default value") {
302+
fieldWithNullableStringInput(input: $input)
303+
}
304+
`);
305+
306+
const result = await execute({
307+
schema,
308+
document: withDefaultsAST,
309+
variableValues: {
310+
input: null,
311+
},
312+
});
313+
314+
expect(result).to.deep.equal({
315+
data: {
316+
fieldWithNullableStringInput: '"Default value"',
317+
},
318+
});
319+
});
320+
321+
it('uses null default value when not provided', async () => {
322+
const withDefaultsAST = parse(`
323+
query q($input: String = null) {
324+
fieldWithNullableStringInput(input: $input)
325+
}
326+
`);
327+
328+
const result = await execute({
329+
schema,
330+
document: withDefaultsAST,
331+
variableValues: {
332+
// Intentionally missing value.
333+
},
334+
});
335+
336+
expect(result).to.deep.equal({
337+
data: {
338+
fieldWithNullableStringInput: null,
339+
},
340+
});
341+
});
342+
267343
it('properly parses single value to list', async () => {
268344
const params = { input: { a: 'foo', b: 'bar', c: 'baz' } };
269345
const result = await execute(schema, ast, null, null, params);
@@ -534,8 +610,7 @@ describe('Execute: Handles inputs', () => {
534610
errors: [
535611
{
536612
message:
537-
'Variable "$value" got invalid value null; ' +
538-
'Expected non-nullable type String! not to be null.',
613+
'Variable "$value" of required type "String!" was not provided.',
539614
locations: [{ line: 2, column: 31 }],
540615
path: undefined,
541616
},
@@ -733,8 +808,7 @@ describe('Execute: Handles inputs', () => {
733808
errors: [
734809
{
735810
message:
736-
'Variable "$input" got invalid value null; ' +
737-
'Expected non-nullable type [String]! not to be null.',
811+
'Variable "$input" of required type "[String]!" was not provided.',
738812
locations: [{ line: 2, column: 17 }],
739813
path: undefined,
740814
},
@@ -846,8 +920,7 @@ describe('Execute: Handles inputs', () => {
846920
errors: [
847921
{
848922
message:
849-
'Variable "$input" got invalid value null; ' +
850-
'Expected non-nullable type [String!]! not to be null.',
923+
'Variable "$input" of required type "[String!]!" was not provided.',
851924
locations: [{ line: 2, column: 17 }],
852925
path: undefined,
853926
},
@@ -985,5 +1058,26 @@ describe('Execute: Handles inputs', () => {
9851058
],
9861059
});
9871060
});
1061+
1062+
it('not when argument type is non-null', async () => {
1063+
const ast = parse(`query optionalVariable($optional: String) {
1064+
fieldWithNonNullableStringInputAndDefaultArgumentValue(input: $optional)
1065+
}`);
1066+
1067+
expect(await execute(schema, ast)).to.deep.equal({
1068+
data: {
1069+
fieldWithNonNullableStringInputAndDefaultArgumentValue: null,
1070+
},
1071+
errors: [
1072+
{
1073+
message:
1074+
'Argument "input" of required type "String!" was provided the ' +
1075+
'variable "$optional" which was not provided a runtime value.',
1076+
locations: [{ line: 2, column: 71 }],
1077+
path: ['fieldWithNonNullableStringInputAndDefaultArgumentValue'],
1078+
},
1079+
],
1080+
});
1081+
});
9881082
});
9891083
});

0 commit comments

Comments
 (0)