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
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@inaccessiblein the supergraph. Regular queries,__schema { types { name } },__type(name: "...")and evendirectives { args { name } }all work; only resolving the argument's type hits it. The standardIntrospectionQuerysent by GraphiQL / Altair / codegen does exactly that via...InputValue { type { ...TypeRef } }, so those clients get a dropped connection.Reproduced on
ghcr.io/graphql-hive/router:0.2.5and0.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@inaccessibletype. Composition itself succeeds, and the router loads the supergraph fine. GraphQL Mesh subgraphs (@transport,@resolveTowith their scalar arguments) are the common source of this shape.Minimal reproduction
Two subgraphs, composed with
@theguild/federation-compositioncomposeSchemaContract(services, { include: new Set(["public"]), exclude: new Set() })(same as a Hive contract with included tagpublic). The resulting supergraph is below; dropping it intosupergraph.source: fileis enough.supergraph.graphql
For comparison, the same two subgraphs composed without the contract filter (so
MetaOptionsis 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
supergraph.source: file@theguild/federation-composition0.26.0 (composeSchemaContract), identical to what the Hive registry serves for a contract