Skip to content

Panic in introspection when a composed directive argument type is @inaccessible (schema contracts) #1487

Description

@marbergq

Summary

The router panics (worker thread dies, connection dropped) when an introspection query resolves the type of a directive argument (__schema { directives { args { type { kind } } } }) and a composed directive definition has an argument whose type is @inaccessible in the supergraph. Regular queries, __schema { types { name } }, __type(name: "...") and even directives { args { name } } all work; only resolving the argument's type hits it. The standard IntrospectionQuery sent by GraphiQL / Altair / codegen does exactly that via ...InputValue { type { ...TypeRef } }, so those clients get a dropped connection.

thread 'main:worker:1' (11) panicked at bin/router/src/executor/introspection/resolve.rs:462:17:
Type 'MetaOptions' not found in the schema unexpectedly during introspection

Reproduced on ghcr.io/graphql-hive/router:0.2.5 and 0.2.6.

How you end up with such a supergraph

Hive Schema Contracts with an include-tag filter mark every untagged coordinate @inaccessible, including scalars that are only used as arguments of a composed directive (@composeDirective). Type-system directive definitions are not pruned from the supergraph, so the contract supergraph contains a directive definition referencing an @inaccessible type. Composition itself succeeds, and the router loads the supergraph fine. GraphQL Mesh subgraphs (@transport, @resolveTo with their scalar arguments) are the common source of this shape.

Minimal reproduction

Two subgraphs, composed with @theguild/federation-composition composeSchemaContract(services, { include: new Set(["public"]), exclude: new Set() }) (same as a Hive contract with included tag public). The resulting supergraph is below; dropping it into supergraph.source: file is enough.

supergraph.graphql
schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) @link(url: "https://example.com/meta/v1.0", import: ["@meta"]) {
  query: Query
}

directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE

directive @join__graph(name: String!, url: String!) on ENUM_VALUE

directive @join__field(
  graph: join__Graph
  requires: join__FieldSet
  provides: join__FieldSet
  type: String
  external: Boolean
  override: String
  usedOverridden: Boolean
) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION

directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE

directive @join__type(
  graph: join__Graph!
  key: join__FieldSet
  extension: Boolean! = false
  resolvable: Boolean! = true
  isInterfaceObject: Boolean! = false
) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR

directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION

scalar join__FieldSet

directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA

scalar link__Import

enum link__Purpose {
  """
  `SECURITY` features provide metadata necessary to securely resolve fields.
  """
  SECURITY
  """
  `EXECUTION` features provide metadata necessary for operation execution.
  """
  EXECUTION
}

directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ENUM | ENUM_VALUE | SCALAR | INPUT_OBJECT | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION

enum join__Graph {
  PRODUCTS @join__graph(name: "products", url: "http://products/graphql")
  REVIEWS @join__graph(name: "reviews", url: "http://reviews/graphql")
}

directive @meta(options: MetaOptions) repeatable on FIELD_DEFINITION

"""
Argument type of a composed directive. Untagged, so an include-tag contract makes it @inaccessible.
"""
scalar MetaOptions @join__type(graph: PRODUCTS) @inaccessible

type Query @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) {
  products: [Product!]! @join__field(graph: PRODUCTS)
  internalCount: Int @meta(options: "{\"x\":1}") @join__field(graph: PRODUCTS) @inaccessible
}

type Product @join__type(graph: PRODUCTS, key: "id") @join__type(graph: REVIEWS, key: "id") {
  id: ID!
  name: String! @join__field(graph: PRODUCTS)
  cost: Int @join__field(graph: PRODUCTS) @inaccessible
  reviews: [String!]! @join__field(graph: REVIEWS)
}
# router.config.yaml
supergraph:
  source: file
  path: /app/config/supergraph.graphql
docker run --rm -p 4000:4000 \
  -v "$PWD/router.config.yaml:/app/config/router.config.yaml:ro" \
  -v "$PWD/supergraph.graphql:/app/config/supergraph.graphql:ro" \
  -e ROUTER_CONFIG_FILE_PATH=/app/config/router.config.yaml \
  ghcr.io/graphql-hive/router:0.2.6

curl -s http://localhost:4000/graphql -H 'content-type: application/json' \
  -d '{"query":"{ __schema { directives { args { type { kind } } } } }"}'
# -> empty reply / connection reset; router log shows the panic above

curl -s http://localhost:4000/graphql -H 'content-type: application/json' \
  -d '{"query":"{ __schema { directives { args { name } } } __type(name: \"MetaOptions\") { name } }"}'
# -> 200, args listed, __type is null (the scalar is correctly hidden)

For comparison, the same two subgraphs composed without the contract filter (so MetaOptions is accessible) introspect fine.

Expected

Either skip the directive (or the argument) in the introspection result when its argument type is inaccessible, or return a GraphQL error. A panic that takes down the worker and drops the connection (surfaces as a 502 from a proxy in front) is the surprising part.

Environment

  • hive-router 0.2.5 and 0.2.6 (docker images), default config plus supergraph.source: file
  • supergraph composed by @theguild/federation-composition 0.26.0 (composeSchemaContract), identical to what the Hive registry serves for a contract

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions