Skip to content

Commit f17d05a

Browse files
Add support for @experimental_disableErrorPropagation (#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 <decroockjovi@gmail.com>
1 parent 72c9044 commit f17d05a

File tree

3 files changed

+103
-2
lines changed

3 files changed

+103
-2
lines changed
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 { execute } from '../execute.js';
12+
import type { ExecutionResult } from '../types.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

+16-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ import {
4444
isNonNullType,
4545
isObjectType,
4646
} from '../type/definition.js';
47-
import { GraphQLStreamDirective } from '../type/directives.js';
47+
import {
48+
GraphQLDisableErrorPropagationDirective,
49+
GraphQLStreamDirective,
50+
} from '../type/directives.js';
4851
import type { GraphQLSchema } from '../type/schema.js';
4952
import { assertValidSchema } from '../type/validate.js';
5053

@@ -170,6 +173,7 @@ export interface ExecutionContext {
170173
abortSignalListener: AbortSignalListener | undefined;
171174
completed: boolean;
172175
cancellableStreams: Set<CancellableStreamRecord> | undefined;
176+
errorPropagation: boolean;
173177
}
174178

175179
interface IncrementalContext {
@@ -314,6 +318,15 @@ export function executeQueryOrMutationOrSubscriptionEvent(
314318
return ensureSinglePayload(result);
315319
}
316320

321+
function errorPropagation(operation: OperationDefinitionNode): boolean {
322+
const directiveNode = operation.directives?.find(
323+
(directive) =>
324+
directive.name.value === GraphQLDisableErrorPropagationDirective.name,
325+
);
326+
327+
return directiveNode === undefined;
328+
}
329+
317330
export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
318331
validatedExecutionArgs: ValidatedExecutionArgs,
319332
): PromiseOrValue<ExecutionResult | ExperimentalIncrementalExecutionResults> {
@@ -326,6 +339,7 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
326339
: undefined,
327340
completed: false,
328341
cancellableStreams: undefined,
342+
errorPropagation: errorPropagation(validatedExecutionArgs.operation),
329343
};
330344
try {
331345
const {
@@ -976,7 +990,7 @@ function handleFieldError(
976990

977991
// If the field type is non-nullable, then it is resolved without any
978992
// protection from errors, however it still properly locates the error.
979-
if (isNonNullType(returnType)) {
993+
if (exeContext.errorPropagation && isNonNullType(returnType)) {
980994
throw error;
981995
}
982996

src/type/directives.ts

+13
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,19 @@ export const GraphQLOneOfDirective: GraphQLDirective = new GraphQLDirective({
276276
args: {},
277277
});
278278

279+
/**
280+
* Disables error propagation (experimental).
281+
*/
282+
export const GraphQLDisableErrorPropagationDirective = new GraphQLDirective({
283+
name: 'experimental_disableErrorPropagation',
284+
description: 'Disables error propagation.',
285+
locations: [
286+
DirectiveLocation.QUERY,
287+
DirectiveLocation.MUTATION,
288+
DirectiveLocation.SUBSCRIPTION,
289+
],
290+
});
291+
279292
/**
280293
* The full list of specified directives.
281294
*/

0 commit comments

Comments
 (0)