Skip to content

Commit d3d83bc

Browse files
feat(useErrorHandler): handle incremental execution errors (#2113)
* fix(useErrorHandler): handle incremental execution errors Fixes #2112 * address review comments * prettier --------- Co-authored-by: Valentin Cocaud <v.cocaud@gmail.com>
1 parent 49b9d8a commit d3d83bc

File tree

5 files changed

+64
-5
lines changed

5 files changed

+64
-5
lines changed

.changeset/breezy-feet-greet.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@envelop/core': minor
3+
---
4+
5+
Handle incremental execution errors in useErrorHandler

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"tslib": "^2.5.0"
6363
},
6464
"devDependencies": {
65+
"@graphql-tools/executor": "^1.1.0",
6566
"@graphql-tools/schema": "10.0.29",
6667
"@graphql-tools/utils": "10.10.3",
6768
"@repeaterjs/repeater": "3.0.6",

packages/core/src/plugins/use-error-handler.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { DefaultContext, ExecutionResult, Plugin, TypedExecutionArgs } from '@envelop/types';
1+
import {
2+
DefaultContext,
3+
ExecutionResult,
4+
IncrementalExecutionResult,
5+
Plugin,
6+
TypedExecutionArgs,
7+
} from '@envelop/types';
28
import { handleStreamOrSingleExecutionResult } from '../utils.js';
39
import { isGraphQLError, SerializableGraphQLErrorLike } from './use-masked-errors.js';
410

@@ -13,15 +19,24 @@ export type ErrorHandler = ({
1319
}) => void;
1420

1521
type ErrorHandlerCallback<ContextType> = {
16-
result: ExecutionResult;
22+
result: ExecutionResult | IncrementalExecutionResult;
1723
args: TypedExecutionArgs<ContextType>;
1824
};
1925

2026
const makeHandleResult =
2127
<ContextType extends Record<any, any>>(errorHandler: ErrorHandler) =>
2228
({ result, args }: ErrorHandlerCallback<ContextType>) => {
23-
if (result.errors?.length) {
24-
errorHandler({ errors: result.errors, context: args, phase: 'execution' });
29+
const errors = result.errors ? [...result.errors] : [];
30+
if ('incremental' in result && result.incremental) {
31+
for (const increment of result.incremental) {
32+
if (increment.errors) {
33+
errors.push(...increment.errors);
34+
}
35+
}
36+
}
37+
38+
if (errors.length) {
39+
errorHandler({ errors, context: args, phase: 'execution' });
2540
}
2641
};
2742

packages/core/test/plugins/use-error-handler.spec.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { useExtendContext } from '@envelop/core';
1+
import * as GraphQLJS from 'graphql';
2+
import { useEngine, useExtendContext } from '@envelop/core';
23
import {
34
assertStreamExecutionValue,
45
collectAsyncIteratorValues,
56
createTestkit,
67
} from '@envelop/testing';
78
import { Plugin } from '@envelop/types';
9+
import { normalizedExecutor } from '@graphql-tools/executor';
810
import { makeExecutableSchema } from '@graphql-tools/schema';
911
import { createGraphQLError } from '@graphql-tools/utils';
1012
import { Repeater } from '@repeaterjs/repeater';
@@ -139,4 +141,37 @@ describe('useErrorHandler', () => {
139141
}),
140142
);
141143
});
144+
145+
it('should invoke error handler when error happens during incremental execution', async () => {
146+
const schema = makeExecutableSchema({
147+
typeDefs: /* GraphQL */ `
148+
directive @defer on FRAGMENT_SPREAD | INLINE_FRAGMENT
149+
150+
type Query {
151+
foo: String
152+
}
153+
`,
154+
resolvers: {
155+
Query: {
156+
foo: () => {
157+
throw new Error('kaboom');
158+
},
159+
},
160+
},
161+
});
162+
163+
const mockHandler = jest.fn();
164+
const testInstance = createTestkit(
165+
[
166+
useEngine({ ...GraphQLJS, execute: normalizedExecutor, subscribe: normalizedExecutor }),
167+
useErrorHandler(mockHandler),
168+
],
169+
schema,
170+
);
171+
const result = await testInstance.execute(`query { ... @defer { foo } }`);
172+
assertStreamExecutionValue(result);
173+
await collectAsyncIteratorValues(result);
174+
175+
expect(mockHandler).toHaveBeenCalledWith(expect.objectContaining({ phase: 'execution' }));
176+
});
142177
});

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)