From 2e9ce4a1187fe2c08ecc50a8c5d787924784e674 Mon Sep 17 00:00:00 2001 From: veemakama Date: Mon, 31 Aug 2026 07:35:34 +0100 Subject: [PATCH] Bug: GraphQLIndexer.query ignores GraphQL-level errors in an HTTP 200 response Updated File --- src/indexer.ts | 23 ++++++++++++++++++- src/tests/graphql-indexer-memory-leak.test.ts | 22 ++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/indexer.ts b/src/indexer.ts index 9eb8873..35e5bf5 100644 --- a/src/indexer.ts +++ b/src/indexer.ts @@ -1,3 +1,5 @@ +import { ConduitError, UNKNOWN_CONTRACT_ERROR_CODE } from './errors.js'; + export interface GraphQLQueryOptions { query: string; variables?: Record; @@ -82,7 +84,26 @@ export class GraphQLIndexer { throw new Error(`GraphQL query failed with status ${response.status}: ${response.statusText}`); } - return (await response.json()) as unknown; + const body = (await response.json()) as { + data?: unknown; + errors?: unknown[]; + }; + + if (Array.isArray(body?.errors) && body.errors.length > 0) { + const messages = body.errors + .map((error) => { + if (error && typeof error === 'object' && 'message' in error) { + const message = (error as { message?: unknown }).message; + return typeof message === 'string' && message.length > 0 ? message : JSON.stringify(error); + } + return String(error); + }) + .filter((message) => message.length > 0); + + throw new ConduitError('stream', UNKNOWN_CONTRACT_ERROR_CODE, messages.join('; ')); + } + + return body?.data; } subscribe(options: GraphQLSubscriptionOptions): IndexerSubscription { diff --git a/src/tests/graphql-indexer-memory-leak.test.ts b/src/tests/graphql-indexer-memory-leak.test.ts index 80cd5f0..3e2042f 100644 --- a/src/tests/graphql-indexer-memory-leak.test.ts +++ b/src/tests/graphql-indexer-memory-leak.test.ts @@ -69,6 +69,28 @@ describe('GraphQLIndexer Memory Leak & Real Network I/O Tests', () => { indexer.cleanup(); }); + it('throws when a GraphQL response contains query-level errors despite HTTP 200', async () => { + vi.spyOn(globalThis, 'fetch').mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ + data: null, + errors: [ + { message: 'Cannot query field "doesNotExist" on type "Query".' }, + { message: 'Resolver error: boom' }, + ], + }), + } as Response); + + const indexer = new GraphQLIndexer(endpoint); + + await expect(indexer.query({ query: 'query { doesNotExist }' })).rejects.toThrow( + 'Cannot query field "doesNotExist" on type "Query".' + ); + + indexer.cleanup(); + }); + it('subscribes and properly cleans up active subscriptions on unsubscribe()', async () => { const indexer = new GraphQLIndexer(endpoint);