-
Notifications
You must be signed in to change notification settings - Fork 683
Description
Feature Request: Resilient Parsing
The graphql spec introduces the concept of error bubbling, where partial errors are allowed by bubbling up to the first element that is nullable.
Protect against backwards incompatible changes
If a server changes a field from being non-null to nullable existing clients that have already been shipped would completely break, even if the client appropriately handles nullability at an object higher up in the response.
Implementation
When parsing object types catch errors and replace the parsed value with null. This achieves a bubble effect where the error will propagate up the parse chain until we hit a nullable object.
When parsing lists a null value is read, if the list is of non-null types the entire list should resolve to null and bubble up the error.
override fun <T : Any> readObject(field: ResponseField, objectReader: ResponseReader.ObjectReader<T>): T? {
if (shouldSkip(field)) {
return null
}
val value: R? = fieldValueResolver.valueFor(recordSet, field)
checkValue(field, value)
willResolve(field, value)
resolveDelegate.willResolveObject(field, value)
val parsedValue: T?
parsedValue = if (value == null) {
resolveDelegate.didResolveNull()
null
} else {
// The read is wrapped in a try catch, and we try to substitute null.
// If the field is non-null, it will just throw a new error when checkValue
// is called
* **try {
objectReader.read(RealResponseReader(operationVariables, errorCollector, value, fieldValueResolver, scalarTypeAdapters, resolveDelegate))
} catch (exception: Exception) {
errorCollector.errors += exception
null
}*
}
resolveDelegate.didResolveObject(field, value)
didResolve(field)
return parsedValue
}
We could consider doing the same thing for custom scalar types, which would allow faulty custom scalar logic to resolve to null for a nullable custom scalar type.
Errors are collected in a partial error response object on the root operation, next to the standard graphql errors:
data class Response<T>(
val operation: Operation<*, *, *>,
val data: T?,
val errors: List<Error>? = null,
// A new field containing errors that occurred during parsing, but may be recoverable.
val parseErrors: List<ParseError>? = null
The ParseError object should form a chain of failures, if the error included multiple bubbled up response. Like the standard Error, this ParseError should include in a location in the query where the failure occurred.
In addition to surfacing the errors at the RootResponse we should be able opt-into generating an errors property on any object element in the response via a generateErrorAccessor directive. This seems possible by having the ErrorCollector track an “ongoing” error object, that “resolves” when the response reader is able to set a value to null.
Examples:
Simple:
Data? -------------
scalar1
object1? -------
scalar2 <--- parsing error here
-
object1 resolves to null
-
error present in data
Object bubble:
Data? -------------
scalar1
object1? -------
object2 --------
scalar2 <---parsing error here
* object 2 resolves to null causing error to bubble to object 1 which successfully resolves to null
* error present in data
Non-null List:
Data? -------------
scalar1
object1? -------
scalar2
list<T>? ------
object2 <------ parsing error here
object3
List resolves to null
Nullable list
Data? -------------
scalar1
object1? -------
scalar2
list<T?>? ------
object2 <------ parsing error here
object3
List contains null element for object 2
Other uses: Allow client “required” directive
An often requested client directive is the ability to mark a field on the client as “required” transforming it from nullable to non-null. However, as in the backwards incompatible case, if this requirement fails, it would be ideal to allow to response to still partially succeed.