From a0ada52ca0ef57c76cf87b2a4004e8a540773944 Mon Sep 17 00:00:00 2001 From: Eugene Formanenko Date: Wed, 8 Jan 2025 15:17:22 +0400 Subject: [PATCH 1/2] feat: add validationMaxErrors option --- .changeset/validation-max-errors.md | 13 +++++ packages/server/src/ApolloServer.ts | 2 + .../server/src/__tests__/ApolloServer.test.ts | 4 +- .../server/src/__tests__/runQuery.test.ts | 48 +++++++++++++++++++ .../server/src/externalTypes/constructor.ts | 1 + packages/server/src/requestPipeline.ts | 1 + 6 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 .changeset/validation-max-errors.md diff --git a/.changeset/validation-max-errors.md b/.changeset/validation-max-errors.md new file mode 100644 index 00000000000..40bd1b03889 --- /dev/null +++ b/.changeset/validation-max-errors.md @@ -0,0 +1,13 @@ +--- +'@apollo/server': minor +--- + +Allowing to configure the maximum number of errors when validating a request. +Validation will be aborted if the limit is exceeded. + +``` +const server = new ApolloServer({ + typeDefs, + resolvers, + validationMaxErrors: 10, +}); diff --git a/packages/server/src/ApolloServer.ts b/packages/server/src/ApolloServer.ts index 4a9c8d97c9f..967a4b1d5d5 100644 --- a/packages/server/src/ApolloServer.ts +++ b/packages/server/src/ApolloServer.ts @@ -156,6 +156,7 @@ export interface ApolloServerInternals { state: ServerState; gatewayExecutor: GatewayExecutor | null; dangerouslyDisableValidation?: boolean; + validationMaxErrors?: number; formatError?: ( formattedError: GraphQLFormattedError, error: unknown, @@ -304,6 +305,7 @@ export class ApolloServer { hideSchemaDetailsFromClientErrors, dangerouslyDisableValidation: config.dangerouslyDisableValidation ?? false, + validationMaxErrors: config.validationMaxErrors, fieldResolver: config.fieldResolver, includeStacktraceInErrorResponses: config.includeStacktraceInErrorResponses ?? diff --git a/packages/server/src/__tests__/ApolloServer.test.ts b/packages/server/src/__tests__/ApolloServer.test.ts index 460a7d5cc76..089e0c0a1c6 100644 --- a/packages/server/src/__tests__/ApolloServer.test.ts +++ b/packages/server/src/__tests__/ApolloServer.test.ts @@ -425,7 +425,9 @@ describe('ApolloServer start', () => { }); }); -function singleResult(body: GraphQLResponseBody): FormattedExecutionResult { +export function singleResult( + body: GraphQLResponseBody, +): FormattedExecutionResult { if (body.kind === 'single') { return body.singleResult; } diff --git a/packages/server/src/__tests__/runQuery.test.ts b/packages/server/src/__tests__/runQuery.test.ts index 854bddfc4e9..8f09b8db544 100644 --- a/packages/server/src/__tests__/runQuery.test.ts +++ b/packages/server/src/__tests__/runQuery.test.ts @@ -25,6 +25,7 @@ import { } from '..'; import { mockLogger } from './mockLogger'; import { jest, describe, it, expect } from '@jest/globals'; +import { singleResult } from './ApolloServer.test'; async function runQuery( config: ApolloServerOptions, @@ -1192,4 +1193,51 @@ describe('parsing and validation cache', () => { expect(parsingDidStart.mock.calls.length).toBe(6); expect(validationDidStart.mock.calls.length).toBe(6); }); + + describe('validationMaxErrors option', () => { + it('should be 100 by default', async () => { + const server = new ApolloServer({ + schema, + }); + await server.start(); + + const vars = new Array(1000).fill(`$a:a`).join(','); + const query = `query aaa (${vars}) { a }`; + + const res = await server.executeOperation({ query }); + expect(res.http.status).toBe(400); + + const body = singleResult(res.body); + + // 100 by default plus one "Too many validation errors" error + // https://github.com/graphql/graphql-js/blob/main/src/validation/validate.ts#L46 + expect(body.errors).toHaveLength(101); + await server.stop(); + }); + it('aborts the validation if max errors more than expected', async () => { + const server = new ApolloServer({ + schema, + validationMaxErrors: 1, + }); + await server.start(); + + const vars = new Array(1000).fill(`$a:a`).join(','); + const query = `query aaa (${vars}) { a }`; + + const res = await server.executeOperation({ query }); + expect(res.http.status).toBe(400); + + const body = singleResult(res.body); + + expect(body.errors).toHaveLength(2); + expect(body.errors?.[0]).toMatchObject({ + message: `There can be only one variable named "$a".`, + }); + expect(body.errors?.[1]).toMatchObject({ + message: `Too many validation errors, error limit reached. Validation aborted.`, + }); + + await server.stop(); + }); + }); }); diff --git a/packages/server/src/externalTypes/constructor.ts b/packages/server/src/externalTypes/constructor.ts index 6d6f354f655..9efea5e88fb 100644 --- a/packages/server/src/externalTypes/constructor.ts +++ b/packages/server/src/externalTypes/constructor.ts @@ -101,6 +101,7 @@ interface ApolloServerOptionsBase { nodeEnv?: string; documentStore?: DocumentStore | null; dangerouslyDisableValidation?: boolean; + validationMaxErrors?: number; csrfPrevention?: CSRFPreventionOptions | boolean; // Used for parsing operations; unlike in AS3, this is not also used for diff --git a/packages/server/src/requestPipeline.ts b/packages/server/src/requestPipeline.ts index b9b4b5542dc..b916a4dc4d5 100644 --- a/packages/server/src/requestPipeline.ts +++ b/packages/server/src/requestPipeline.ts @@ -247,6 +247,7 @@ export async function processGraphQLRequest( schemaDerivedData.schema, requestContext.document, [...specifiedRules, ...internals.validationRules], + { maxErrors: internals.validationMaxErrors }, ); if (validationErrors.length === 0) { From e170f73c3a8604c8b66092a41e8e48e7388785ee Mon Sep 17 00:00:00 2001 From: Eugene Formanenko Date: Wed, 8 Jan 2025 23:52:37 +0400 Subject: [PATCH 2/2] chore: expose all validation options --- .changeset/validate-options.md | 12 ++++++++++++ .changeset/validation-max-errors.md | 13 ------------- packages/server/src/ApolloServer.ts | 7 +++++-- packages/server/src/__tests__/runQuery.test.ts | 7 ++++--- packages/server/src/externalTypes/constructor.ts | 3 ++- packages/server/src/requestPipeline.ts | 2 +- 6 files changed, 24 insertions(+), 20 deletions(-) create mode 100644 .changeset/validate-options.md delete mode 100644 .changeset/validation-max-errors.md diff --git a/.changeset/validate-options.md b/.changeset/validate-options.md new file mode 100644 index 00000000000..06cef98d532 --- /dev/null +++ b/.changeset/validate-options.md @@ -0,0 +1,12 @@ +--- +'@apollo/server': minor +--- + +Expose `graphql` validation options. + +``` +const server = new ApolloServer({ + typeDefs, + resolvers, + validateOptions: { maxErrors: 10 }, +}); diff --git a/.changeset/validation-max-errors.md b/.changeset/validation-max-errors.md deleted file mode 100644 index 40bd1b03889..00000000000 --- a/.changeset/validation-max-errors.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@apollo/server': minor ---- - -Allowing to configure the maximum number of errors when validating a request. -Validation will be aborted if the limit is exceeded. - -``` -const server = new ApolloServer({ - typeDefs, - resolvers, - validationMaxErrors: 10, -}); diff --git a/packages/server/src/ApolloServer.ts b/packages/server/src/ApolloServer.ts index 967a4b1d5d5..a597900ab4f 100644 --- a/packages/server/src/ApolloServer.ts +++ b/packages/server/src/ApolloServer.ts @@ -14,6 +14,7 @@ import { assertValidSchema, print, printSchema, + type validate, type DocumentNode, type FormattedExecutionResult, type GraphQLFieldResolver, @@ -152,11 +153,12 @@ type ServerState = stopError: Error | null; }; +export type ValidateOptions = NonNullable[3]>; + export interface ApolloServerInternals { state: ServerState; gatewayExecutor: GatewayExecutor | null; dangerouslyDisableValidation?: boolean; - validationMaxErrors?: number; formatError?: ( formattedError: GraphQLFormattedError, error: unknown, @@ -168,6 +170,7 @@ export interface ApolloServerInternals { apolloConfig: ApolloConfig; plugins: ApolloServerPlugin[]; parseOptions: ParseOptions; + validationOptions: ValidateOptions; // `undefined` means we figure out what to do during _start (because // the default depends on whether or not we used the background version // of start). @@ -305,7 +308,7 @@ export class ApolloServer { hideSchemaDetailsFromClientErrors, dangerouslyDisableValidation: config.dangerouslyDisableValidation ?? false, - validationMaxErrors: config.validationMaxErrors, + validationOptions: config.validationOptions ?? {}, fieldResolver: config.fieldResolver, includeStacktraceInErrorResponses: config.includeStacktraceInErrorResponses ?? diff --git a/packages/server/src/__tests__/runQuery.test.ts b/packages/server/src/__tests__/runQuery.test.ts index 8f09b8db544..85ad6e4b8a5 100644 --- a/packages/server/src/__tests__/runQuery.test.ts +++ b/packages/server/src/__tests__/runQuery.test.ts @@ -1201,7 +1201,7 @@ describe('parsing and validation cache', () => { }); await server.start(); - const vars = new Array(1000).fill(`$a:a`).join(','); + const vars = new Array(1000).fill('$a:a').join(','); const query = `query aaa (${vars}) { a }`; const res = await server.executeOperation({ query }); @@ -1214,14 +1214,15 @@ describe('parsing and validation cache', () => { expect(body.errors).toHaveLength(101); await server.stop(); }); + it('aborts the validation if max errors more than expected', async () => { const server = new ApolloServer({ schema, - validationMaxErrors: 1, + validationOptions: { maxErrors: 1 }, }); await server.start(); - const vars = new Array(1000).fill(`$a:a`).join(','); + const vars = new Array(1000).fill('$a:a').join(','); const query = `query aaa (${vars}) { a }`; const res = await server.executeOperation({ query }); diff --git a/packages/server/src/externalTypes/constructor.ts b/packages/server/src/externalTypes/constructor.ts index 9efea5e88fb..794144d483c 100644 --- a/packages/server/src/externalTypes/constructor.ts +++ b/packages/server/src/externalTypes/constructor.ts @@ -20,6 +20,7 @@ import type { GatewayInterface } from '@apollo/server-gateway-interface'; import type { ApolloServerPlugin } from './plugins.js'; import type { BaseContext } from './index.js'; import type { GraphQLExperimentalIncrementalExecutionResults } from '../incrementalDeliveryPolyfill.js'; +import type { ValidateOptions } from '../ApolloServer.js'; export type DocumentStore = KeyValueCache; @@ -101,7 +102,7 @@ interface ApolloServerOptionsBase { nodeEnv?: string; documentStore?: DocumentStore | null; dangerouslyDisableValidation?: boolean; - validationMaxErrors?: number; + validationOptions?: ValidateOptions; csrfPrevention?: CSRFPreventionOptions | boolean; // Used for parsing operations; unlike in AS3, this is not also used for diff --git a/packages/server/src/requestPipeline.ts b/packages/server/src/requestPipeline.ts index b916a4dc4d5..4ae15b6b1e1 100644 --- a/packages/server/src/requestPipeline.ts +++ b/packages/server/src/requestPipeline.ts @@ -247,7 +247,7 @@ export async function processGraphQLRequest( schemaDerivedData.schema, requestContext.document, [...specifiedRules, ...internals.validationRules], - { maxErrors: internals.validationMaxErrors }, + internals.validationOptions, ); if (validationErrors.length === 0) {