Skip to content

Exception thrown when interfaces is null inside an object [0.12.0] #746

@hlship

Description

@hlship

The schema introspection definitions (https://facebook.github.io/graphql/June2018/#sec-Schema-Introspection) indicate that the interfaces field of __Type is a nullable list.

  interfaces: [__Type!]

Our implementation of GraphQL (Lacinia) does, in fact, return a null for objects that do not implement an interface, rather than an empty list.

This results in an exception in the data pane at GraphiQL startup.

Error: Introspection result missing interfaces: {"kind":"OBJECT","name":"QueryRoot","description":"Root of all queries.","fields":[{"name":"hello","description":"Simplest possible query.","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null}
    at buildObjectDef (http://localhost:8888/assets/graphiql/graphiql.js:33585:13)
    at buildType (http://localhost:8888/assets/graphiql/graphiql.js:33559:18)
    at getNamedType (http://localhost:8888/assets/graphiql/graphiql.js:33524:19)
    at http://localhost:8888/assets/graphiql/graphiql.js:33703:12
    at Array.map (<anonymous>)
    at buildClientSchema (http://localhost:8888/assets/graphiql/graphiql.js:33702:41)
    at http://localhost:8888/assets/graphiql/graphiql.js:2174:55

even though the GraphQL introspection query (here formatted for readability) matches, AFAIK, the introspection schema:

{
    "data": {
        "__schema": {
            "queryType": {
                "name": "QueryRoot"
            },
            "mutationType": null,
            "subscriptionType": {
                "name": "SubscriptionRoot"
            },
            "types": [
                {
                    "kind": "SCALAR",
                    "name": "Boolean",
                    "description": null,
                    "fields": null,
                    "inputFields": null,
                    "interfaces": null,
                    "enumValues": null,
                    "possibleTypes": null
                },
                {
                    "kind": "SCALAR",
                    "name": "Float",
                    "description": null,
                    "fields": null,
                    "inputFields": null,
                    "interfaces": null,
                    "enumValues": null,
                    "possibleTypes": null
                },
                {
                    "kind": "SCALAR",
                    "name": "ID",
                    "description": null,
                    "fields": null,
                    "inputFields": null,
                    "interfaces": null,
                    "enumValues": null,
                    "possibleTypes": null
                },
                {
                    "kind": "SCALAR",
                    "name": "Int",
                    "description": null,
                    "fields": null,
                    "inputFields": null,
                    "interfaces": null,
                    "enumValues": null,
                    "possibleTypes": null
                },
                {
                    "kind": "OBJECT",
                    "name": "QueryRoot",
                    "description": "Root of all queries.",
                    "fields": [
                        {
                            "name": "hello",
                            "description": "Simplest possible query.",
                            "args": [],
                            "type": {
                                "kind": "SCALAR",
                                "name": "String",
                                "ofType": null
                            },
                            "isDeprecated": false,
                            "deprecationReason": null
                        }
                    ],
                    "inputFields": null,
                    "interfaces": null,
                    "enumValues": null,
                    "possibleTypes": null
                },
                {
                    "kind": "SCALAR",
                    "name": "String",
                    "description": null,
                    "fields": null,
                    "inputFields": null,
                    "interfaces": null,
                    "enumValues": null,
                    "possibleTypes": null
                },
                {
                    "kind": "OBJECT",
                    "name": "SubscriptionRoot",
                    "description": "Root of all subscriptions.",
                    "fields": [
                        {
                            "name": "ticks",
                            "description": null,
                            "args": [
                                {
                                    "name": "count",
                                    "description": "Number of ticks to send via subscription.",
                                    "type": {
                                        "kind": "SCALAR",
                                        "name": "Int",
                                        "ofType": null
                                    },
                                    "defaultValue": "5"
                                }
                            ],
                            "type": {
                                "kind": "OBJECT",
                                "name": "tick",
                                "ofType": null
                            },
                            "isDeprecated": false,
                            "deprecationReason": null
                        }
                    ],
                    "inputFields": null,
                    "interfaces": null,
                    "enumValues": null,
                    "possibleTypes": null
                },
                {
                    "kind": "OBJECT",
                    "name": "tick",
                    "description": "A subscription response.",
                    "fields": [
                        {
                            "name": "count",
                            "description": null,
                            "args": [],
                            "type": {
                                "kind": "SCALAR",
                                "name": "Int",
                                "ofType": null
                            },
                            "isDeprecated": false,
                            "deprecationReason": null
                        },
                        {
                            "name": "time_ms",
                            "description": "Time when tick is emittied (as string-ified long milliseconds since epoch).",
                            "args": [],
                            "type": {
                                "kind": "SCALAR",
                                "name": "String",
                                "ofType": null
                            },
                            "isDeprecated": false,
                            "deprecationReason": null
                        }
                    ],
                    "inputFields": null,
                    "interfaces": null,
                    "enumValues": null,
                    "possibleTypes": null
                }
            ],
            "directives": [
                {
                    "name": "skip",
                    "description": "Skip the selection only when the `if` argument is true.",
                    "locations": [
                        "INLINE_FRAGMENT",
                        "FIELD",
                        "FRAGMENT_SPREAD"
                    ],
                    "args": [
                        {
                            "name": "if",
                            "description": "Triggering argument for skip directive.",
                            "type": {
                                "kind": "NON_NULL",
                                "name": null,
                                "ofType": {
                                    "kind": "SCALAR",
                                    "name": "Boolean",
                                    "ofType": null
                                }
                            },
                            "defaultValue": null
                        }
                    ]
                },
                {
                    "name": "include",
                    "description": "Include the selection only when the `if` argument is true.",
                    "locations": [
                        "INLINE_FRAGMENT",
                        "FIELD",
                        "FRAGMENT_SPREAD"
                    ],
                    "args": [
                        {
                            "name": "if",
                            "description": "Triggering argument for include directive.",
                            "type": {
                                "kind": "NON_NULL",
                                "name": null,
                                "ofType": {
                                    "kind": "SCALAR",
                                    "name": "Boolean",
                                    "ofType": null
                                }
                            },
                            "defaultValue": null
                        }
                    ]
                }
            ]
        }
    }
}

You can see here in the code (buildClientSchema.js.flow, line 213):

  function buildObjectDef(
    objectIntrospection: IntrospectionObjectType,
  ): GraphQLObjectType {
    if (!objectIntrospection.interfaces) {
      throw new Error(
        'Introspection result missing interfaces: ' +
          JSON.stringify(objectIntrospection),
      );
    }
    return new GraphQLObjectType({
      name: objectIntrospection.name,
      description: objectIntrospection.description,
      interfaces: objectIntrospection.interfaces.map(getInterfaceType),
      fields: () => buildFieldDefMap(objectIntrospection),
    });
  }

... there's a check that interfaces are returned ... which is IMO not valid, as interfaces is nullable and a null was returned.

A quick scan of the source code shows that this may apply to possibleTypes, enumValues, inputFields, fields (in __Type) and __Field.args.

By contrast, __Directive.args, which is obviously more recent, was added to the spec as args: [__InputValue!]!.

I'm looking into a workaround where we define these fields as non-null, to force an empty list to be emitted into the response; still either the spec should be updated, or GraphiQL should be updated to match the current spec.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions