From afe0cc5ddfc7a1291dc878c61793b58850ae848b Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 3 Apr 2024 11:30:19 +0300 Subject: [PATCH] enhance(supergraph/runtime): better error messages in case of supergraph 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 --- .changeset/orange-coats-agree.md | 22 +++ .prettierignore | 1 + .../legacy/handlers/supergraph/src/index.ts | 38 +++-- .../tests/fixtures/supergraph-invalid.graphql | 1 + .../supergraph/tests/supergraph.spec.ts | 135 +++++++++++++----- packages/legacy/runtime/src/get-mesh.ts | 4 +- 6 files changed, 159 insertions(+), 42 deletions(-) create mode 100644 .changeset/orange-coats-agree.md create mode 100644 packages/legacy/handlers/supergraph/tests/fixtures/supergraph-invalid.graphql diff --git a/.changeset/orange-coats-agree.md b/.changeset/orange-coats-agree.md new file mode 100644 index 000000000000..55d094cf5cb5 --- /dev/null +++ b/.changeset/orange-coats-agree.md @@ -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 . +``` diff --git a/.prettierignore b/.prettierignore index 332bfcb61c0c..a01a91002415 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,3 +7,4 @@ dist/ /.husky/_/ .bob/ .yarn +supergraph-invalid.graphql diff --git a/packages/legacy/handlers/supergraph/src/index.ts b/packages/legacy/handlers/supergraph/src/index.ts index a584c80e8e3d..8e8724916078 100644 --- a/packages/legacy/handlers/supergraph/src/index.ts +++ b/packages/legacy/handlers/supergraph/src/index.ts @@ -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(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); }); } @@ -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; +} diff --git a/packages/legacy/handlers/supergraph/tests/fixtures/supergraph-invalid.graphql b/packages/legacy/handlers/supergraph/tests/fixtures/supergraph-invalid.graphql new file mode 100644 index 000000000000..cbcdcddaa7e1 --- /dev/null +++ b/packages/legacy/handlers/supergraph/tests/fixtures/supergraph-invalid.graphql @@ -0,0 +1 @@ +type Query { diff --git a/packages/legacy/handlers/supergraph/tests/supergraph.spec.ts b/packages/legacy/handlers/supergraph/tests/supergraph.spec.ts index dc321ade631f..b721b73bc1f1 100644 --- a/packages/legacy/handlers/supergraph/tests/supergraph.spec.ts +++ b/packages/legacy/handlers/supergraph/tests/supergraph.spec.ts @@ -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, @@ -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, @@ -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 .`); + }); + 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`); + }); }); diff --git a/packages/legacy/runtime/src/get-mesh.ts b/packages/legacy/runtime/src/get-mesh.ts index 1b764b0582ee..e2736665dfc2 100644 --- a/packages/legacy/runtime/src/get-mesh.ts +++ b/packages/legacy/runtime/src/get-mesh.ts @@ -209,7 +209,9 @@ export async function getMesh(options: GetMeshOptions): Promise { 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; } }),