Skip to content

Commit

Permalink
Add support for @experimental_disableErrorPropagation (#4348)
Browse files Browse the repository at this point in the history
~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]>
  • Loading branch information
martinbonnin and JoviDeCroock authored Feb 24, 2025
1 parent 72c9044 commit f17d05a
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 2 deletions.
74 changes: 74 additions & 0 deletions src/execution/__tests__/errorPropagation-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { describe, it } from 'mocha';

import { expectJSON } from '../../__testUtils__/expectJSON.js';

import type { PromiseOrValue } from '../../jsutils/PromiseOrValue.js';

import { parse } from '../../language/parser.js';

import { buildSchema } from '../../utilities/buildASTSchema.js';

import { execute } from '../execute.js';
import type { ExecutionResult } from '../types.js';

const syncError = new Error('bar');

const throwingData = {
foo() {
throw syncError;
},
};

const schema = buildSchema(`
type Query {
foo : Int!
}
directive @experimental_disableErrorPropagation on QUERY | MUTATION | SUBSCRIPTION
`);

function executeQuery(
query: string,
rootValue: unknown,
): PromiseOrValue<ExecutionResult> {
return execute({ schema, document: parse(query), rootValue });
}

describe('Execute: handles errors', () => {
it('with `@experimental_disableErrorPropagation returns null', async () => {
const query = `
query getFoo @experimental_disableErrorPropagation {
foo
}
`;
const result = await executeQuery(query, throwingData);
expectJSON(result).toDeepEqual({
data: { foo: null },
errors: [
{
message: 'bar',
path: ['foo'],
locations: [{ line: 3, column: 9 }],
},
],
});
});
it('without `experimental_disableErrorPropagation` propagates the error', async () => {
const query = `
query getFoo {
foo
}
`;
const result = await executeQuery(query, throwingData);
expectJSON(result).toDeepEqual({
data: null,
errors: [
{
message: 'bar',
path: ['foo'],
locations: [{ line: 3, column: 9 }],
},
],
});
});
});
18 changes: 16 additions & 2 deletions src/execution/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ import {
isNonNullType,
isObjectType,
} from '../type/definition.js';
import { GraphQLStreamDirective } from '../type/directives.js';
import {
GraphQLDisableErrorPropagationDirective,
GraphQLStreamDirective,
} from '../type/directives.js';
import type { GraphQLSchema } from '../type/schema.js';
import { assertValidSchema } from '../type/validate.js';

Expand Down Expand Up @@ -170,6 +173,7 @@ export interface ExecutionContext {
abortSignalListener: AbortSignalListener | undefined;
completed: boolean;
cancellableStreams: Set<CancellableStreamRecord> | undefined;
errorPropagation: boolean;
}

interface IncrementalContext {
Expand Down Expand Up @@ -314,6 +318,15 @@ export function executeQueryOrMutationOrSubscriptionEvent(
return ensureSinglePayload(result);
}

function errorPropagation(operation: OperationDefinitionNode): boolean {
const directiveNode = operation.directives?.find(
(directive) =>
directive.name.value === GraphQLDisableErrorPropagationDirective.name,
);

return directiveNode === undefined;
}

export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
validatedExecutionArgs: ValidatedExecutionArgs,
): PromiseOrValue<ExecutionResult | ExperimentalIncrementalExecutionResults> {
Expand All @@ -326,6 +339,7 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
: undefined,
completed: false,
cancellableStreams: undefined,
errorPropagation: errorPropagation(validatedExecutionArgs.operation),
};
try {
const {
Expand Down Expand Up @@ -976,7 +990,7 @@ function handleFieldError(

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

Expand Down
13 changes: 13 additions & 0 deletions src/type/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,19 @@ export const GraphQLOneOfDirective: GraphQLDirective = new GraphQLDirective({
args: {},
});

/**
* Disables error propagation (experimental).
*/
export const GraphQLDisableErrorPropagationDirective = new GraphQLDirective({
name: 'experimental_disableErrorPropagation',
description: 'Disables error propagation.',
locations: [
DirectiveLocation.QUERY,
DirectiveLocation.MUTATION,
DirectiveLocation.SUBSCRIPTION,
],
});

/**
* The full list of specified directives.
*/
Expand Down

0 comments on commit f17d05a

Please sign in to comment.