From 46ac468f5cfeee8bd333a5493e02833e884c51d3 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 27 Mar 2025 17:33:45 +0000 Subject: [PATCH 01/15] Implement onError proposal --- src/error/ErrorBehavior.ts | 9 +++++ src/error/index.ts | 1 + src/execution/__tests__/executor-test.ts | 2 ++ src/execution/execute.ts | 45 ++++++++++++++++++++++-- src/graphql.ts | 13 +++++++ src/index.ts | 1 + src/type/definition.ts | 3 ++ 7 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 src/error/ErrorBehavior.ts diff --git a/src/error/ErrorBehavior.ts b/src/error/ErrorBehavior.ts new file mode 100644 index 0000000000..665f241905 --- /dev/null +++ b/src/error/ErrorBehavior.ts @@ -0,0 +1,9 @@ +export type GraphQLErrorBehavior = 'PROPAGATE' | 'NO_PROPAGATE' | 'ABORT'; + +export function isErrorBehavior( + onError: unknown, +): onError is GraphQLErrorBehavior { + return ( + onError === 'PROPAGATE' || onError === 'NO_PROPAGATE' || onError === 'ABORT' + ); +} diff --git a/src/error/index.ts b/src/error/index.ts index 7e5d267f50..b9da3e897e 100644 --- a/src/error/index.ts +++ b/src/error/index.ts @@ -9,3 +9,4 @@ export type { export { syntaxError } from './syntaxError'; export { locatedError } from './locatedError'; +export type { GraphQLErrorBehavior } from './ErrorBehavior'; diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 0f0c5b2861..60c70b3ba5 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -264,6 +264,7 @@ describe('Execute: Handles basic execution tasks', () => { 'rootValue', 'operation', 'variableValues', + 'errorBehavior', ); const operation = document.definitions[0]; @@ -276,6 +277,7 @@ describe('Execute: Handles basic execution tasks', () => { schema, rootValue, operation, + errorBehavior: 'PROPAGATE', }); const field = operation.selectionSet.selections[0]; diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 5cd64d40f9..fdf1616f09 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -13,6 +13,8 @@ import { promiseForObject } from '../jsutils/promiseForObject'; import type { PromiseOrValue } from '../jsutils/PromiseOrValue'; import { promiseReduce } from '../jsutils/promiseReduce'; +import type { GraphQLErrorBehavior } from '../error/ErrorBehavior'; +import { isErrorBehavior } from '../error/ErrorBehavior'; import type { GraphQLFormattedError } from '../error/GraphQLError'; import { GraphQLError } from '../error/GraphQLError'; import { locatedError } from '../error/locatedError'; @@ -115,6 +117,7 @@ export interface ExecutionContext { typeResolver: GraphQLTypeResolver; subscribeFieldResolver: GraphQLFieldResolver; errors: Array; + errorBehavior: GraphQLErrorBehavior; } /** @@ -130,6 +133,7 @@ export interface ExecutionResult< > { errors?: ReadonlyArray; data?: TData | null; + onError?: GraphQLErrorBehavior; extensions?: TExtensions; } @@ -152,6 +156,15 @@ export interface ExecutionArgs { fieldResolver?: Maybe>; typeResolver?: Maybe>; subscribeFieldResolver?: Maybe>; + /** + * Experimental. Set to NO_PROPAGATE to prevent error propagation. Set to ABORT to + * abort a request when any error occurs. + * + * Default: PROPAGATE + * + * @experimental + */ + onError?: GraphQLErrorBehavior; /** Additional execution options. */ options?: { /** Set the maximum number of errors allowed for coercing (defaults to 50). */ @@ -291,9 +304,18 @@ export function buildExecutionContext( fieldResolver, typeResolver, subscribeFieldResolver, + onError, options, } = args; + if (onError != null && !isErrorBehavior(onError)) { + return [ + new GraphQLError( + 'Unsupported `onError` value; supported values are `PROPAGATE`, `NO_PROPAGATE` and `ABORT`.', + ), + ]; + } + let operation: OperationDefinitionNode | undefined; const fragments: ObjMap = Object.create(null); for (const definition of document.definitions) { @@ -353,6 +375,7 @@ export function buildExecutionContext( typeResolver: typeResolver ?? defaultTypeResolver, subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, errors: [], + errorBehavior: onError ?? 'PROPAGATE', }; } @@ -591,6 +614,7 @@ export function buildResolveInfo( rootValue: exeContext.rootValue, operation: exeContext.operation, variableValues: exeContext.variableValues, + errorBehavior: exeContext.errorBehavior, }; } @@ -599,10 +623,25 @@ function handleFieldError( returnType: GraphQLOutputType, exeContext: ExecutionContext, ): null { - // 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.errorBehavior === 'PROPAGATE') { + // If the field type is non-nullable, then it is resolved without any + // protection from errors, however it still properly locates the error. + // Note: semantic non-null types are treated as nullable for the purposes + // of error handling. + if (isNonNullType(returnType)) { + throw error; + } + } else if (exeContext.errorBehavior === 'ABORT') { + // In this mode, any error aborts the request throw error; + } else if (exeContext.errorBehavior === 'NO_PROPAGATE') { + // In this mode, the client takes responsibility for error handling, so we + // treat the field as if it were nullable. + } else { + invariant( + false, + 'Unexpected errorBehavior setting: ' + inspect(exeContext.errorBehavior), + ); } // Otherwise, error protection is applied, logging the error and resolving diff --git a/src/graphql.ts b/src/graphql.ts index bc6fb9bb72..7edd260b83 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -3,6 +3,8 @@ import { isPromise } from './jsutils/isPromise'; import type { Maybe } from './jsutils/Maybe'; import type { PromiseOrValue } from './jsutils/PromiseOrValue'; +import type { GraphQLErrorBehavior } from './error/ErrorBehavior'; + import { parse } from './language/parser'; import type { Source } from './language/source'; @@ -66,6 +68,15 @@ export interface GraphQLArgs { operationName?: Maybe; fieldResolver?: Maybe>; typeResolver?: Maybe>; + /** + * Experimental. Set to NO_PROPAGATE to prevent error propagation. Set to ABORT to + * abort a request when any error occurs. + * + * Default: PROPAGATE + * + * @experimental + */ + onError?: GraphQLErrorBehavior; } export function graphql(args: GraphQLArgs): Promise { @@ -106,6 +117,7 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, + onError, } = args; // Validate Schema @@ -138,5 +150,6 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, + onError, }); } diff --git a/src/index.ts b/src/index.ts index 73c713a203..4df70d7d74 100644 --- a/src/index.ts +++ b/src/index.ts @@ -395,6 +395,7 @@ export { } from './error/index'; export type { + GraphQLErrorBehavior, GraphQLErrorOptions, GraphQLFormattedError, GraphQLErrorExtensions, diff --git a/src/type/definition.ts b/src/type/definition.ts index 7eaac560dc..61c57c4f38 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -14,6 +14,7 @@ import type { PromiseOrValue } from '../jsutils/PromiseOrValue'; import { suggestionList } from '../jsutils/suggestionList'; import { toObjMap } from '../jsutils/toObjMap'; +import type { GraphQLErrorBehavior } from '../error/ErrorBehavior'; import { GraphQLError } from '../error/GraphQLError'; import type { @@ -988,6 +989,8 @@ export interface GraphQLResolveInfo { readonly rootValue: unknown; readonly operation: OperationDefinitionNode; readonly variableValues: { [variable: string]: unknown }; + /** @experimental */ + readonly errorBehavior: GraphQLErrorBehavior; } /** From 0d89dd136da6b0efacce3dc9b9cc50e2e69bafad Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 27 Mar 2025 17:52:34 +0000 Subject: [PATCH 02/15] Add tests --- src/execution/__tests__/executor-test.ts | 192 +++++++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 60c70b3ba5..01c1a2d0b8 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -288,6 +288,70 @@ describe('Execute: Handles basic execution tasks', () => { }); }); + it('reflects onError:NO_PROPAGATE via errorBehavior', () => { + let resolvedInfo; + const testType = new GraphQLObjectType({ + name: 'Test', + fields: { + test: { + type: GraphQLString, + resolve(_val, _args, _ctx, info) { + resolvedInfo = info; + }, + }, + }, + }); + const schema = new GraphQLSchema({ query: testType }); + + const document = parse('query ($var: String) { result: test }'); + const rootValue = { root: 'val' }; + const variableValues = { var: 'abc' }; + + executeSync({ + schema, + document, + rootValue, + variableValues, + onError: 'NO_PROPAGATE', + }); + + expect(resolvedInfo).to.include({ + errorBehavior: 'NO_PROPAGATE', + }); + }); + + it('reflects onError:ABORT via errorBehavior', () => { + let resolvedInfo; + const testType = new GraphQLObjectType({ + name: 'Test', + fields: { + test: { + type: GraphQLString, + resolve(_val, _args, _ctx, info) { + resolvedInfo = info; + }, + }, + }, + }); + const schema = new GraphQLSchema({ query: testType }); + + const document = parse('query ($var: String) { result: test }'); + const rootValue = { root: 'val' }; + const variableValues = { var: 'abc' }; + + executeSync({ + schema, + document, + rootValue, + variableValues, + onError: 'ABORT', + }); + + expect(resolvedInfo).to.include({ + errorBehavior: 'ABORT', + }); + }); + it('populates path correctly with complex types', () => { let path; const someObject = new GraphQLObjectType({ @@ -742,6 +806,134 @@ describe('Execute: Handles basic execution tasks', () => { }); }); + it('Full response path is included for non-nullable fields with onError:NO_PROPAGATE', () => { + const A: GraphQLObjectType = new GraphQLObjectType({ + name: 'A', + fields: () => ({ + nullableA: { + type: A, + resolve: () => ({}), + }, + nonNullA: { + type: new GraphQLNonNull(A), + resolve: () => ({}), + }, + throws: { + type: new GraphQLNonNull(GraphQLString), + resolve: () => { + throw new Error('Catch me if you can'); + }, + }, + }), + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'query', + fields: () => ({ + nullableA: { + type: A, + resolve: () => ({}), + }, + }), + }), + }); + + const document = parse(` + query { + nullableA { + aliasedA: nullableA { + nonNullA { + anotherA: nonNullA { + throws + } + } + } + } + } + `); + + const result = executeSync({ schema, document, onError: 'NO_PROPAGATE' }); + expectJSON(result).toDeepEqual({ + data: { + nullableA: { + aliasedA: { + nonNullA: { + anotherA: { + throws: null, + }, + }, + }, + }, + }, + errors: [ + { + message: 'Catch me if you can', + locations: [{ line: 7, column: 17 }], + path: ['nullableA', 'aliasedA', 'nonNullA', 'anotherA', 'throws'], + }, + ], + }); + }); + + it('Full response path is included for non-nullable fields with onError:ABORT', () => { + const A: GraphQLObjectType = new GraphQLObjectType({ + name: 'A', + fields: () => ({ + nullableA: { + type: A, + resolve: () => ({}), + }, + nonNullA: { + type: new GraphQLNonNull(A), + resolve: () => ({}), + }, + throws: { + type: new GraphQLNonNull(GraphQLString), + resolve: () => { + throw new Error('Catch me if you can'); + }, + }, + }), + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'query', + fields: () => ({ + nullableA: { + type: A, + resolve: () => ({}), + }, + }), + }), + }); + + const document = parse(` + query { + nullableA { + aliasedA: nullableA { + nonNullA { + anotherA: nonNullA { + throws + } + } + } + } + } + `); + + const result = executeSync({ schema, document, onError: 'ABORT' }); + expectJSON(result).toDeepEqual({ + data: null, + errors: [ + { + message: 'Catch me if you can', + locations: [{ line: 7, column: 17 }], + path: ['nullableA', 'aliasedA', 'nonNullA', 'anotherA', 'throws'], + }, + ], + }); + }); + it('uses the inline operation if no operation name is provided', () => { const schema = new GraphQLSchema({ query: new GraphQLObjectType({ From 3381f98a000d9f84d4109ebc31ead5ca89a1c7f7 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 27 Mar 2025 17:58:07 +0000 Subject: [PATCH 03/15] Test invalid onError is handled --- src/execution/__tests__/executor-test.ts | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 01c1a2d0b8..bde56dbc38 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -934,6 +934,36 @@ describe('Execute: Handles basic execution tasks', () => { }); }); + it('raises request error with invalid onError', () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'query', + fields: () => ({ + a: { + type: GraphQLInt, + resolve: () => ({}), + }, + }), + }), + }); + + const document = parse('{ a }'); + const result = executeSync({ + schema, + document, + // @ts-expect-error + onError: 'DANCE', + }); + expectJSON(result).toDeepEqual({ + errors: [ + { + message: + 'Unsupported `onError` value; supported values are `PROPAGATE`, `NO_PROPAGATE` and `ABORT`.', + }, + ], + }); + }); + it('uses the inline operation if no operation name is provided', () => { const schema = new GraphQLSchema({ query: new GraphQLObjectType({ From 6bb45cf8f7efb1bee05a0cf512e695541a0f0d76 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 27 Mar 2025 18:02:32 +0000 Subject: [PATCH 04/15] Ignore invariant from code coverage --- src/execution/execute.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index fdf1616f09..057747e667 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -638,6 +638,7 @@ function handleFieldError( // In this mode, the client takes responsibility for error handling, so we // treat the field as if it were nullable. } else { + /* c8 ignore next 4 */ invariant( false, 'Unexpected errorBehavior setting: ' + inspect(exeContext.errorBehavior), From cdb365e6f48f97b62a0eba8c9e2a967c76b592cd Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 27 Mar 2025 18:03:44 +0000 Subject: [PATCH 05/15] Finickity --- src/execution/execute.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 057747e667..60044a4420 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -638,7 +638,7 @@ function handleFieldError( // In this mode, the client takes responsibility for error handling, so we // treat the field as if it were nullable. } else { - /* c8 ignore next 4 */ + /* c8 ignore next 5 */ invariant( false, 'Unexpected errorBehavior setting: ' + inspect(exeContext.errorBehavior), From 0ff06c344ec445b9330fc6ac55046b65f9716ccb Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 27 Mar 2025 18:06:20 +0000 Subject: [PATCH 06/15] Urghhhhhh --- src/execution/execute.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 60044a4420..7026b207f2 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -637,8 +637,8 @@ function handleFieldError( } else if (exeContext.errorBehavior === 'NO_PROPAGATE') { // In this mode, the client takes responsibility for error handling, so we // treat the field as if it were nullable. + /* c8 ignore next 6 */ } else { - /* c8 ignore next 5 */ invariant( false, 'Unexpected errorBehavior setting: ' + inspect(exeContext.errorBehavior), From 34cb2cf8836b1904282028d72e9ef56e2ebc6ef0 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 27 Mar 2025 18:09:25 +0000 Subject: [PATCH 07/15] Remove unnecessary resolver causing coverage issue --- src/execution/__tests__/executor-test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index bde56dbc38..f0ace0c136 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -941,7 +941,6 @@ describe('Execute: Handles basic execution tasks', () => { fields: () => ({ a: { type: GraphQLInt, - resolve: () => ({}), }, }), }), From a5474f455250524709b19cf1e48e2193a9d750f7 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 30 Apr 2025 12:51:01 +0100 Subject: [PATCH 08/15] Reorder so NO_PROPAGATE is first --- src/error/ErrorBehavior.ts | 4 ++-- src/execution/__tests__/executor-test.ts | 2 +- src/execution/execute.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/error/ErrorBehavior.ts b/src/error/ErrorBehavior.ts index 665f241905..44495318ca 100644 --- a/src/error/ErrorBehavior.ts +++ b/src/error/ErrorBehavior.ts @@ -1,9 +1,9 @@ -export type GraphQLErrorBehavior = 'PROPAGATE' | 'NO_PROPAGATE' | 'ABORT'; +export type GraphQLErrorBehavior = 'NO_PROPAGATE' | 'PROPAGATE' | 'ABORT'; export function isErrorBehavior( onError: unknown, ): onError is GraphQLErrorBehavior { return ( - onError === 'PROPAGATE' || onError === 'NO_PROPAGATE' || onError === 'ABORT' + onError === 'NO_PROPAGATE' || onError === 'PROPAGATE' || onError === 'ABORT' ); } diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index f0ace0c136..86b4be0d9d 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -957,7 +957,7 @@ describe('Execute: Handles basic execution tasks', () => { errors: [ { message: - 'Unsupported `onError` value; supported values are `PROPAGATE`, `NO_PROPAGATE` and `ABORT`.', + 'Unsupported `onError` value; supported values are `NO_PROPAGATE`, `PROPAGATE` and `ABORT`.', }, ], }); diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 7026b207f2..89ee8ec461 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -311,7 +311,7 @@ export function buildExecutionContext( if (onError != null && !isErrorBehavior(onError)) { return [ new GraphQLError( - 'Unsupported `onError` value; supported values are `PROPAGATE`, `NO_PROPAGATE` and `ABORT`.', + 'Unsupported `onError` value; supported values are `NO_PROPAGATE`, `PROPAGATE` and `ABORT`.', ), ]; } From 252abf177386feaa17636c680e0f6e72d32e3bc4 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 30 Apr 2025 13:00:42 +0100 Subject: [PATCH 09/15] Allow the schema to set a default error behavior --- src/execution/execute.ts | 2 +- src/type/schema.ts | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 89ee8ec461..9ec4509f1a 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -375,7 +375,7 @@ export function buildExecutionContext( typeResolver: typeResolver ?? defaultTypeResolver, subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, errors: [], - errorBehavior: onError ?? 'PROPAGATE', + errorBehavior: onError ?? schema.defaultErrorBehavior, }; } diff --git a/src/type/schema.ts b/src/type/schema.ts index 97c2782145..3681e66578 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -31,6 +31,8 @@ import { import type { GraphQLDirective } from './directives'; import { isDirective, specifiedDirectives } from './directives'; import { __Schema } from './introspection'; +import type { GraphQLErrorBehavior } from '../error'; +import { isErrorBehavior } from '../error/ErrorBehavior'; /** * Test if the given value is a GraphQL schema. @@ -129,6 +131,8 @@ export interface GraphQLSchemaExtensions { */ export class GraphQLSchema { description: Maybe; + /** @experimental */ + readonly defaultErrorBehavior: GraphQLErrorBehavior; extensions: Readonly; astNode: Maybe; extensionASTNodes: ReadonlyArray; @@ -163,8 +167,15 @@ export class GraphQLSchema { '"directives" must be Array if provided but got: ' + `${inspect(config.directives)}.`, ); + devAssert( + !config.defaultErrorBehavior || + isErrorBehavior(config.defaultErrorBehavior), + '"defaultErrorBehavior" must be one of "NO_PROPAGATE", "PROPAGATE" or "ABORT", but got: ' + + `${inspect(config.defaultErrorBehavior)}.`, + ); this.description = config.description; + this.defaultErrorBehavior = config.defaultErrorBehavior ?? 'PROPAGATE'; this.extensions = toObjMap(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes ?? []; @@ -386,6 +397,20 @@ export interface GraphQLSchemaConfig extends GraphQLSchemaValidationOptions { subscription?: Maybe; types?: Maybe>; directives?: Maybe>; + /** + * Experimental. Defines the default GraphQL error behavior when the + * GraphQLArgs does not include an `onError` property. + * + * Set to NO_PROPAGATE if your schema only needs to support modern + * "error-handling" clients. + * + * It is not recommended to set this to ABORT. + * + * Default: PROPAGATE + * + * @experimental + */ + defaultErrorBehavior?: GraphQLErrorBehavior; extensions?: Maybe>; astNode?: Maybe; extensionASTNodes?: Maybe>; From f945573b91228f070162c8d595aac21400d824d8 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 30 Apr 2025 13:07:11 +0100 Subject: [PATCH 10/15] Started work on introspection --- src/type/__tests__/introspection-test.ts | 58 ++++++++++++++++++++++++ src/utilities/getIntrospectionQuery.ts | 11 +++++ 2 files changed, 69 insertions(+) diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index 8c5cacba0d..d7bd11cf95 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -26,6 +26,7 @@ describe('Introspection', () => { descriptions: false, specifiedByUrl: true, directiveIsRepeatable: true, + errorBehavior: true, }); const result = graphqlSync({ schema, source }); @@ -35,6 +36,7 @@ describe('Introspection', () => { queryType: { name: 'SomeObject', kind: 'OBJECT' }, mutationType: null, subscriptionType: null, + defaultErrorBehavior: 'PROPAGATE', types: [ { kind: 'OBJECT', @@ -1754,4 +1756,60 @@ describe('Introspection', () => { }); expect(result).to.not.have.property('errors'); }); + + it('reflects the default error behavior (default)', () => { + const schema = buildSchema(` + type SomeObject { + someField: String + } + + schema { + query: SomeObject + } + `); + + const source = getIntrospectionQuery({ + descriptions: false, + specifiedByUrl: true, + directiveIsRepeatable: true, + }); + + const result = graphqlSync({ schema, source }); + expect(result).to.deep.equal({ + data: { + __schema: { + defaultErrorBehavior: 'PROPAGATE', + }, + }, + }); + }); + + it('reflects the default error behavior (NO_PROPAGATE)', () => { + const schema = buildSchema(` + type SomeObject { + someField: String + } + + schema @behavior(onError: NO_PROPAGATE) { + query: SomeObject + } + `); + + const source = /* GraphQL */ ` + { + __schema { + defaultErrorBehavior + } + } + `; + + const result = graphqlSync({ schema, source }); + expect(result).to.deep.equal({ + data: { + __schema: { + defaultErrorBehavior: 'NO_PROPAGATE', + }, + }, + }); + }); }); diff --git a/src/utilities/getIntrospectionQuery.ts b/src/utilities/getIntrospectionQuery.ts index 373b474ed5..0bf9692e10 100644 --- a/src/utilities/getIntrospectionQuery.ts +++ b/src/utilities/getIntrospectionQuery.ts @@ -38,6 +38,12 @@ export interface IntrospectionOptions { * Default: false */ oneOf?: boolean; + + /** + * Whether target GraphQL server supports changing error behaviors. + * Default: false + */ + errorBehavior?: boolean; } /** @@ -52,6 +58,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { schemaDescription: false, inputValueDeprecation: false, oneOf: false, + errorBehavior: false, ...options, }; @@ -65,6 +72,9 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { const schemaDescription = optionsWithDefault.schemaDescription ? descriptions : ''; + const defaultErrorBehavior = optionsWithDefault.errorBehavior + ? 'defaultErrorBehavior' + : ''; function inputDeprecation(str: string) { return optionsWithDefault.inputValueDeprecation ? str : ''; @@ -78,6 +88,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { queryType { name kind } mutationType { name kind } subscriptionType { name kind } + ${defaultErrorBehavior} types { ...FullType } From 775271bc668fb12838cbc237ce7c2e1f0ad92a56 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 30 Apr 2025 14:15:19 +0100 Subject: [PATCH 11/15] Integrate into introspection --- src/__tests__/starWarsIntrospection-test.ts | 1 + src/index.ts | 1 + src/type/__tests__/introspection-test.ts | 73 +++++++++++++++++-- src/type/__tests__/schema-test.ts | 1 + src/type/directives.ts | 17 +++++ src/type/index.ts | 1 + src/type/introspection.ts | 30 ++++++++ .../__tests__/buildASTSchema-test.ts | 15 +++- .../__tests__/findBreakingChanges-test.ts | 2 + src/utilities/__tests__/printSchema-test.ts | 26 +++++++ src/utilities/extendSchema.ts | 22 ++++++ src/utilities/printSchema.ts | 15 +++- 12 files changed, 193 insertions(+), 11 deletions(-) diff --git a/src/__tests__/starWarsIntrospection-test.ts b/src/__tests__/starWarsIntrospection-test.ts index d637787c4a..8f453b01d4 100644 --- a/src/__tests__/starWarsIntrospection-test.ts +++ b/src/__tests__/starWarsIntrospection-test.ts @@ -37,6 +37,7 @@ describe('Star Wars Introspection Tests', () => { { name: 'Droid' }, { name: 'Query' }, { name: 'Boolean' }, + { name: '__ErrorBehavior' }, { name: '__Schema' }, { name: '__Type' }, { name: '__TypeKind' }, diff --git a/src/index.ts b/src/index.ts index 4df70d7d74..93f544ddc9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -79,6 +79,7 @@ export { __InputValue, __EnumValue, __TypeKind, + __ErrorBehavior, // Meta-field definitions. SchemaMetaFieldDef, TypeMetaFieldDef, diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index d7bd11cf95..d6913c5440 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -80,6 +80,32 @@ describe('Introspection', () => { enumValues: null, possibleTypes: null, }, + { + enumValues: [ + { + deprecationReason: null, + isDeprecated: false, + name: 'NO_PROPAGATE', + }, + { + deprecationReason: null, + isDeprecated: false, + name: 'PROPAGATE', + }, + { + deprecationReason: null, + isDeprecated: false, + name: 'ABORT', + }, + ], + fields: null, + inputFields: null, + interfaces: null, + kind: 'ENUM', + name: '__ErrorBehavior', + possibleTypes: null, + specifiedByURL: null, + }, { kind: 'OBJECT', name: '__Schema', @@ -179,6 +205,21 @@ describe('Introspection', () => { isDeprecated: false, deprecationReason: null, }, + { + name: 'defaultErrorBehavior', + args: [], + type: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'ENUM', + name: '__ErrorBehavior', + ofType: null, + }, + }, + isDeprecated: false, + deprecationReason: null, + }, ], inputFields: null, interfaces: [], @@ -1008,6 +1049,26 @@ describe('Introspection', () => { locations: ['INPUT_OBJECT'], args: [], }, + { + args: [ + { + defaultValue: 'PROPAGATE', + name: 'onError', + type: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'ENUM', + name: '__ErrorBehavior', + ofType: null, + }, + }, + }, + ], + isRepeatable: false, + locations: ['SCHEMA'], + name: 'behavior', + }, ], }, }, @@ -1768,11 +1829,13 @@ describe('Introspection', () => { } `); - const source = getIntrospectionQuery({ - descriptions: false, - specifiedByUrl: true, - directiveIsRepeatable: true, - }); + const source = /* GraphQL */ ` + { + __schema { + defaultErrorBehavior + } + } + `; const result = graphqlSync({ schema, source }); expect(result).to.deep.equal({ diff --git a/src/type/__tests__/schema-test.ts b/src/type/__tests__/schema-test.ts index 8a31b50ada..8878e545e2 100644 --- a/src/type/__tests__/schema-test.ts +++ b/src/type/__tests__/schema-test.ts @@ -296,6 +296,7 @@ describe('Type System: Schema', () => { 'ASub', 'Boolean', 'String', + '__ErrorBehavior', '__Schema', '__Type', '__TypeKind', diff --git a/src/type/directives.ts b/src/type/directives.ts index 6881f20532..a64be1f76e 100644 --- a/src/type/directives.ts +++ b/src/type/directives.ts @@ -18,6 +18,7 @@ import { defineArguments, GraphQLNonNull, } from './definition'; +import { __ErrorBehavior } from './introspection'; import { GraphQLBoolean, GraphQLString } from './scalars'; /** @@ -220,6 +221,21 @@ export const GraphQLOneOfDirective: GraphQLDirective = new GraphQLDirective({ args: {}, }); +/** + * Used to indicate the default error behavior. + */ +export const GraphQLBehaviorDirective: GraphQLDirective = new GraphQLDirective({ + name: 'behavior', + description: 'Indicates the default error behavior of the schema.', + locations: [DirectiveLocation.SCHEMA], + args: { + onError: { + type: new GraphQLNonNull(__ErrorBehavior), + defaultValue: 'PROPAGATE', + }, + }, +}); + /** * The full list of specified directives. */ @@ -230,6 +246,7 @@ export const specifiedDirectives: ReadonlyArray = GraphQLDeprecatedDirective, GraphQLSpecifiedByDirective, GraphQLOneOfDirective, + GraphQLBehaviorDirective, ]); export function isSpecifiedDirective(directive: GraphQLDirective): boolean { diff --git a/src/type/index.ts b/src/type/index.ts index cf276d1e02..6d9870a016 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -172,6 +172,7 @@ export { __InputValue, __EnumValue, __TypeKind, + __ErrorBehavior, // "Enum" of Type Kinds TypeKind, // Meta-field definitions. diff --git a/src/type/introspection.ts b/src/type/introspection.ts index 2c66ca5098..c176e7175c 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -74,6 +74,12 @@ export const __Schema: GraphQLObjectType = new GraphQLObjectType({ ), resolve: (schema) => schema.getDirectives(), }, + defaultErrorBehavior: { + description: + 'The default error behavior that will be used for requests which do not specify `onError`.', + type: new GraphQLNonNull(__ErrorBehavior), + resolve: (schema) => schema.defaultErrorBehavior, + }, } as GraphQLFieldConfigMap), }); @@ -500,6 +506,29 @@ export const __TypeKind: GraphQLEnumType = new GraphQLEnumType({ }, }); +export const __ErrorBehavior: GraphQLEnumType = new GraphQLEnumType({ + name: '__ErrorBehavior', + description: + 'An enum detailing the error behavior a GraphQL request should use.', + values: { + NO_PROPAGATE: { + value: 'NO_PROPAGATE', + description: + 'Indicates that an error should result in the response position becoming null, even if it is marked as non-null.', + }, + PROPAGATE: { + value: 'PROPAGATE', + description: + 'Indicates that an error that occurs in a non-null position should propagate to the nearest nullable response position.', + }, + ABORT: { + value: 'ABORT', + description: + 'Indicates execution should cease when the first error occurs, and that the response data should be null.', + }, + }, +}); + /** * Note that these are GraphQLField and not GraphQLFieldConfig, * so the format for args is different. @@ -558,6 +587,7 @@ export const introspectionTypes: ReadonlyArray = __InputValue, __EnumValue, __TypeKind, + __ErrorBehavior, ]); export function isIntrospectionType(type: GraphQLNamedType): boolean { diff --git a/src/utilities/__tests__/buildASTSchema-test.ts b/src/utilities/__tests__/buildASTSchema-test.ts index 29280474ec..06cded5816 100644 --- a/src/utilities/__tests__/buildASTSchema-test.ts +++ b/src/utilities/__tests__/buildASTSchema-test.ts @@ -21,6 +21,7 @@ import { } from '../../type/definition'; import { assertDirective, + GraphQLBehaviorDirective, GraphQLDeprecatedDirective, GraphQLIncludeDirective, GraphQLOneOfDirective, @@ -223,7 +224,7 @@ describe('Schema Builder', () => { it('Maintains @include, @skip & @specifiedBy', () => { const schema = buildSchema('type Query'); - expect(schema.getDirectives()).to.have.lengthOf(5); + expect(schema.getDirectives()).to.have.lengthOf(6); expect(schema.getDirective('skip')).to.equal(GraphQLSkipDirective); expect(schema.getDirective('include')).to.equal(GraphQLIncludeDirective); expect(schema.getDirective('deprecated')).to.equal( @@ -232,6 +233,7 @@ describe('Schema Builder', () => { expect(schema.getDirective('specifiedBy')).to.equal( GraphQLSpecifiedByDirective, ); + expect(schema.getDirective('behavior')).to.equal(GraphQLBehaviorDirective); expect(schema.getDirective('oneOf')).to.equal(GraphQLOneOfDirective); }); @@ -241,10 +243,11 @@ describe('Schema Builder', () => { directive @include on FIELD directive @deprecated on FIELD_DEFINITION directive @specifiedBy on FIELD_DEFINITION + directive @behavior on SCHEMA directive @oneOf on OBJECT `); - expect(schema.getDirectives()).to.have.lengthOf(5); + expect(schema.getDirectives()).to.have.lengthOf(6); expect(schema.getDirective('skip')).to.not.equal(GraphQLSkipDirective); expect(schema.getDirective('include')).to.not.equal( GraphQLIncludeDirective, @@ -255,19 +258,23 @@ describe('Schema Builder', () => { expect(schema.getDirective('specifiedBy')).to.not.equal( GraphQLSpecifiedByDirective, ); + expect(schema.getDirective('behavior')).to.not.equal( + GraphQLBehaviorDirective, + ); expect(schema.getDirective('oneOf')).to.not.equal(GraphQLOneOfDirective); }); - it('Adding directives maintains @include, @skip, @deprecated, @specifiedBy, and @oneOf', () => { + it('Adding directives maintains @include, @skip, @deprecated, @specifiedBy, @behavior and @oneOf', () => { const schema = buildSchema(` directive @foo(arg: Int) on FIELD `); - expect(schema.getDirectives()).to.have.lengthOf(6); + expect(schema.getDirectives()).to.have.lengthOf(7); expect(schema.getDirective('skip')).to.not.equal(undefined); expect(schema.getDirective('include')).to.not.equal(undefined); expect(schema.getDirective('deprecated')).to.not.equal(undefined); expect(schema.getDirective('specifiedBy')).to.not.equal(undefined); + expect(schema.getDirective('behavior')).to.not.equal(undefined); expect(schema.getDirective('oneOf')).to.not.equal(undefined); }); diff --git a/src/utilities/__tests__/findBreakingChanges-test.ts b/src/utilities/__tests__/findBreakingChanges-test.ts index ba526deb48..135b50b411 100644 --- a/src/utilities/__tests__/findBreakingChanges-test.ts +++ b/src/utilities/__tests__/findBreakingChanges-test.ts @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import { + GraphQLBehaviorDirective, GraphQLDeprecatedDirective, GraphQLIncludeDirective, GraphQLOneOfDirective, @@ -803,6 +804,7 @@ describe('findBreakingChanges', () => { GraphQLSkipDirective, GraphQLIncludeDirective, GraphQLSpecifiedByDirective, + GraphQLBehaviorDirective, GraphQLOneOfDirective, ], }); diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts index 37af4a60f7..c6560cae2c 100644 --- a/src/utilities/__tests__/printSchema-test.ts +++ b/src/utilities/__tests__/printSchema-test.ts @@ -694,6 +694,27 @@ describe('Type System Printer', () => { """ directive @oneOf on INPUT_OBJECT + """Indicates the default error behavior of the schema.""" + directive @behavior(onError: __ErrorBehavior! = PROPAGATE) on SCHEMA + + """An enum detailing the error behavior a GraphQL request should use.""" + enum __ErrorBehavior { + """ + Indicates that an error should result in the response position becoming null, even if it is marked as non-null. + """ + NO_PROPAGATE + + """ + Indicates that an error that occurs in a non-null position should propagate to the nearest nullable response position. + """ + PROPAGATE + + """ + Indicates execution should cease when the first error occurs, and that the response data should be null. + """ + ABORT + } + """ A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations. """ @@ -718,6 +739,11 @@ describe('Type System Printer', () => { """A list of all directives supported by this server.""" directives: [__Directive!]! + + """ + The default error behavior that will be used for requests which do not specify \`onError\`. + """ + defaultErrorBehavior: __ErrorBehavior! } """ diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index d53752d919..f5cc50001f 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -64,6 +64,7 @@ import { isUnionType, } from '../type/definition'; import { + GraphQLBehaviorDirective, GraphQLDeprecatedDirective, GraphQLDirective, GraphQLOneOfDirective, @@ -82,6 +83,7 @@ import { assertValidSDLExtension } from '../validation/validate'; import { getDirectiveValues } from '../execution/values'; import { valueFromAST } from './valueFromAST'; +import type { GraphQLErrorBehavior } from '../error'; interface Options extends GraphQLSchemaValidationOptions { /** @@ -165,6 +167,14 @@ export function extendSchemaImpl( } } + let defaultErrorBehavior: Maybe = schemaDef + ? getDefaultErrorBehavior(schemaDef) + : null; + for (const extensionNode of schemaExtensions) { + defaultErrorBehavior = + getDefaultErrorBehavior(extensionNode) ?? defaultErrorBehavior; + } + // If this document contains no new types, extensions, or directives then // return the same unmodified GraphQLSchema instance. if ( @@ -201,6 +211,7 @@ export function extendSchemaImpl( // Then produce and return a Schema config with these types. return { description: schemaDef?.description?.value, + defaultErrorBehavior, ...operationTypes, types: Object.values(typeMap), directives: [ @@ -691,3 +702,14 @@ function getSpecifiedByURL( function isOneOf(node: InputObjectTypeDefinitionNode): boolean { return Boolean(getDirectiveValues(GraphQLOneOfDirective, node)); } + +/** + * Given a schema node, returns the GraphQL error behavior from the `@behavior(onError:)` argument. + */ +function getDefaultErrorBehavior( + node: SchemaDefinitionNode | SchemaExtensionNode, +): Maybe { + const behavior = getDirectiveValues(GraphQLBehaviorDirective, node); + // @ts-expect-error validated by `getDirectiveValues` + return behavior?.onError; +} diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts index edac6262c5..f248b9a367 100644 --- a/src/utilities/printSchema.ts +++ b/src/utilities/printSchema.ts @@ -70,7 +70,11 @@ function printFilteredSchema( } function printSchemaDefinition(schema: GraphQLSchema): Maybe { - if (schema.description == null && isSchemaOfCommonNames(schema)) { + if ( + schema.description == null && + schema.defaultErrorBehavior === 'PROPAGATE' && + isSchemaOfCommonNames(schema) + ) { return; } @@ -90,8 +94,15 @@ function printSchemaDefinition(schema: GraphQLSchema): Maybe { if (subscriptionType) { operationTypes.push(` subscription: ${subscriptionType.name}`); } + const directives = + schema.defaultErrorBehavior !== 'PROPAGATE' + ? `@behavior(onError: ${schema.defaultErrorBehavior}) ` + : ``; - return printDescription(schema) + `schema {\n${operationTypes.join('\n')}\n}`; + return ( + printDescription(schema) + + `schema ${directives}{\n${operationTypes.join('\n')}\n}` + ); } /** From a90f2a261921f398970c9e4eb0810aaa70c8216f Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 30 Apr 2025 14:27:24 +0100 Subject: [PATCH 12/15] Lint --- src/type/schema.ts | 4 ++-- src/utilities/extendSchema.ts | 3 ++- src/utilities/printSchema.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/type/schema.ts b/src/type/schema.ts index 3681e66578..8e431d6c52 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -6,6 +6,8 @@ import type { Maybe } from '../jsutils/Maybe'; import type { ObjMap } from '../jsutils/ObjMap'; import { toObjMap } from '../jsutils/toObjMap'; +import type { GraphQLErrorBehavior } from '../error/ErrorBehavior'; +import { isErrorBehavior } from '../error/ErrorBehavior'; import type { GraphQLError } from '../error/GraphQLError'; import type { @@ -31,8 +33,6 @@ import { import type { GraphQLDirective } from './directives'; import { isDirective, specifiedDirectives } from './directives'; import { __Schema } from './introspection'; -import type { GraphQLErrorBehavior } from '../error'; -import { isErrorBehavior } from '../error/ErrorBehavior'; /** * Test if the given value is a GraphQL schema. diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index f5cc50001f..65a0414a5e 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -5,6 +5,8 @@ import { keyMap } from '../jsutils/keyMap'; import { mapValue } from '../jsutils/mapValue'; import type { Maybe } from '../jsutils/Maybe'; +import type { GraphQLErrorBehavior } from '../error/ErrorBehavior'; + import type { DirectiveDefinitionNode, DocumentNode, @@ -83,7 +85,6 @@ import { assertValidSDLExtension } from '../validation/validate'; import { getDirectiveValues } from '../execution/values'; import { valueFromAST } from './valueFromAST'; -import type { GraphQLErrorBehavior } from '../error'; interface Options extends GraphQLSchemaValidationOptions { /** diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts index f248b9a367..88e7e0fc19 100644 --- a/src/utilities/printSchema.ts +++ b/src/utilities/printSchema.ts @@ -97,7 +97,7 @@ function printSchemaDefinition(schema: GraphQLSchema): Maybe { const directives = schema.defaultErrorBehavior !== 'PROPAGATE' ? `@behavior(onError: ${schema.defaultErrorBehavior}) ` - : ``; + : ''; return ( printDescription(schema) + From 34d0991d5e206521a4b119fa8c951dc2b7b08ed9 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 30 Apr 2025 14:32:59 +0100 Subject: [PATCH 13/15] Fix TypeScript issue --- src/type/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/type/schema.ts b/src/type/schema.ts index 8e431d6c52..632855f637 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -410,7 +410,7 @@ export interface GraphQLSchemaConfig extends GraphQLSchemaValidationOptions { * * @experimental */ - defaultErrorBehavior?: GraphQLErrorBehavior; + defaultErrorBehavior?: Maybe; extensions?: Maybe>; astNode?: Maybe; extensionASTNodes?: Maybe>; From 64a9162d8de1cd4ba961a27cab6710e4e44b09f7 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 30 Apr 2025 14:36:46 +0100 Subject: [PATCH 14/15] Add missing test --- src/utilities/__tests__/printSchema-test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts index c6560cae2c..ad6463bd29 100644 --- a/src/utilities/__tests__/printSchema-test.ts +++ b/src/utilities/__tests__/printSchema-test.ts @@ -276,6 +276,21 @@ describe('Type System Printer', () => { `); }); + it('Prints schema with NO_PROPAGATE error behavior', () => { + const schema = new GraphQLSchema({ + defaultErrorBehavior: 'NO_PROPAGATE', + query: new GraphQLObjectType({ name: 'Query', fields: {} }), + }); + + expectPrintedSchema(schema).to.equal(dedent` + schema @behavior(onError: NO_PROPAGATE) { + query: Query + } + + type Query + `); + }); + it('Omits schema of common names', () => { const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', fields: {} }), From 0f0a31c85a88a4abd01416b717e1347a5fab2fca Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 1 May 2025 08:54:09 +0100 Subject: [PATCH 15/15] Reflect defaultErrorBehavior in buildClientSchema --- .../__tests__/buildClientSchema-test.ts | 21 +++++++++++++++++++ src/utilities/buildClientSchema.ts | 3 +++ src/utilities/getIntrospectionQuery.ts | 3 +++ 3 files changed, 27 insertions(+) diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts index e8cf046921..bb2bfa7e22 100644 --- a/src/utilities/__tests__/buildClientSchema-test.ts +++ b/src/utilities/__tests__/buildClientSchema-test.ts @@ -18,6 +18,7 @@ import { GraphQLString, } from '../../type/scalars'; import { GraphQLSchema } from '../../type/schema'; +import { validateSchema } from '../../type/validate'; import { graphqlSync } from '../../graphql'; @@ -158,6 +159,26 @@ describe('Type System: build schema from introspection', () => { expect(clientSchema.getType('ID')).to.equal(undefined); }); + it('reflects defaultErrorBehavior', () => { + const schema = buildSchema(` + schema @behavior(onError: NO_PROPAGATE) { + query: Query + } + type Query { + foo: String + } + `); + const introspection = introspectionFromSchema(schema, { + errorBehavior: true, + }); + const clientSchema = buildClientSchema(introspection); + + expect(clientSchema.defaultErrorBehavior).to.equal('NO_PROPAGATE'); + + const errors = validateSchema(clientSchema); + expect(errors).to.have.length(0); + }); + it('builds a schema with a recursive type reference', () => { const sdl = dedent` schema { diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index 83f6abada8..cc216af461 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -108,6 +108,8 @@ export function buildClientSchema( ? schemaIntrospection.directives.map(buildDirective) : []; + const defaultErrorBehavior = schemaIntrospection.defaultErrorBehavior; + // Then produce and return a Schema with these types. return new GraphQLSchema({ description: schemaIntrospection.description, @@ -116,6 +118,7 @@ export function buildClientSchema( subscription: subscriptionType, types: Object.values(typeMap), directives, + defaultErrorBehavior, assumeValid: options?.assumeValid, }); diff --git a/src/utilities/getIntrospectionQuery.ts b/src/utilities/getIntrospectionQuery.ts index 0bf9692e10..38d8e2c1d2 100644 --- a/src/utilities/getIntrospectionQuery.ts +++ b/src/utilities/getIntrospectionQuery.ts @@ -1,5 +1,7 @@ import type { Maybe } from '../jsutils/Maybe'; +import type { GraphQLErrorBehavior } from '../error/ErrorBehavior'; + import type { DirectiveLocation } from '../language/directiveLocation'; export interface IntrospectionOptions { @@ -206,6 +208,7 @@ export interface IntrospectionSchema { >; readonly types: ReadonlyArray; readonly directives: ReadonlyArray; + readonly defaultErrorBehavior?: Maybe; } export type IntrospectionType =