Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion src/indexer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ConduitError, UNKNOWN_CONTRACT_ERROR_CODE } from './errors.js';
import { IndexerTimeoutError } from './errors.js';

export interface GraphQLQueryOptions {
Expand Down Expand Up @@ -106,7 +107,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;
}

/**
Expand Down
22 changes: 22 additions & 0 deletions src/tests/graphql-indexer-memory-leak.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,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);

Expand Down