Skip to content

Feature request: Resilient Parsing #2520

@BenSchwab

Description

@BenSchwab

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions