apollographql / apollographql/federation
ApolloServer returns null for whole request when one of `__resolveReference` returns `null`
- Dominant language
- TypeScript
- Stars
- 725
- Forks
- 276
- Avg merge
- 1h 47m
- Merged PRs (30d)
- 1
Description
ApolloServer returns null for whole request when one of `__resolveReference` returns `null`.
That happens only when array item type is not optional.
```
assets: [Asset!]!
```
Code to reproduce:
```ts
import { buildFederatedSchema } from '@apollo/federation';
import { ApolloGateway } from '@apollo/gateway';
import { ApolloServer, gql, ServerInfo } from 'apollo-server';
import { ApolloServerTestClient, createTestClient } from 'apollo-server-testing';
describe('#handler', () => {
let gatewayServer: ApolloServer;
let landingPagesService: ServerInfo;
let assetsService: ServerInfo;
let testClient: ApolloServerTestClient;
beforeAll(async () => {
landingPagesService = await runLandingPages();
assetsService = await runAssets();
const gateway = new ApolloGateway({
serviceList: [
{ name: 'landingPages', url: `${landingPagesService.url}graphql` },
{ name: 'assets', url: `${assetsService.url}graphql` },
],
});
gatewayServer = new ApolloServer({
gateway,
engine: false,
subscriptions: false,
logger: console,
});
testClient = createTestClient(gatewayServer);
});
afterAll(() => {
landingPagesService.server.close();
assetsService.server.close();
});
it('should perform query', async () => {
// arrange
// run query against the server and snapshot the output
const body = await testClient.query({
query: '{ landingPage { assets { id name } } }',
});
expect(body.errors).not.toBeDefined();
expect(body.data).not.toBe(null);
expect(body.data).toMatchSnapshot();
}, 10000);
});
async function runAssets() {
const typeDefs = gql`
type Asset @key(fields: "id") {
id: String!
name: String!
}
`;
const assets = [
{
id: '1',
name: 'Asset0',
},
{
id: '2',
name: 'Asset1',
},
];
const resolvers = {
Asset: {
// eslint-disable-next-line @typescript-eslint/naming-convention
__resolveReference(object: any) {
return assets.find(asset => asset.id === object.id);
},
},
};
const server = new ApolloServer({
schema: buildFederatedSchema([
{
typeDefs,
resolvers,
},
]),
engine: false,
tracing: false,
});
const listener = await server.listen({ port: 40001 });
return listener;
}
async function runLandingPages() {
const typeDefs = gql`
extend type Query {
landingPage: LandingPage!
}
extend type Asset @key(fields: "id") {
id: String! @external
}
type LandingPage {
assets: [Asset!]!
}
`;
const resolvers = {
Query: {
landingPage() {
return {
assets: [
{ id: '1' },
{ id: '2' },
// uncomment to see incorrect behavior
// { id: '10' },
],
};
},
},
};
const server = new ApolloServer({
schema: buildFederatedSchema([
{
typeDefs,
resolvers,
},
]),
engine: false,
tracing: false,
});
const listener = await server.listen({ port: 40002 });
return listener;
}
```
### The expected behavior
Null items are filter out, and `errors` is filled.
### The actual behavior.
`data` is null.
versions:
- @apollo/gateway@0.28.1
- apollo-server@2.24.0
- apollo-server-testing@2.24.0
Contributor guide
Research direction
Start by running the supplied TypeScript reproduction using ApolloGateway, buildFederatedSchema, and the __resolveReference entry point, including the commented-out missing asset. Trace how the gateway handles a null reference for the non-null assets: [Asset!]! field. Done means the response follows the reported expectation, with null items filtered, errors populated, and data remaining non-null.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- graphql, typescript
- Domain
- api, backend, distributed-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100