Skip to content

Commit

Permalink
enhance(supergraph/runtime): better error messages in case of supergr…
Browse files Browse the repository at this point in the history
…aph endpoint is down or invalid (#6790)

* enhance(supergraph/runtime): better error messages in case of supergraph endpoint is down or invalid

* Use default fetch

* Check if context has request headers

* Fix tests

* Better error message

* Remove unrelated changes
  • Loading branch information
ardatan committed Apr 3, 2024
1 parent b5f9ebb commit afe0cc5
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 42 deletions.
22 changes: 22 additions & 0 deletions .changeset/orange-coats-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"@graphql-mesh/supergraph": patch
"@graphql-mesh/runtime": patch
---

Better error messages in case of Supergraph SDL endpoint returns invalid result or it is down

If the endpoint is down;
```
Failed to generate the schema for the source "supergraph"
Failed to load supergraph SDL from http://down-sdl-source.com/my-sdl.graphql:
Couldn't resolve host name
```

If the endpoint returns invalid result;
```
Failed to generate the schema for the source "supergraph"
Supergraph source must be a valid GraphQL SDL string or a parsed DocumentNode, but got an invalid result from ./fixtures/supergraph-invalid.graphql instead.
Got result: type Query {
Got error: Syntax Error: Expected Name, found <EOF>.
```
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ dist/
/.husky/_/
.bob/
.yarn
supergraph-invalid.graphql
38 changes: 30 additions & 8 deletions packages/legacy/handlers/supergraph/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,24 +61,25 @@ export default class SupergraphHandler implements MeshHandler {
importFn: this.importFn,
fetch: this.fetchFn,
logger: this.logger,
}).catch(e => {
throw new Error(`Failed to load supergraph SDL from ${interpolatedSource}:\n ${e.message}`);
});
if (typeof res === 'string') {
return parse(res, { noLocation: true });
}
return res;
return handleSupergraphResponse(res, interpolatedSource);
}
return this.supergraphSdl.getWithSet(async () => {
const sdlOrIntrospection = await readFile<string | DocumentNode>(interpolatedSource, {
headers: schemaHeadersFactory({
env: process.env,
}),
cwd: this.baseDir,
allowUnknownExtensions: true,
importFn: this.importFn,
fetch: this.fetchFn,
logger: this.logger,
}).catch(e => {
throw new Error(`Failed to load supergraph SDL from ${interpolatedSource}:\n ${e.message}`);
});
if (typeof sdlOrIntrospection === 'string') {
return parse(sdlOrIntrospection, { noLocation: true });
}
return sdlOrIntrospection;
return handleSupergraphResponse(sdlOrIntrospection, interpolatedSource);
});
}

Expand Down Expand Up @@ -158,3 +159,24 @@ export default class SupergraphHandler implements MeshHandler {
};
}
}

