Troubleshooting Connectors
Fixes and workarounds for general issues and specific errors
Debugging
If you haven't already, set up IDE support for Apollo Connectors. These plugins enable syntax highlighting, validation, autocomplete, and more.
Return debug information in GraphQL responses
To diagnose issues with your connectors, you can return debugging information as part of each GraphQL response, including the details of each HTTP request and response from your connectors.
First, run your router in development mode. This happens automatically when using rover dev
. If you're running a router manually, you can pass it the --dev
CLI argument.
Your local Apollo Sandbox presents request, response, and mapping information in the right panel after you execute an operation.
Adding debug information to telemetry
You can also add information about each connector request and response to your router telemetry. For example, the following router config will emit an event at the INFO level containing the details of each request and response. This event will contain the HTTP body and headers of each request and response, as well as the status code of each response:
telemetry:
instrumentation:
events:
connector:
request: info
response: info
Additionally, you can add information about any mapping problems to your router telemetry using selectors. For example, to emit an error event whenever there is one or more mapping problems, use the following router config:
telemetry:
instrumentation:
events:
connector:
request.mapping.problems:
message: "[Request Mapping Problems]"
level: error
on: request
condition:
gt:
- connector_response_mapping_problems: count
- 0
attributes:
request_mapping_problems:
connector_request_mapping_problems: problems
response.mapping.problems:
message: "[Response Mapping Problems]"
level: error
on: response
condition:
gt:
- connector_response_mapping_problems: count
- 0
attributes:
response_mapping_problems:
connector_response_mapping_problems: problems
The above events will be sent to the telemetry exporters you have configured in the router. For example, if you have the stdout
logging exporter configured, you might see error events in the router logs:
ERROR response_mapping_problems=["{"message":"Property .missing not found in object","path":"@.missing","count":10}"] [Response Mapping Problems] kind=response.mapping.problems
Common errors
The following are the most common composition errors you may encounter with connectors.
Satisfiability errors
A satisfiability error means that the GraphQL API composed of your REST APIs can't satisfy all possible requests. This occurs because he rules of federation composition, including rules for unreachable fields, apply equally to connectors and GraphQL subgraphs.
Example scenario
Consider a schema with two connectors that hit different endpoints and get differing representations of the Post
type. The goal is to merge the representations into a unified Post
type so that clients can access all the fields without caring about the underlying data sources.
type Query {
posts: [Post]
@connect(
http: { GET: "https://api.example.com/v1/posts" }
selection: "id title createdAt"
)
post(id: ID!): Post
@connect(
http: { GET: "https://api.example.com/v2/posts/{$args.id}" }
selection: "id title body"
)
}
type Post {
id: ID!
title: String
body: String
createdAt: String
}
These connectors would raise the following satisfiability errors:
SATISFIABILITY_ERROR: The following supergraph API query:
{
posts {
body
}
}
cannot be satisfied by the subgraphs because:
- from subgraph "posts":
- cannot find field "Post.body".
- cannot move to subgraph "posts", which has field "Post.body", because type "Post" has no @key defined in subgraph "posts".
SATISFIABILITY_ERROR: The following supergraph API query:
{
post(id: "<any id>") {
createdAt
}
}
cannot be satisfied by the subgraphs because:
- from subgraph "posts":
- cannot find field "Post.createdAt".
- cannot move to subgraph "posts", which has field "Post.createdAt", because type "Post" has no @key defined in subgraph "posts".
This error means we can't reach body
from the Query.posts
field, and we can't reach createdAt
from the Query.post(id:)
field.
@key
directive and an entity resolver for the type. For a connector, adding entity: true
is the equivalent of defining a @key
and an entity resolver.The first issue (body
) is easily solved with entity connectors. By adding entity: true
to the connector on Query.post(id:)
, the query planner can make subsequent fetches to fetch that field.
type Query {
post(id: ID!): Post
@connect(
http: { GET: "https://api.example.com/v2/posts/{$args.id}" }
selection: "id title body"
entity: true
)
}
The second issue (createdAt
) doesn't have a straightforward solution. We need an endpoint that takes the ID of a Post
and returns the createdAt
field. The simplest solution is to add a second connector to the Query.post(id:)
field:
type Query {
post(id: ID!): Post
@connect(
http: { GET: "https://api.example.com/v2/posts/{$args.id}" }
selection: "id title body"
entity: true
)
@connect(
http: { GET: "https://api.example.com/v1/posts/{$args.id}" }
selection: "id createdAt"
entity: true
)
}
Now there is always a way to resolve all fields, regardless of which Query root field the client uses.
entity: true
.Circular references
If you get a composition error about circular references, you've run into a connectors limitation. Direct circular references, such as User.friends: [User]
, are not supported. Indirect circular references, such as User.company: Business
and Business.employees: [User]
, are supported using certain patterns.
Example scenario
When using a connector, a selection
of a type can't refer to itself. For example, a selection that results in User.friends: [User]
is invalid:
type Query {
user(id: ID!): User
@connect(
http: { GET: "https://api.example.com/users/{$args.id}" }
selection: """
id
name
friends { # Circular reference to User
id
}
"""
)
}
type User {
id: ID!
name: String
friends: [User]
}
This occurs if any type within the selection can lead to a circular reference, not just the top-level type. For example, this is also invalid:
type Query {
user(id: ID!): User
@connect(
http: { GET: "https://api.example.com/users/{$args.id}" }
selection: """
id
name
favoriteBooks { # First reference to Book
id
author {
id
books { # Circular reference to Book
id
}
}
}
"""
)
}
type User {
id: ID!
name: String
favoriteBooks: [Book]
}
type Book {
id: ID!
author: Author
}
type Author {
id: ID!
books: [Book]
}
To avoid circular references you can often use another connector:
type Query {
user(id: ID!): User
@connect(
http: { GET: "https://api.example.com/users/{$args.id}" }
selection: """
id
name
favoriteBooks {
id
title
author {
id
# No reference to books here
}
}
"""
)
}
type User {
id: ID!
name: String
favoriteBooks: [Book]
}
type Book {
id: ID!
title: String!
author: Author
@connect(
http: { GET: "https://api.example.com/books/{$this.id}/author" }
selection: """
id
name
# No reference to books
"""
)
}
type Author {
id: ID!
books: [Book]
@connect(
http: { GET: "https://api.example.com/authors/{$this.id}/books" }
selection: """
id
title
# No reference to author
"""
)
}
null
response values
If a connector's selection mapping and the corresponding type or field don't match, a null
value is returned to avoid breaking the client contract.
For example, the following connector's http
and selection
arguments target a single product, but Query.product
indicates an array of products should be returned:
type Query {
# TYPO: the response contains a single product, not an array
product(id: ID!): [Product]
@connect(
http: { GET: "https://api/products/{$args.id}" }
selection: "id name price"
)
}
In the response, the product
field will be null
, though you won't see any mapping errors in the debugger.
{
"data": {
"product": null
}
}
To resolve issues like this, you can inspect the raw Response body in the debugger and then update the schema accordingly.
In this case, the fix is to change the return type from [Product]
to Product
.