Skip to content

Commit c676828

Browse files
martinbonninJoviDeCroock
authored andcommitted
Add support for @experimental_disableErrorPropagation (graphql#4348)
~This pull request adds support for `@onError(action: NULL)` to disable error propagation for error aware clients:~ This pull request adds support for `@experimental_disableErrorPropagation` to disable error propagation for error aware clients: ```graphql """ Disables error propagation. """ directive @experimental_disableErrorPropagation on QUERY | MUTATION | SUBSCRIPTION ``` I'm not super used to write TypeScript, feel free to amend the PR as needed but I figured it'd be good to have. The logic is unconditional. The matching [graphql-java](graphql-java/graphql-java#3772) PR has a specific opt-in flag so that it's not enabled by accident in the very unlikely event that a schema already contains a matching directive. Let me know if this is an issue. Many thanks @JoviDeCroock for pointing me in the right direction 🙏 See graphql/nullability-wg#85 See graphql/graphql-spec#1050 --------- Co-authored-by: Jovi De Croock <[email protected]>
1 parent a3c71af commit c676828

File tree

3 files changed

+100
-1
lines changed

3 files changed

+100
-1
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { describe, it } from 'mocha';
2+
3+
import { expectJSON } from '../../__testUtils__/expectJSON.js';
4+
5+
import type { PromiseOrValue } from '../../jsutils/PromiseOrValue.js';
6+
7+
import { parse } from '../../language/parser.js';
8+
9+
import { buildSchema } from '../../utilities/buildASTSchema.js';
10+
11+
import type { ExecutionResult } from '../execute.js';
12+
import { execute } from '../execute.js';
13+
14+
const syncError = new Error('bar');
15+
16+
const throwingData = {
17+
foo() {
18+
throw syncError;
19+
},
20+
};
21+
22+
const schema = buildSchema(`
23+
type Query {
24+
foo : Int!
25+
}
26+
27+
directive @experimental_disableErrorPropagation on QUERY | MUTATION | SUBSCRIPTION
28+
`);
29+
30+
function executeQuery(
31+
query: string,
32+
rootValue: unknown,
33+
): PromiseOrValue<ExecutionResult> {
34+
return execute({ schema, document: parse(query), rootValue });
35+
}
36+
37+
describe('Execute: handles errors', () => {
38+
it('with `@experimental_disableErrorPropagation returns null', async () => {
39+
const query = `
40+
query getFoo @experimental_disableErrorPropagation {
41+
foo
42+
}
43+
`;
44+
const result = await executeQuery(query, throwingData);
45+
expectJSON(result).toDeepEqual({
46+
data: { foo: null },
47+
errors: [
48+
{
49+
message: 'bar',
50+
path: ['foo'],
51+
locations: [{ line: 3, column: 9 }],
52+
},
53+
],
54+
});
55+
});
56+
it('without `experimental_disableErrorPropagation` propagates the error', async () => {
57+
const query = `
58+
query getFoo {
59+
foo
60+
}
61+
`;
62+
const result = await executeQuery(query, throwingData);
63+
expectJSON(result).toDeepEqual({
64+
data: null,
65+
errors: [
66+
{
67+
message: 'bar',
68+
path: ['foo'],
69+
locations: [{ line: 3, column: 9 }],
70+
},
71+
],
72+
});
73+
});
74+
});

src/execution/execute.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
isNonNullType,
4545
isObjectType,
4646
} from '../type/definition.js';
47+
import { GraphQLDisableErrorPropagationDirective } from '../type/directives.js';
4748
import type { GraphQLSchema } from '../type/schema.js';
4849
import { assertValidSchema } from '../type/validate.js';
4950

@@ -150,6 +151,7 @@ export interface ExecutionContext {
150151
errors: Array<GraphQLError>;
151152
abortSignalListener: AbortSignalListener | undefined;
152153
completed: boolean;
154+
errorPropagation: boolean;
153155
}
154156

155157
/**
@@ -224,6 +226,15 @@ export function execute(args: ExecutionArgs): PromiseOrValue<ExecutionResult> {
224226
return executeQueryOrMutationOrSubscriptionEvent(validatedExecutionArgs);
225227
}
226228

229+
function errorPropagation(operation: OperationDefinitionNode): boolean {
230+
const directiveNode = operation.directives?.find(
231+
(directive) =>
232+
directive.name.value === GraphQLDisableErrorPropagationDirective.name,
233+
);
234+
235+
return directiveNode === undefined;
236+
}
237+
227238
/**
228239
* Implements the "Executing operations" section of the spec.
229240
*
@@ -250,6 +261,7 @@ export function executeQueryOrMutationOrSubscriptionEvent(
250261
? new AbortSignalListener(abortSignal)
251262
: undefined,
252263
completed: false,
264+
errorPropagation: errorPropagation(validatedExecutionArgs.operation),
253265
};
254266
try {
255267
const {
@@ -736,7 +748,7 @@ function handleFieldError(
736748

737749
// If the field type is non-nullable, then it is resolved without any
738750
// protection from errors, however it still properly locates the error.
739-
if (isNonNullType(returnType)) {
751+
if (exeContext.errorPropagation && isNonNullType(returnType)) {
740752
throw error;
741753
}
742754

src/type/directives.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,19 @@ export const GraphQLOneOfDirective: GraphQLDirective = new GraphQLDirective({
226226
args: {},
227227
});
228228

229+
/**
230+
* Disables error propagation (experimental).
231+
*/
232+
export const GraphQLDisableErrorPropagationDirective = new GraphQLDirective({
233+
name: 'experimental_disableErrorPropagation',
234+
description: 'Disables error propagation.',
235+
locations: [
236+
DirectiveLocation.QUERY,
237+
DirectiveLocation.MUTATION,
238+
DirectiveLocation.SUBSCRIPTION,
239+
],
240+
});
241+
229242
/**
230243
* The full list of specified directives.
231244
*/

0 commit comments

Comments
 (0)