function handleSupergraphResponse(
sdlOrDocumentNode: string | DocumentNode,
interpolatedSource: string,
) {
if (typeof sdlOrDocumentNode === 'string') {
try {
return parse(sdlOrDocumentNode, { noLocation: true });
} catch (e) {
throw new Error(
`Supergraph source must be a valid GraphQL SDL string or a parsed DocumentNode, but got an invalid result from ${interpolatedSource} instead.\n Got result: ${sdlOrDocumentNode}\n Got error: ${e.message}`,
);
}
}
if (sdlOrDocumentNode?.kind !== 'Document') {
throw new Error(
`Supergraph source must be a valid GraphQL SDL string or a parsed DocumentNode, but got an invalid result from ${interpolatedSource} instead.\n Got result: ${JSON.stringify(sdlOrDocumentNode, null, 2)}`,
);
}
return sdlOrDocumentNode;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
type Query {
135 changes: 102 additions & 33 deletions packages/legacy/handlers/supergraph/tests/supergraph.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import BareMerger from '@graphql-mesh/merger-bare';
import { getMesh } from '@graphql-mesh/runtime';
import { InMemoryStoreStorageAdapter, MeshStore } from '@graphql-mesh/store';
import SupergraphHandler from '@graphql-mesh/supergraph';
import { MeshFetch } from '@graphql-mesh/types';
import { DefaultLogger, defaultImportFn as importFn, PubSub } from '@graphql-mesh/utils';
import { Logger, MeshFetch } from '@graphql-mesh/types';
import { defaultImportFn as importFn, PubSub } from '@graphql-mesh/utils';
import { fetch as defaultFetchFn } from '@whatwg-node/fetch';
import {
AUTH_HEADER as AUTHORS_AUTH_HEADER,
server as authorsServer,
Expand All @@ -16,38 +17,58 @@ import {
} from './fixtures/service-book/server';

describe('Supergraph', () => {
const baseDir = __dirname;
const cache = new LocalforageCache();
const store = new MeshStore('test', new InMemoryStoreStorageAdapter(), {
validate: false,
readonly: false,
});
const logger = new DefaultLogger('test');
const pubsub = new PubSub();
const merger = new BareMerger({ cache, pubsub, store, logger });
const fetchFn: MeshFetch = async (url, options) => {
if (url.includes('authors')) {
return authorsServer.fetch(url, options);
}
if (url.includes('books')) {
return booksServer.fetch(url, options);
}
throw new Error(`Unknown URL: ${url}`);
};
const baseHandlerConfig = {
name: 'BooksAndAuthors',
baseDir,
cache,
store,
pubsub,
logger,
importFn,
};
const baseGetMeshConfig = {
cache,
fetchFn,
merger,
let baseHandlerConfig;
let baseGetMeshConfig;
const logger = {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
log: jest.fn(),
child() {
return logger;
},
};
const libcurl = globalThis.libcurl;
beforeEach(() => {
globalThis.libcurl = null;
const baseDir = __dirname;
const cache = new LocalforageCache();
const store = new MeshStore('test', new InMemoryStoreStorageAdapter(), {
validate: false,
readonly: false,
});
const pubsub = new PubSub();
const merger = new BareMerger({ cache, pubsub, store, logger });
const fetchFn: MeshFetch = async (url, options) => {
if (url.includes('authors')) {
return authorsServer.fetch(url, options);
}
if (url.includes('books')) {
return booksServer.fetch(url, options);
}
return defaultFetchFn(url, options);
};
baseHandlerConfig = {
name: 'BooksAndAuthors',
baseDir,
cache,
store,
pubsub,
logger,
importFn,
};
baseGetMeshConfig = {
cache,
logger,
fetchFn,
merger,
};
jest.clearAllMocks();
});
afterEach(() => {
globalThis.libcurl = libcurl;
});
it('supports individual headers for each subgraph with interpolation', async () => {
const handler = new SupergraphHandler({
...baseHandlerConfig,
Expand Down Expand Up @@ -209,4 +230,52 @@ describe('Supergraph', () => {
},
});
});
it('throws a helpful error when the supergraph is an invalid SDL', async () => {
const handler = new SupergraphHandler({
...baseHandlerConfig,
config: {
source: './fixtures/supergraph-invalid.graphql',
},
});
await expect(
getMesh({
sources: [
{
name: 'supergraph',
handler,
},
],
...baseGetMeshConfig,
}),
).rejects.toThrow();
expect(logger.error.mock.calls[0][0].toString())
.toBe(`Failed to generate the schema for the source "supergraph"
Supergraph source must be a valid GraphQL SDL string or a parsed DocumentNode, but got an invalid result from ./fixtures/supergraph-invalid.graphql instead.
Got result: type Query {
Got error: Syntax Error: Expected Name, found <EOF>.`);
});
it('throws a helpful error when the source is down', async () => {
const handler = new SupergraphHandler({
...baseHandlerConfig,
config: {
source: 'http://down-sdl-source.com/my-sdl.graphql',
},
});
await expect(
getMesh({
sources: [
{
name: 'supergraph',
handler,
},
],
...baseGetMeshConfig,
}),
).rejects.toThrow();
expect(logger.error.mock.calls[0][0].toString())
.toBe(`Failed to generate the schema for the source "supergraph"
Failed to load supergraph SDL from http://down-sdl-source.com/my-sdl.graphql:
getaddrinfo ENOTFOUND down-sdl-source.com`);
});
});
4 changes: 3 additions & 1 deletion packages/legacy/runtime/src/get-mesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,9 @@ export async function getMesh(options: GetMeshOptions): Promise<MeshInstance> {
createProxyingResolver: createProxyingResolverFactory(apiName, rootTypeMap),
});
} catch (e: any) {
sourceLogger.error(`Failed to generate the schema`, e);
sourceLogger.error(
`Failed to generate the schema for the source "${apiName}"\n ${e.message}`,
);
failed = true;
}
}),
Expand Down

0 comments on commit afe0cc5

Please sign in to comment.