Skip to content
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

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

Closed
hlship opened this issue Nov 20, 2018 · 7 comments
Closed

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

hlship opened this issue Nov 20, 2018 · 7 comments

Comments

@hlship
Copy link

hlship commented Nov 20, 2018

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.

@IvanGoncharov
Copy link
Member

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

@hlship Right, but the spec has additional constraints based on the value of kind.

interfaces: The set of interfaces that an object implements.

https://facebook.github.io/graphql/June2018/#sec-Object
For all other values of kind it should be null:

All other fields must return null.

https://facebook.github.io/graphql/June2018/#sec-Scalar

That's is why according to the spec interfaces is nullable but should be a list when kind === 'OBJECT' and null in all other cases.

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

Because __Directive doesn't have kind field.

@IvanGoncharov
Copy link
Member

@hlship BTW. same principle applies to fields, possibleTypes, enumValues, inputFields.

@hlship
Copy link
Author

hlship commented Nov 22, 2018

Interesting; that's easy to miss, as I would have thought that the schema for __Type would have capture it, but in this case, a careful reading of the prose overrides.

Cool enough. I can update Lacinia to match that, I've already done interfaces, but I'll expand to the other mentioned fields.

@hlship hlship closed this as completed Nov 22, 2018
@IvanGoncharov
Copy link
Member

IvanGoncharov commented Nov 22, 2018

Interesting; that's easy to miss

@hlship Agree 👍 What if we change Introspection SDL like this:

type __Type {
  # ...

  # should be non-null for OBJECT and null for others
  interfaces: [__Type!]
}

What do you think?

@hlship
Copy link
Author

hlship commented Nov 22, 2018

That would be helpful for the next person who hits this, I suspect. It's interesting that this is exactly what unions are for, but that would make building and using the introspection schema much more complex.

@IvanGoncharov
Copy link
Member

IvanGoncharov commented Nov 22, 2018

It's interesting that this is exactly what unions are for

@hlship I think the interface would be a better match:

interface __Type {
  description: String
}

type __TypeObject implements __Type {
  description: String
  interfaces: [__TypeInteface!]!
}

Because with unions this query is invalid:

{
  __type(name: "SomeObject") {
    description
  }
}

And you need to have inline fragments for every subtype.

using the introspection schema much more complex.

Not sure about that. Would be interesting to ask Lee about the motivation for this design decision.

@hlship
Copy link
Author

hlship commented Nov 22, 2018 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants