-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Bug 3276 - default values not being used correctly during validation #3278
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
This condition is different for SDL-based vs programmatic: if (schemaDefault.isPresent() && schemaDefault.get().isLiteral()) { //true for SDL
schemaDefaultValue = (Value) schemaDefault.get().getValue();
} else if (schemaDefault.isPresent() && schemaDefault.get().isSet()) { //valueToLiteral explodes for programmatic
schemaDefaultValue = ValuesResolver.valueToLiteral(schemaDefault.get(), expectedType, getValidationContext().getGraphQLContext(), getValidationContext().getI18n().getLocale());
}The interesting part is that an object But the root cause in all cases is that the query tree isn't properly traversed apparently, and the argument and its default value are in the context when a variable is being validated. Seems like the traverser doesn't descent deeper into the input object itself in this case. |
Actually I suspect i was a copy and paste bug. When we call |
|
Bah, I hate to be harping on this but... this method seems to be using the default value of the whole input object while claiming to be using the default value of the input object field where the variable is used instead: // variableType is Int, variableDefaultValue is object??
public boolean doesVariableTypesMatch(GraphQLType variableType, Value variableDefaultValue, GraphQLType expectedType) {
return checkType(effectiveType(variableType, variableDefaultValue), expectedType);
}Is that... surely ok? It feels like it's comparing apples to oranges 😐 |
|
E.g. if I were to add the default values to type Query {
items(pagination: Pagination = {limit: 10, offset: 0}): [String]
}
input Pagination {
limit: Int = 10 #now has a default
offset: Int = 0
}and query: query Items( $limit: Int, $offset: Int) {
items(
pagination: {limit: $limit, offset: $offset}
)
}shouldn't Sorry if I'm confused and just pestering you at this point 😳 |
|
I think I managed to break the nullness deduction (exploiting the thing I explained above): Schema: type Query {
items(pagination: Pagination = {limit: 1, offset: 1}): [String]
}
input Pagination {
limit: Int! #non-null this time, no default value
offset: Int! #non-null this time, no default value
}Query (same as before): query Items( $limit: Int, $offset: Int) {
items(
pagination: {limit: $limit, offset: $offset}
)
}Variables: Map<String, Object> vars = new HashMap<>();
vars.put("limit", 5);
vars.put("offset", null); //explicit null for a non-null input fieldThis validates (and should't)! Calling
|
|
I will look into this new bug condition when I can... Seem 309fb9e1747f05ce0f3c52c6aa92570a11144dab is when this code was last changed. It was a bug fix around nullness I think we need to pull on that string to see its correct or not - it feels wrong based on the above. I have created a repro and it does blow up as stated |
|
So having looked into this more it seems that the fix created to adress #2573 is not correct enough,. As @kaqqao has said it does not cover cases where the variable reference is down in part of the input type. It only covers the case where a $variable reference is the whole input type. I think this fix needs to be reconsidered from first principles. eg a variable reference can point to part of an arguments structure and the null / non null overrride rules from the spec need to somehow descend into the structure OR be checked in some other manner |
|
I am reading the spec here now
One line of thought is that we don't have to check that innards of the default value are ok but rather that the argument has a default value at all. HOWEVER - I took the graphql-js reference implementation and added a new test to it using our examples in harness.ts and and this shows the query as invalid. So I think we need to go back and deeply read the spec here because my initial thought is that the defaulting on the argument above would allow it to proceed BUT the reference implementation does not allow it Are we reading the spec wrong OR has the reference implementation got the same bug as we do? Hmmm more investigation needed |
|
FWIW, graphql-js behavior looks unambiguously correct to me, spec-wise.
graphql-java's behavior is very different. It instead validated the query, because it looked at the default value of
True, but it's the location where the variable is used that matters, and the locations in question (
Not at query execution time, no. I reckon that's already done once during schema-building, and that's all that's needed. The spec text above deals with query execution time only. |
Yes I have been rereading the spec and the reference implementation and I think I have come to the same conclusion. The example IS invalid based on spec even if it could be argued that code COULD somehow work out how to default it (eg some how look upwards to the argument defaults and smoosh the values in). We do indeed still have a bug in graphql-java that allows this to be considered valid and then fail later BUT I was trying to work out the desired validation behavior. |
|
@kaqqao - I have implemented a fix for this problem. I went back to the graphql-js implementation for inspiration and to check expected behavior If you can please have a look this will help us. |
I think this is an improvement we could add - eg at schema build time we validate that the default values are in fact valid for that location |
| private final List<GraphQLOutputType> outputTypeStack = new ArrayList<>(); | ||
| private final List<GraphQLCompositeType> parentTypeStack = new ArrayList<>(); | ||
| private final List<GraphQLInputType> inputTypeStack = new ArrayList<>(); | ||
| private final List<InputValueWithState> defaultValueStack = new ArrayList<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
inspiration taken from graphql-js - keep a stack of default values as we traverse the AST
the older existing code was doing exactly this
| } | ||
| addInputType(inputType); | ||
| // List positions never have a default value. See graphql-js impl for inspiration | ||
| addDefaultValue(null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did this because graphql-js did this
| if (argument.getName().equals(name)) { | ||
| return argument; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
auto format puts in braces (as it should)
|
|
||
| private <T> T pop(List<T> list) { | ||
| return list.remove(list.size() - 1); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
neater
| for (Definition<?> definition : document.getDefinitions()) { | ||
| if (!(definition instanceof FragmentDefinition)) { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
auto format
| GraphQLTypeUtil.simplePrint(locationType)); | ||
| addError(VariableTypeMismatch, variableReference.getSourceLocation(), message); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The twitter folks dropped of a fix like
if (!variablesTypesMatcher.doesVariableTypesMatch(variableType, variableDefinition.getDefaultValue(), expectedType) &&
!variablesTypesMatcher.doesVariableTypesMatch(variableType, schemaDefaultValue, expectedType)) {
but this was kinda wrong. Its not the argument default value but the location default value
I went back to the reference implementation for inspiration and this is MUCH more like that it does
| GraphQLType nullableLocationType = unwrapNonNull(locationType); | ||
| return checkType(varType, nullableLocationType); | ||
| } | ||
| return checkType(varType, locationType); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if you looking in the graphql-js reference implementation - the logic is almost exactly the same
| er.errors[0].message == "Validation error (VariableTypeMismatch@[items]) : Variable type 'Int' does not match expected type 'Int!'" | ||
| er.errors[1].message == "Validation error (VariableTypeMismatch@[items]) : Variable type 'Int' does not match expected type 'Int!'" | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These cases are covered in unit tests BUT I wanted to have an end to end test to ensure we didnt miss something
| outputTypeStack.remove(outputTypeStack.size() - 1); | ||
| pop(outputTypeStack); | ||
| } else if (node instanceof VariableDefinition) { | ||
| inputTypeStack.remove(inputTypeStack.size() - 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| inputTypeStack.remove(inputTypeStack.size() - 1); | |
| pop(inputTypeStack); |
I just assmed this was a refactoring miss.
Should it also pop the defaultValueStack as the other cases bellow?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes changed
|
Hi! So the commit @bbakerman linked, which was merged Dec 27, 2021, definitely sounds like it could have introduced this issue. I assume it was released with the v18.0 release Mar 15, 2022. |
This now looks right to my eyes, and I confirm that I can no longer break it 😄 Thanks a bunch for this! |
| # | ||
| VariablesAreInputTypes.wrongType=Validierungsfehler ({0}) : Eingabevariable ''{1}'' Typ ''{2}'' ist kein Eingabetyp | ||
| # | ||
| VariableTypesMatchRule.unexpectedType=Validierungsfehler ({0}) : Der Variablentyp ''{1}'' stimmt nicht mit dem erwarteten Typ ''{2}'' überein |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI: GitHub's diff viewer can't handle non-ASCII characters. It was a lower case u with umlaut
…ult-value 20.x backport of #3278 default value bugfix
…ult-value 19.x backport of #3278 default value bugfix
Bug 3276 - default values not being used correctly during validation (cherry picked from commit ba589a8)
…ult-value 21.x cherry pick of #3278 default value bugfix
I created two reproduction test methods
The first is in Groovy with SDL and it works as expected
The second is in Grrovy / Java with the repro code as provided. It does indeed throw an assertion error
I havent worked out why the first works while the second doesnt yet. but here is the repro
#3276