From 9274bb7623578444936de54ea0ebb2676985f249 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 23 Sep 2025 18:07:32 -0600 Subject: [PATCH 01/93] DeMorgan's Law --- packages/server/src/runHttpQuery.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index 06341dd1b30..94314290370 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -275,18 +275,14 @@ export async function runHttpQuery({ // anything has changed). const acceptHeader = httpRequest.headers.get('accept'); if ( - !( - acceptHeader && - new Negotiator({ - headers: { accept: httpRequest.headers.get('accept') }, - }).mediaType([ - // mediaType() will return the first one that matches, so if the client - // doesn't include the deferSpec parameter it will match this one here, - // which isn't good enough. - MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC, - MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL, - ]) === MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL - ) + !acceptHeader || + new Negotiator({ headers: { accept: acceptHeader } }).mediaType([ + // mediaType() will return the first one that matches, so if the client + // doesn't include the deferSpec parameter it will match this one here, + // which isn't good enough. + MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC, + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL, + ]) !== MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL ) { // The client ran an operation that would yield multiple parts, but didn't // specify `accept: multipart/mixed`. We return an error. From 922da179015c5934edd501b949d83aee05f84ce7 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 23 Sep 2025 18:08:41 -0600 Subject: [PATCH 02/93] Compare against no defer spec --- packages/server/src/runHttpQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index 94314290370..eb46eb663c7 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -282,7 +282,7 @@ export async function runHttpQuery({ // which isn't good enough. MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC, MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL, - ]) !== MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL + ]) === MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC ) { // The client ran an operation that would yield multiple parts, but didn't // specify `accept: multipart/mixed`. We return an error. From 0bb58a0ddd825712e025dd26b1a2bd53383db9f4 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 23 Sep 2025 18:12:11 -0600 Subject: [PATCH 03/93] Add new multipart mixed header --- packages/server/src/ApolloServer.ts | 3 +++ packages/server/src/runHttpQuery.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/packages/server/src/ApolloServer.ts b/packages/server/src/ApolloServer.ts index 0f377e97a7b..a383589fb1b 100644 --- a/packages/server/src/ApolloServer.ts +++ b/packages/server/src/ApolloServer.ts @@ -1205,6 +1205,7 @@ export class ApolloServer { // by the order in the list we provide, so we put text/html last. MEDIA_TYPES.APPLICATION_JSON, MEDIA_TYPES.APPLICATION_GRAPHQL_RESPONSE_JSON, + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_JUNE_2023, MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL, MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC, MEDIA_TYPES.TEXT_HTML, @@ -1402,6 +1403,8 @@ export const MEDIA_TYPES = { // delivery is part of the official GraphQL spec. MULTIPART_MIXED_NO_DEFER_SPEC: 'multipart/mixed', MULTIPART_MIXED_EXPERIMENTAL: 'multipart/mixed; deferSpec=20220824', + MULTIPART_MIXED_EXPERIMENTAL_JUNE_2023: + 'multipart/mixed; incrementalDeliverySpec=20230621', TEXT_HTML: 'text/html', }; diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index eb46eb663c7..e42adbe0cc3 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -282,6 +282,7 @@ export async function runHttpQuery({ // which isn't good enough. MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC, MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL, + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_JUNE_2023, ]) === MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC ) { // The client ran an operation that would yield multiple parts, but didn't From 9b63f2478c2f299e477e1509a741ed1a95071a26 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 23 Sep 2025 18:13:11 -0600 Subject: [PATCH 04/93] Update request error message to suggest new format --- packages/server/src/runHttpQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index e42adbe0cc3..5608e79d377 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -291,7 +291,7 @@ export async function runHttpQuery({ 'Apollo server received an operation that uses incremental delivery ' + '(@defer or @stream), but the client does not accept multipart/mixed ' + 'HTTP responses. To enable incremental delivery support, add the HTTP ' + - "header 'Accept: multipart/mixed; deferSpec=20220824'.", + "header 'Accept: multipart/mixed; incrementalDeliverySpec=20230621'.", // Use 406 Not Accepted { extensions: { http: { status: 406 } } }, ); From 7a8292c616a4c8f2ae3e20b2e0b066c5d415f98a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 00:21:35 -0600 Subject: [PATCH 05/93] Add Alpha2 suffix to existing incremental types --- packages/server/src/externalTypes/graphql.ts | 8 +++---- .../incrementalDeliveryPolyfill.ts | 21 ++++++++++-------- packages/server/src/externalTypes/index.ts | 10 ++++----- packages/server/src/externalTypes/plugins.ts | 4 ++-- packages/server/src/requestPipeline.ts | 8 +++---- packages/server/src/runHttpQuery.ts | 22 +++++++++---------- 6 files changed, 38 insertions(+), 35 deletions(-) diff --git a/packages/server/src/externalTypes/graphql.ts b/packages/server/src/externalTypes/graphql.ts index 20090b4daa7..16173236433 100644 --- a/packages/server/src/externalTypes/graphql.ts +++ b/packages/server/src/externalTypes/graphql.ts @@ -9,8 +9,8 @@ import type { BaseContext } from './context.js'; import type { HTTPGraphQLHead, HTTPGraphQLRequest } from './http.js'; import type { WithRequired } from '@apollo/utils.withrequired'; import type { - GraphQLExperimentalFormattedInitialIncrementalExecutionResult, - GraphQLExperimentalFormattedSubsequentIncrementalExecutionResult, + GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2, + GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2, } from './incrementalDeliveryPolyfill.js'; export interface GraphQLRequest< @@ -37,8 +37,8 @@ export type GraphQLResponseBody> = } | { kind: 'incremental'; - initialResult: GraphQLExperimentalFormattedInitialIncrementalExecutionResult; - subsequentResults: AsyncIterable; + initialResult: GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2; + subsequentResults: AsyncIterable; }; export type GraphQLInProgressResponse> = { diff --git a/packages/server/src/externalTypes/incrementalDeliveryPolyfill.ts b/packages/server/src/externalTypes/incrementalDeliveryPolyfill.ts index 6ddbc419e1b..c5b6c1cb661 100644 --- a/packages/server/src/externalTypes/incrementalDeliveryPolyfill.ts +++ b/packages/server/src/externalTypes/incrementalDeliveryPolyfill.ts @@ -11,36 +11,39 @@ interface ObjMap { [key: string]: T; } -export interface GraphQLExperimentalFormattedInitialIncrementalExecutionResult< +export interface GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2< TData = ObjMap, TExtensions = ObjMap, > extends FormattedExecutionResult { hasNext: boolean; incremental?: ReadonlyArray< - GraphQLExperimentalFormattedIncrementalResult + GraphQLExperimentalFormattedIncrementalResultAlpha2 >; extensions?: TExtensions; } -export interface GraphQLExperimentalFormattedSubsequentIncrementalExecutionResult< +export interface GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2< TData = ObjMap, TExtensions = ObjMap, > { hasNext: boolean; incremental?: ReadonlyArray< - GraphQLExperimentalFormattedIncrementalResult + GraphQLExperimentalFormattedIncrementalResultAlpha2 >; extensions?: TExtensions; } -export type GraphQLExperimentalFormattedIncrementalResult< +export type GraphQLExperimentalFormattedIncrementalResultAlpha2< TData = ObjMap, TExtensions = ObjMap, > = - | GraphQLExperimentalFormattedIncrementalDeferResult - | GraphQLExperimentalFormattedIncrementalStreamResult; + | GraphQLExperimentalFormattedIncrementalDeferResultAlpha2 + | GraphQLExperimentalFormattedIncrementalStreamResultAlpha2< + TData, + TExtensions + >; -export interface GraphQLExperimentalFormattedIncrementalDeferResult< +export interface GraphQLExperimentalFormattedIncrementalDeferResultAlpha2< TData = ObjMap, TExtensions = ObjMap, > extends FormattedExecutionResult { @@ -48,7 +51,7 @@ export interface GraphQLExperimentalFormattedIncrementalDeferResult< label?: string; } -export interface GraphQLExperimentalFormattedIncrementalStreamResult< +export interface GraphQLExperimentalFormattedIncrementalStreamResultAlpha2< TData = Array, TExtensions = ObjMap, > { diff --git a/packages/server/src/externalTypes/index.ts b/packages/server/src/externalTypes/index.ts index 44825446a73..16cfc2f1310 100644 --- a/packages/server/src/externalTypes/index.ts +++ b/packages/server/src/externalTypes/index.ts @@ -51,9 +51,9 @@ export type { } from './constructor.js'; export type { - GraphQLExperimentalFormattedInitialIncrementalExecutionResult, - GraphQLExperimentalFormattedSubsequentIncrementalExecutionResult, - GraphQLExperimentalFormattedIncrementalResult, - GraphQLExperimentalFormattedIncrementalDeferResult, - GraphQLExperimentalFormattedIncrementalStreamResult, + GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2, + GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2, + GraphQLExperimentalFormattedIncrementalResultAlpha2, + GraphQLExperimentalFormattedIncrementalDeferResultAlpha2, + GraphQLExperimentalFormattedIncrementalStreamResultAlpha2, } from './incrementalDeliveryPolyfill.js'; diff --git a/packages/server/src/externalTypes/plugins.ts b/packages/server/src/externalTypes/plugins.ts index 32dd89604dc..3607415ca1a 100644 --- a/packages/server/src/externalTypes/plugins.ts +++ b/packages/server/src/externalTypes/plugins.ts @@ -9,7 +9,7 @@ import type { GraphQLError, GraphQLResolveInfo, GraphQLSchema } from 'graphql'; import type { ApolloConfig } from './constructor.js'; import type { BaseContext } from './context.js'; import type { GraphQLResponse } from './graphql.js'; -import type { GraphQLExperimentalFormattedSubsequentIncrementalExecutionResult } from './incrementalDeliveryPolyfill.js'; +import type { GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2 } from './incrementalDeliveryPolyfill.js'; import type { GraphQLRequestContext, GraphQLRequestContextDidEncounterErrors, @@ -180,7 +180,7 @@ export interface GraphQLRequestListener { // You can use hasNext to tell if this is the end or not. willSendSubsequentPayload?( requestContext: GraphQLRequestContextWillSendSubsequentPayload, - payload: GraphQLExperimentalFormattedSubsequentIncrementalExecutionResult, + payload: GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2, ): Promise; } diff --git a/packages/server/src/requestPipeline.ts b/packages/server/src/requestPipeline.ts index 362762e21c5..df6aad53755 100644 --- a/packages/server/src/requestPipeline.ts +++ b/packages/server/src/requestPipeline.ts @@ -40,7 +40,7 @@ import type { GraphQLRequestExecutionListener, BaseContext, GraphQLResponse, - GraphQLExperimentalFormattedSubsequentIncrementalExecutionResult, + GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2, } from './externalTypes/index.js'; import { @@ -110,7 +110,7 @@ type SemiFormattedExecuteIncrementallyResults = } | { initialResult: GraphQLExperimentalInitialIncrementalExecutionResult; - subsequentResults: AsyncIterable; + subsequentResults: AsyncIterable; }; export async function processGraphQLRequest( @@ -580,9 +580,9 @@ export async function processGraphQLRequest( async function* formatErrorsInSubsequentResults( results: AsyncIterable, - ): AsyncIterable { + ): AsyncIterable { for await (const result of results) { - const payload: GraphQLExperimentalFormattedSubsequentIncrementalExecutionResult = + const payload: GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2 = result.incremental ? { ...result, diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index 5608e79d377..0c05d233f25 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -1,8 +1,8 @@ import type { BaseContext, - GraphQLExperimentalFormattedIncrementalResult, - GraphQLExperimentalFormattedInitialIncrementalExecutionResult, - GraphQLExperimentalFormattedSubsequentIncrementalExecutionResult, + GraphQLExperimentalFormattedIncrementalResultAlpha2, + GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2, + GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2, GraphQLRequest, HTTPGraphQLHead, HTTPGraphQLRequest, @@ -314,8 +314,8 @@ export async function runHttpQuery({ } async function* writeMultipartBody( - initialResult: GraphQLExperimentalFormattedInitialIncrementalExecutionResult, - subsequentResults: AsyncIterable, + initialResult: GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2, + subsequentResults: AsyncIterable, ): AsyncGenerator { // Note: we assume in this function that every result other than the last has // hasNext=true and the last has hasNext=false. That is, we choose which kind @@ -348,8 +348,8 @@ function orderExecutionResultFields( }; } function orderInitialIncrementalExecutionResultFields( - result: GraphQLExperimentalFormattedInitialIncrementalExecutionResult, -): GraphQLExperimentalFormattedInitialIncrementalExecutionResult { + result: GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2, +): GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2 { return { hasNext: result.hasNext, errors: result.errors, @@ -359,8 +359,8 @@ function orderInitialIncrementalExecutionResultFields( }; } function orderSubsequentIncrementalExecutionResultFields( - result: GraphQLExperimentalFormattedSubsequentIncrementalExecutionResult, -): GraphQLExperimentalFormattedSubsequentIncrementalExecutionResult { + result: GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2, +): GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2 { return { hasNext: result.hasNext, incremental: orderIncrementalResultFields(result.incremental), @@ -369,8 +369,8 @@ function orderSubsequentIncrementalExecutionResultFields( } function orderIncrementalResultFields( - incremental?: readonly GraphQLExperimentalFormattedIncrementalResult[], -): undefined | GraphQLExperimentalFormattedIncrementalResult[] { + incremental?: readonly GraphQLExperimentalFormattedIncrementalResultAlpha2[], +): undefined | GraphQLExperimentalFormattedIncrementalResultAlpha2[] { return incremental?.map((i: any) => ({ hasNext: i.hasNext, errors: i.errors, From 852a6a6bb3c4eb5d8efbff564bc6eb27b7586e1c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 00:23:52 -0600 Subject: [PATCH 06/93] Add Alpha2 suffix to polyfill file --- packages/server/src/externalTypes/graphql.ts | 2 +- ...DeliveryPolyfill.ts => incrementalDeliveryPolyfillAlpha2.ts} | 0 packages/server/src/externalTypes/index.ts | 2 +- packages/server/src/externalTypes/plugins.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename packages/server/src/externalTypes/{incrementalDeliveryPolyfill.ts => incrementalDeliveryPolyfillAlpha2.ts} (100%) diff --git a/packages/server/src/externalTypes/graphql.ts b/packages/server/src/externalTypes/graphql.ts index 16173236433..dbd505f9106 100644 --- a/packages/server/src/externalTypes/graphql.ts +++ b/packages/server/src/externalTypes/graphql.ts @@ -11,7 +11,7 @@ import type { WithRequired } from '@apollo/utils.withrequired'; import type { GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2, GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2, -} from './incrementalDeliveryPolyfill.js'; +} from './incrementalDeliveryPolyfillAlpha2.js'; export interface GraphQLRequest< TVariables extends VariableValues = VariableValues, diff --git a/packages/server/src/externalTypes/incrementalDeliveryPolyfill.ts b/packages/server/src/externalTypes/incrementalDeliveryPolyfillAlpha2.ts similarity index 100% rename from packages/server/src/externalTypes/incrementalDeliveryPolyfill.ts rename to packages/server/src/externalTypes/incrementalDeliveryPolyfillAlpha2.ts diff --git a/packages/server/src/externalTypes/index.ts b/packages/server/src/externalTypes/index.ts index 16cfc2f1310..0c65077ea94 100644 --- a/packages/server/src/externalTypes/index.ts +++ b/packages/server/src/externalTypes/index.ts @@ -56,4 +56,4 @@ export type { GraphQLExperimentalFormattedIncrementalResultAlpha2, GraphQLExperimentalFormattedIncrementalDeferResultAlpha2, GraphQLExperimentalFormattedIncrementalStreamResultAlpha2, -} from './incrementalDeliveryPolyfill.js'; +} from './incrementalDeliveryPolyfillAlpha2.js'; diff --git a/packages/server/src/externalTypes/plugins.ts b/packages/server/src/externalTypes/plugins.ts index 3607415ca1a..901f0a12fea 100644 --- a/packages/server/src/externalTypes/plugins.ts +++ b/packages/server/src/externalTypes/plugins.ts @@ -9,7 +9,7 @@ import type { GraphQLError, GraphQLResolveInfo, GraphQLSchema } from 'graphql'; import type { ApolloConfig } from './constructor.js'; import type { BaseContext } from './context.js'; import type { GraphQLResponse } from './graphql.js'; -import type { GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2 } from './incrementalDeliveryPolyfill.js'; +import type { GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2 } from './incrementalDeliveryPolyfillAlpha2.js'; import type { GraphQLRequestContext, GraphQLRequestContextDidEncounterErrors, From 8ac5c719a88bda1e1a2a0f58a6ad683f52b93663 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 00:36:01 -0600 Subject: [PATCH 07/93] Add polyfill types for alpha9 --- .../incrementalDeliveryPolyfillAlpha9.ts | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 packages/server/src/externalTypes/incrementalDeliveryPolyfillAlpha9.ts diff --git a/packages/server/src/externalTypes/incrementalDeliveryPolyfillAlpha9.ts b/packages/server/src/externalTypes/incrementalDeliveryPolyfillAlpha9.ts new file mode 100644 index 00000000000..36126880c5f --- /dev/null +++ b/packages/server/src/externalTypes/incrementalDeliveryPolyfillAlpha9.ts @@ -0,0 +1,82 @@ +import type { FormattedExecutionResult, GraphQLFormattedError } from 'graphql'; + +// This file defines types used in our public interface that will be imported +// from `graphql-js` once graphql 17 is released. It is possible that these +// types will change slightly before the final v17 is released, in which case +// the relevant parts of our API may change incompatibly in a minor version of +// AS5; this should not affect any users who aren't explicitly installing +// pre-releases of graphql 17. + +interface ObjMap { + [key: string]: T; +} + +export interface GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha9< + TData = ObjMap, + TExtensions = ObjMap, +> extends FormattedExecutionResult { + data: TData; + pending: ReadonlyArray; + hasNext: boolean; + extensions?: TExtensions; +} + +export interface GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha9< + TData = ObjMap, + TExtensions = ObjMap, +> { + hasNext: boolean; + pending?: ReadonlyArray; + incremental?: ReadonlyArray< + GraphQLExperimentalFormattedIncrementalResultAlpha9 + >; + completed?: ReadonlyArray; + extensions?: TExtensions; +} + +export type GraphQLExperimentalFormattedIncrementalResultAlpha9< + TData = ObjMap, + TExtensions = ObjMap, +> = + | GraphQLExperimentalFormattedIncrementalDeferResultAlpha9 + | GraphQLExperimentalFormattedIncrementalStreamResultAlpha9< + TData, + TExtensions + >; + +export interface GraphQLExperimentalFormattedIncrementalDeferResultAlpha9< + TData = ObjMap, + TExtensions = ObjMap, +> { + errors?: ReadonlyArray; + data: TData; + id: string; + subPath?: ReadonlyArray; + extensions?: TExtensions; +} + +export interface GraphQLExperimentalFormattedIncrementalStreamResultAlpha9< + TData = Array, + TExtensions = ObjMap, +> { + errors?: ReadonlyArray; + items: TData; + id: string; + subPath?: ReadonlyArray; + extensions?: TExtensions; +} + +interface GraphQLExperimentalPendingResultAlpha9 { + id: string; + path: ReadonlyArray; + label?: string; +} + +// Deviation. The type implemented in alpha.9 is wrong. The type below is the +// correct type implementation. We may or may not need to provide a patch. +// +// This has been fixed by https://github.com/graphql/graphql-js/pull/4481 +export interface GraphQLExperimentalFormattedCompletedResultAlpha9 { + id: string; + errors?: ReadonlyArray; +} From 96112c0546baa1978640b5fdef264b9bc13c38d1 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 00:37:18 -0600 Subject: [PATCH 08/93] Export alpha9 types --- .../externalTypes/incrementalDeliveryPolyfillAlpha9.ts | 2 +- packages/server/src/externalTypes/index.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/server/src/externalTypes/incrementalDeliveryPolyfillAlpha9.ts b/packages/server/src/externalTypes/incrementalDeliveryPolyfillAlpha9.ts index 36126880c5f..37effbff5f3 100644 --- a/packages/server/src/externalTypes/incrementalDeliveryPolyfillAlpha9.ts +++ b/packages/server/src/externalTypes/incrementalDeliveryPolyfillAlpha9.ts @@ -66,7 +66,7 @@ export interface GraphQLExperimentalFormattedIncrementalStreamResultAlpha9< extensions?: TExtensions; } -interface GraphQLExperimentalPendingResultAlpha9 { +export interface GraphQLExperimentalPendingResultAlpha9 { id: string; path: ReadonlyArray; label?: string; diff --git a/packages/server/src/externalTypes/index.ts b/packages/server/src/externalTypes/index.ts index 0c65077ea94..4f64b2c0c2a 100644 --- a/packages/server/src/externalTypes/index.ts +++ b/packages/server/src/externalTypes/index.ts @@ -57,3 +57,13 @@ export type { GraphQLExperimentalFormattedIncrementalDeferResultAlpha2, GraphQLExperimentalFormattedIncrementalStreamResultAlpha2, } from './incrementalDeliveryPolyfillAlpha2.js'; + +export type { + GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha9, + GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha9, + GraphQLExperimentalFormattedIncrementalStreamResultAlpha9, + GraphQLExperimentalFormattedIncrementalDeferResultAlpha9, + GraphQLExperimentalFormattedCompletedResultAlpha9, + GraphQLExperimentalFormattedIncrementalResultAlpha9, + GraphQLExperimentalPendingResultAlpha9, +} from './incrementalDeliveryPolyfillAlpha9.js'; From 56245aa5a2c99cdd9f4222e45d09ac3016d31dc3 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 14:52:10 -0600 Subject: [PATCH 09/93] Remove ordering of incremental fields --- packages/server/src/runHttpQuery.ts | 39 ++--------------------------- 1 file changed, 2 insertions(+), 37 deletions(-) diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index 0c05d233f25..69609977b0e 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -1,6 +1,5 @@ import type { BaseContext, - GraphQLExperimentalFormattedIncrementalResultAlpha2, GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2, GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2, GraphQLRequest, @@ -326,12 +325,12 @@ async function* writeMultipartBody( // iterator is finished until we do async work. yield `\r\n---\r\ncontent-type: application/json; charset=utf-8\r\n\r\n${JSON.stringify( - orderInitialIncrementalExecutionResultFields(initialResult), + initialResult, )}\r\n---${initialResult.hasNext ? '' : '--'}\r\n`; for await (const result of subsequentResults) { yield `content-type: application/json; charset=utf-8\r\n\r\n${JSON.stringify( - orderSubsequentIncrementalExecutionResultFields(result), + result, )}\r\n---${result.hasNext ? '' : '--'}\r\n`; } } @@ -347,40 +346,6 @@ function orderExecutionResultFields( extensions: result.extensions, }; } -function orderInitialIncrementalExecutionResultFields( - result: GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2, -): GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2 { - return { - hasNext: result.hasNext, - errors: result.errors, - data: result.data, - incremental: orderIncrementalResultFields(result.incremental), - extensions: result.extensions, - }; -} -function orderSubsequentIncrementalExecutionResultFields( - result: GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2, -): GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2 { - return { - hasNext: result.hasNext, - incremental: orderIncrementalResultFields(result.incremental), - extensions: result.extensions, - }; -} - -function orderIncrementalResultFields( - incremental?: readonly GraphQLExperimentalFormattedIncrementalResultAlpha2[], -): undefined | GraphQLExperimentalFormattedIncrementalResultAlpha2[] { - return incremental?.map((i: any) => ({ - hasNext: i.hasNext, - errors: i.errors, - path: i.path, - label: i.label, - data: i.data, - items: i.items, - extensions: i.extensions, - })); -} // The result of a curl does not appear well in the terminal, so we add an extra new line export function prettyJSONStringify(value: FormattedExecutionResult) { From 92fa99bee9ce7e59d0b986ec6aca2ee12d6c7cf4 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 14:54:54 -0600 Subject: [PATCH 10/93] Add type for GraphQLResponseBody with alpha 9 format --- packages/server/src/externalTypes/graphql.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/server/src/externalTypes/graphql.ts b/packages/server/src/externalTypes/graphql.ts index dbd505f9106..ec433338043 100644 --- a/packages/server/src/externalTypes/graphql.ts +++ b/packages/server/src/externalTypes/graphql.ts @@ -12,6 +12,10 @@ import type { GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2, GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2, } from './incrementalDeliveryPolyfillAlpha2.js'; +import type { + GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha9, + GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha9, +} from './incrementalDeliveryPolyfillAlpha9.js'; export interface GraphQLRequest< TVariables extends VariableValues = VariableValues, @@ -39,6 +43,11 @@ export type GraphQLResponseBody> = kind: 'incremental'; initialResult: GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2; subsequentResults: AsyncIterable; + } + | { + kind: 'incremental'; + initialResult: GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha9; + subsequentResults: AsyncIterable; }; export type GraphQLInProgressResponse> = { From fbfa0dfabe289262ae06e6b486caf1c89cc90c6e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 15:32:40 -0600 Subject: [PATCH 11/93] Update writeMultipartBody signature --- packages/server/src/runHttpQuery.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index 69609977b0e..aea6037ee76 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -1,7 +1,9 @@ import type { BaseContext, GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2, + GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha9, GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2, + GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha9, GraphQLRequest, HTTPGraphQLHead, HTTPGraphQLRequest, @@ -313,8 +315,13 @@ export async function runHttpQuery({ } async function* writeMultipartBody( - initialResult: GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2, - subsequentResults: AsyncIterable, + initialResult: + | GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2 + | GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha9, + subsequentResults: AsyncIterable< + | GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2 + | GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha9 + >, ): AsyncGenerator { // Note: we assume in this function that every result other than the last has // hasNext=true and the last has hasNext=false. That is, we choose which kind From c7abd876a12e84019d65a07d9020ba6212831676 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 17:14:46 -0600 Subject: [PATCH 12/93] Update payload type for willSendSubsequentPayload --- packages/server/src/externalTypes/plugins.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/server/src/externalTypes/plugins.ts b/packages/server/src/externalTypes/plugins.ts index 901f0a12fea..57151621256 100644 --- a/packages/server/src/externalTypes/plugins.ts +++ b/packages/server/src/externalTypes/plugins.ts @@ -23,6 +23,7 @@ import type { GraphQLRequestContextWillSendResponse, GraphQLRequestContextWillSendSubsequentPayload, } from './requestPipeline.js'; +import type { GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha9 } from './incrementalDeliveryPolyfillAlpha9.js'; export interface GraphQLServerContext { readonly logger: Logger; @@ -180,7 +181,9 @@ export interface GraphQLRequestListener { // You can use hasNext to tell if this is the end or not. willSendSubsequentPayload?( requestContext: GraphQLRequestContextWillSendSubsequentPayload, - payload: GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2, + payload: + | GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2 + | GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha9, ): Promise; } From 4d3beb053ab2bdcad00379e647d56d8afab8761d Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 17:24:14 -0600 Subject: [PATCH 13/93] Load from alpha 9 --- packages/server/src/incrementalDeliveryPolyfill.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/server/src/incrementalDeliveryPolyfill.ts b/packages/server/src/incrementalDeliveryPolyfill.ts index 1dbc1b7a746..23206f1510a 100644 --- a/packages/server/src/incrementalDeliveryPolyfill.ts +++ b/packages/server/src/incrementalDeliveryPolyfill.ts @@ -102,6 +102,12 @@ async function tryToLoadGraphQL17() { ) { graphqlExperimentalExecuteIncrementally = (graphql as any) .experimentalExecuteIncrementally; + } else if ( + graphql.version === '17.0.0-alpha.9' && + 'experimentalExecuteIncrementally' in graphql + ) { + graphqlExperimentalExecuteIncrementally = (graphql as any) + .experimentalExecuteIncrementally; } else { graphqlExperimentalExecuteIncrementally = null; } From d6e19f6744329f751cea1bc69724c9620b93c682 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 17:34:08 -0600 Subject: [PATCH 14/93] Add alpha2 suffix to existing execute types and add alpha9 types --- .../server/src/incrementalDeliveryPolyfill.ts | 128 ++++++++++++++++-- 1 file changed, 114 insertions(+), 14 deletions(-) diff --git a/packages/server/src/incrementalDeliveryPolyfill.ts b/packages/server/src/incrementalDeliveryPolyfill.ts index 23206f1510a..20c04707ed2 100644 --- a/packages/server/src/incrementalDeliveryPolyfill.ts +++ b/packages/server/src/incrementalDeliveryPolyfill.ts @@ -13,36 +13,38 @@ import { interface ObjMap { [key: string]: T; } -export interface GraphQLExperimentalInitialIncrementalExecutionResult< + +// 17.0.0-alpha.2 +export interface GraphQLExperimentalInitialIncrementalExecutionResultAlpha2< TData = ObjMap, TExtensions = ObjMap, > extends ExecutionResult { hasNext: boolean; incremental?: ReadonlyArray< - GraphQLExperimentalIncrementalResult + GraphQLExperimentalIncrementalResultAlpha2 >; extensions?: TExtensions; } -export interface GraphQLExperimentalSubsequentIncrementalExecutionResult< +export interface GraphQLExperimentalSubsequentIncrementalExecutionResultAlpha2< TData = ObjMap, TExtensions = ObjMap, > { hasNext: boolean; incremental?: ReadonlyArray< - GraphQLExperimentalIncrementalResult + GraphQLExperimentalIncrementalResultAlpha2 >; extensions?: TExtensions; } -type GraphQLExperimentalIncrementalResult< +type GraphQLExperimentalIncrementalResultAlpha2< TData = ObjMap, TExtensions = ObjMap, > = - | GraphQLExperimentalIncrementalDeferResult - | GraphQLExperimentalIncrementalStreamResult; + | GraphQLExperimentalIncrementalDeferResultAlpha2 + | GraphQLExperimentalIncrementalStreamResultAlpha2; -interface GraphQLExperimentalIncrementalDeferResult< +interface GraphQLExperimentalIncrementalDeferResultAlpha2< TData = ObjMap, TExtensions = ObjMap, > extends ExecutionResult { @@ -50,7 +52,7 @@ interface GraphQLExperimentalIncrementalDeferResult< label?: string; } -interface GraphQLExperimentalIncrementalStreamResult< +interface GraphQLExperimentalIncrementalStreamResultAlpha2< TData = Array, TExtensions = ObjMap, > { @@ -61,16 +63,112 @@ interface GraphQLExperimentalIncrementalStreamResult< extensions?: TExtensions; } -export interface GraphQLExperimentalIncrementalExecutionResults< +export interface GraphQLExperimentalIncrementalExecutionResultsAlpha2< TData = ObjMap, TExtensions = ObjMap, > { - initialResult: GraphQLExperimentalInitialIncrementalExecutionResult< + initialResult: GraphQLExperimentalInitialIncrementalExecutionResultAlpha2< TData, TExtensions >; subsequentResults: AsyncGenerator< - GraphQLExperimentalSubsequentIncrementalExecutionResult, + GraphQLExperimentalSubsequentIncrementalExecutionResultAlpha2< + TData, + TExtensions + >, + void, + void + >; +} + +// 17.0.0-alpha.9 +export interface GraphQLExperimentalInitialIncrementalExecutionResultAlpha9< + TData = ObjMap, + TExtensions = ObjMap, +> extends ExecutionResult { + data: TData; + pending: ReadonlyArray; + hasNext: true; + extensions?: TExtensions; +} + +export interface GraphQLExperimentalSubsequentIncrementalExecutionResultAlpha9< + TData = ObjMap, + TExtensions = ObjMap, +> { + pending?: ReadonlyArray; + incremental?: ReadonlyArray< + GraphQLExperimentalIncrementalResultAlpha9 + >; + completed?: ReadonlyArray; + hasNext: boolean; + extensions?: TExtensions; +} + +interface GraphQLExperimentalExecutionGroupResultAlpha9< + TData = ObjMap, +> { + errors?: ReadonlyArray; + data: TData; +} + +interface GraphQLExperimentalIncrementalDeferResultAlpha9< + TData = ObjMap, + TExtensions = ObjMap, +> extends GraphQLExperimentalExecutionGroupResultAlpha9 { + id: string; + subPath?: ReadonlyArray; + extensions?: TExtensions; +} + +interface GraphQLExperimentalStreamItemsRecordResultAlpha9< + TData = ReadonlyArray, +> { + errors?: ReadonlyArray; + items: TData; +} + +interface GraphQLExperimentalIncrementalStreamResultAlpha9< + TData = ReadonlyArray, + TExtensions = ObjMap, +> extends GraphQLExperimentalStreamItemsRecordResultAlpha9 { + id: string; + subPath?: ReadonlyArray; + extensions?: TExtensions; +} + +type GraphQLExperimentalIncrementalResultAlpha9< + TData = unknown, + TExtensions = ObjMap, +> = + | GraphQLExperimentalIncrementalDeferResultAlpha9 + | GraphQLExperimentalIncrementalStreamResultAlpha9; + +export interface GraphQLExperimentalPendingResultAlpha9 { + id: string; + path: ReadonlyArray; + label?: string; +} + +export interface GraphQLExperimentalCompletedResultAlpha9 { + id: string; + errors?: ReadonlyArray; +} + +export interface GraphQLExperimentalIncrementalExecutionResultsAlpha9< + TInitial = ObjMap, + TSubsequent = unknown, + TExtensions = ObjMap, +> { + initialResult: GraphQLExperimentalInitialIncrementalExecutionResultAlpha9< + TInitial, + TExtensions + >; + subsequentResults: AsyncGenerator< + GraphQLExperimentalSubsequentIncrementalExecutionResultAlpha9< + TSubsequent, + TExtensions + >, void, void >; @@ -86,7 +184,7 @@ let graphqlExperimentalExecuteIncrementally: | (( args: ExecutionArgs, ) => PromiseOrValue< - ExecutionResult | GraphQLExperimentalIncrementalExecutionResults + ExecutionResult | GraphQLExperimentalIncrementalExecutionResultsAlpha2 >) | null | undefined = undefined; @@ -115,7 +213,9 @@ async function tryToLoadGraphQL17() { export async function executeIncrementally( args: ExecutionArgs, -): Promise { +): Promise< + ExecutionResult | GraphQLExperimentalIncrementalExecutionResultsAlpha2 +> { await tryToLoadGraphQL17(); if (graphqlExperimentalExecuteIncrementally) { return graphqlExperimentalExecuteIncrementally(args); From ddddd7912bb51d412c0855ab38a4019060708bcf Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 17:35:07 -0600 Subject: [PATCH 15/93] Update ApolloServer types --- packages/server/src/ApolloServer.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/server/src/ApolloServer.ts b/packages/server/src/ApolloServer.ts index a383589fb1b..b69665ab5f5 100644 --- a/packages/server/src/ApolloServer.ts +++ b/packages/server/src/ApolloServer.ts @@ -57,7 +57,10 @@ import type { PersistedQueryOptions, } from './externalTypes/index.js'; import { runPotentiallyBatchedHttpQuery } from './httpBatching.js'; -import type { GraphQLExperimentalIncrementalExecutionResults } from './incrementalDeliveryPolyfill.js'; +import type { + GraphQLExperimentalIncrementalExecutionResultsAlpha2, + GraphQLExperimentalIncrementalExecutionResultsAlpha9, +} from './incrementalDeliveryPolyfill.js'; import { pluginIsInternal, type InternalPluginId } from './internalPlugin.js'; import { preventCsrf, @@ -162,7 +165,9 @@ export interface ApolloServerInternals { fieldResolver?: GraphQLFieldResolver; // TODO(AS6): remove this option. status400ForVariableCoercionErrors?: boolean; - __testing_incrementalExecutionResults?: GraphQLExperimentalIncrementalExecutionResults; + __testing_incrementalExecutionResults?: + | GraphQLExperimentalIncrementalExecutionResultsAlpha2 + | GraphQLExperimentalIncrementalExecutionResultsAlpha9; stringifyResult: ( value: FormattedExecutionResult, ) => string | Promise; From bb827ce6985e80194c911f1dc94aee2bf0ee568d Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 17:35:41 -0600 Subject: [PATCH 16/93] Update constructor types --- packages/server/src/externalTypes/constructor.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/server/src/externalTypes/constructor.ts b/packages/server/src/externalTypes/constructor.ts index 9f2dd7d11bf..285edb2852a 100644 --- a/packages/server/src/externalTypes/constructor.ts +++ b/packages/server/src/externalTypes/constructor.ts @@ -19,7 +19,10 @@ import type { KeyValueCache } from '@apollo/utils.keyvaluecache'; 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 { + GraphQLExperimentalIncrementalExecutionResultsAlpha2, + GraphQLExperimentalIncrementalExecutionResultsAlpha9, +} from '../incrementalDeliveryPolyfill.js'; export type DocumentStore = KeyValueCache; @@ -119,7 +122,9 @@ interface ApolloServerOptionsBase { status400ForVariableCoercionErrors?: boolean; // For testing only. - __testing_incrementalExecutionResults?: GraphQLExperimentalIncrementalExecutionResults; + __testing_incrementalExecutionResults?: + | GraphQLExperimentalIncrementalExecutionResultsAlpha2 + | GraphQLExperimentalIncrementalExecutionResultsAlpha9; } export interface ApolloServerOptionsWithGateway From 0da6d928ff039c3c64bb15d5f74dba1577312ea0 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 17:36:56 -0600 Subject: [PATCH 17/93] Ensure executeIncrementally returns alpha9 types --- packages/server/src/incrementalDeliveryPolyfill.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/server/src/incrementalDeliveryPolyfill.ts b/packages/server/src/incrementalDeliveryPolyfill.ts index 20c04707ed2..a44e55dabfe 100644 --- a/packages/server/src/incrementalDeliveryPolyfill.ts +++ b/packages/server/src/incrementalDeliveryPolyfill.ts @@ -184,7 +184,9 @@ let graphqlExperimentalExecuteIncrementally: | (( args: ExecutionArgs, ) => PromiseOrValue< - ExecutionResult | GraphQLExperimentalIncrementalExecutionResultsAlpha2 + | ExecutionResult + | GraphQLExperimentalIncrementalExecutionResultsAlpha2 + | GraphQLExperimentalIncrementalExecutionResultsAlpha9 >) | null | undefined = undefined; @@ -214,7 +216,9 @@ async function tryToLoadGraphQL17() { export async function executeIncrementally( args: ExecutionArgs, ): Promise< - ExecutionResult | GraphQLExperimentalIncrementalExecutionResultsAlpha2 + | ExecutionResult + | GraphQLExperimentalIncrementalExecutionResultsAlpha2 + | GraphQLExperimentalIncrementalExecutionResultsAlpha9 > { await tryToLoadGraphQL17(); if (graphqlExperimentalExecuteIncrementally) { From d8fa3ece59838c65b6f86e7765c2efbe3ddd06ef Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 17:54:16 -0600 Subject: [PATCH 18/93] Handle formatting alpha9 errors --- packages/server/src/requestPipeline.ts | 108 +++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 8 deletions(-) diff --git a/packages/server/src/requestPipeline.ts b/packages/server/src/requestPipeline.ts index df6aad53755..1542075f318 100644 --- a/packages/server/src/requestPipeline.ts +++ b/packages/server/src/requestPipeline.ts @@ -41,6 +41,7 @@ import type { BaseContext, GraphQLResponse, GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2, + GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha9, } from './externalTypes/index.js'; import { @@ -64,8 +65,10 @@ import type { } from './externalTypes/requestPipeline.js'; import { executeIncrementally, - type GraphQLExperimentalInitialIncrementalExecutionResult, - type GraphQLExperimentalSubsequentIncrementalExecutionResult, + type GraphQLExperimentalSubsequentIncrementalExecutionResultAlpha9, + type GraphQLExperimentalInitialIncrementalExecutionResultAlpha2, + type GraphQLExperimentalInitialIncrementalExecutionResultAlpha9, + type GraphQLExperimentalSubsequentIncrementalExecutionResultAlpha2, } from './incrementalDeliveryPolyfill.js'; import { HeaderMap } from './utils/HeaderMap.js'; @@ -109,8 +112,12 @@ type SemiFormattedExecuteIncrementallyResults = singleResult: ExecutionResult; } | { - initialResult: GraphQLExperimentalInitialIncrementalExecutionResult; + initialResult: GraphQLExperimentalInitialIncrementalExecutionResultAlpha2; subsequentResults: AsyncIterable; + } + | { + initialResult: GraphQLExperimentalInitialIncrementalExecutionResultAlpha9; + subsequentResults: AsyncIterable; }; export async function processGraphQLRequest( @@ -568,9 +575,14 @@ export async function processGraphQLRequest( if ('initialResult' in resultOrResults) { return { initialResult: resultOrResults.initialResult, - subsequentResults: formatErrorsInSubsequentResults( - resultOrResults.subsequentResults, - ), + subsequentResults: + 'pending' in resultOrResults.initialResult + ? formatErrorsInSubsequentResultsAlpha9( + resultOrResults.subsequentResults as AsyncIterable, + ) + : formatErrorsInSubsequentResultsAlpha2( + resultOrResults.subsequentResults, + ), }; } else { return { singleResult: resultOrResults }; @@ -578,8 +590,8 @@ export async function processGraphQLRequest( } } - async function* formatErrorsInSubsequentResults( - results: AsyncIterable, + async function* formatErrorsInSubsequentResultsAlpha2( + results: AsyncIterable, ): AsyncIterable { for await (const result of results) { const payload: GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2 = @@ -628,6 +640,86 @@ export async function processGraphQLRequest( } } + async function* formatErrorsInSubsequentResultsAlpha9( + results: AsyncIterable, + ): AsyncIterable { + for await (const result of results) { + const payload: GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha9 = + result.incremental + ? { + ...result, + incremental: await seriesAsyncMap( + result.incremental, + async (incrementalResult) => { + const { errors } = incrementalResult; + if (errors) { + await Promise.all( + requestListeners.map((l) => + l.didEncounterSubsequentErrors?.( + requestContext as GraphQLRequestContextDidEncounterSubsequentErrors, + errors, + ), + ), + ); + + return { + ...incrementalResult, + // Note that any `http` extensions in errors have no + // effect, because we've already sent the status code + // and response headers. + errors: formatErrors(errors).formattedErrors, + }; + } + return incrementalResult; + }, + ), + } + : result; + + if (result.completed) { + payload.completed = await seriesAsyncMap( + result.completed, + async (completedResult) => { + const { errors } = completedResult; + + if (errors) { + await Promise.all( + requestListeners.map((l) => + l.didEncounterSubsequentErrors?.( + requestContext as GraphQLRequestContextDidEncounterSubsequentErrors, + errors, + ), + ), + ); + + return { + ...completedResult, + // Note that any `http` extensions in errors have no + // effect, because we've already sent the status code + // and response headers. + errors: formatErrors(errors).formattedErrors, + }; + } + + return completedResult; + }, + ); + } + + // Invoke hook, which is allowed to mutate payload if it really wants to. + await Promise.all( + requestListeners.map((l) => + l.willSendSubsequentPayload?.( + requestContext as GraphQLRequestContextWillSendSubsequentPayload, + payload, + ), + ), + ); + + yield payload; + } + } + async function invokeWillSendResponse() { await Promise.all( requestListeners.map((l) => From bd8d0c32de843f8a91e0bf3e1f56a92df4b8b36a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 18:05:55 -0600 Subject: [PATCH 19/93] Throw if version mismatch from header --- packages/server/src/runHttpQuery.ts | 44 +++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index aea6037ee76..fcd61085a65 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -17,7 +17,11 @@ import { MEDIA_TYPES, type SchemaDerivedData, } from './ApolloServer.js'; -import { type FormattedExecutionResult, Kind } from 'graphql'; +import { + type FormattedExecutionResult, + Kind, + version as graphqlVersion, +} from 'graphql'; import { BadRequestError } from './internalErrorClasses.js'; import Negotiator from 'negotiator'; import { HeaderMap } from './utils/HeaderMap.js'; @@ -292,7 +296,43 @@ export async function runHttpQuery({ 'Apollo server received an operation that uses incremental delivery ' + '(@defer or @stream), but the client does not accept multipart/mixed ' + 'HTTP responses. To enable incremental delivery support, add the HTTP ' + - "header 'Accept: multipart/mixed; incrementalDeliverySpec=20230621'.", + `header 'Accept: ${graphqlVersion === '17.0.0-alpha.2' ? MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL : MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_JUNE_2023}'.`, + // Use 406 Not Accepted + { extensions: { http: { status: 406 } } }, + ); + } + + if ( + graphqlVersion === '17.0.0-alpha.2' && + new Negotiator({ headers: { accept: acceptHeader } }).mediaType([ + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL, + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_JUNE_2023, + ]) !== MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL + ) { + // The client ran an operation that would yield multiple parts, but + // specified the wrong version. We return an error + throw new BadRequestError( + 'Apollo server received an operation that uses incremental delivery ' + + '(@defer or @stream) with a spec version incompatible with the the ' + + `installed version of graphql.js. Please use the HTTP header 'Accept: ${MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL}'.`, + // Use 406 Not Accepted + { extensions: { http: { status: 406 } } }, + ); + } + + if ( + graphqlVersion === '17.0.0-alpha.9' && + new Negotiator({ headers: { accept: acceptHeader } }).mediaType([ + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL, + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_JUNE_2023, + ]) !== MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_JUNE_2023 + ) { + // The client ran an operation that would yield multiple parts, but + // specified the wrong version. We return an error + throw new BadRequestError( + 'Apollo server received an operation that uses incremental delivery ' + + '(@defer or @stream) with a spec version incompatible with the the ' + + `installed version of graphql.js. Please use the HTTP header 'Accept: ${MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_JUNE_2023}'.`, // Use 406 Not Accepted { extensions: { http: { status: 406 } } }, ); From 1916a4660730f2b1d85fd3cf9f855014028108dc Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 18:16:21 -0600 Subject: [PATCH 20/93] Simplify check --- packages/server/src/runHttpQuery.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index fcd61085a65..92e19b4ead6 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -306,7 +306,6 @@ export async function runHttpQuery({ graphqlVersion === '17.0.0-alpha.2' && new Negotiator({ headers: { accept: acceptHeader } }).mediaType([ MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL, - MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_JUNE_2023, ]) !== MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL ) { // The client ran an operation that would yield multiple parts, but @@ -323,7 +322,6 @@ export async function runHttpQuery({ if ( graphqlVersion === '17.0.0-alpha.9' && new Negotiator({ headers: { accept: acceptHeader } }).mediaType([ - MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL, MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_JUNE_2023, ]) !== MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_JUNE_2023 ) { From 5ec9b5ed3470efe1063993bb03f0b42f153a067e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 18:20:05 -0600 Subject: [PATCH 21/93] Rename media types --- packages/server/src/ApolloServer.ts | 8 ++++---- packages/server/src/runHttpQuery.ts | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/server/src/ApolloServer.ts b/packages/server/src/ApolloServer.ts index b69665ab5f5..525a6e7aeed 100644 --- a/packages/server/src/ApolloServer.ts +++ b/packages/server/src/ApolloServer.ts @@ -1210,8 +1210,8 @@ export class ApolloServer { // by the order in the list we provide, so we put text/html last. MEDIA_TYPES.APPLICATION_JSON, MEDIA_TYPES.APPLICATION_GRAPHQL_RESPONSE_JSON, - MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_JUNE_2023, - MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL, + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9, + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2, MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC, MEDIA_TYPES.TEXT_HTML, ]) === MEDIA_TYPES.TEXT_HTML @@ -1407,8 +1407,8 @@ export const MEDIA_TYPES = { // We do *not* currently support this content-type; we will once incremental // delivery is part of the official GraphQL spec. MULTIPART_MIXED_NO_DEFER_SPEC: 'multipart/mixed', - MULTIPART_MIXED_EXPERIMENTAL: 'multipart/mixed; deferSpec=20220824', - MULTIPART_MIXED_EXPERIMENTAL_JUNE_2023: + MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2: 'multipart/mixed; deferSpec=20220824', + MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9: 'multipart/mixed; incrementalDeliverySpec=20230621', TEXT_HTML: 'text/html', }; diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index 92e19b4ead6..9e94a072a9a 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -286,8 +286,8 @@ export async function runHttpQuery({ // doesn't include the deferSpec parameter it will match this one here, // which isn't good enough. MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC, - MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL, - MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_JUNE_2023, + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2, + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9, ]) === MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC ) { // The client ran an operation that would yield multiple parts, but didn't @@ -296,7 +296,7 @@ export async function runHttpQuery({ 'Apollo server received an operation that uses incremental delivery ' + '(@defer or @stream), but the client does not accept multipart/mixed ' + 'HTTP responses. To enable incremental delivery support, add the HTTP ' + - `header 'Accept: ${graphqlVersion === '17.0.0-alpha.2' ? MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL : MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_JUNE_2023}'.`, + `header 'Accept: ${graphqlVersion === '17.0.0-alpha.2' ? MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2 : MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9}'.`, // Use 406 Not Accepted { extensions: { http: { status: 406 } } }, ); @@ -305,15 +305,15 @@ export async function runHttpQuery({ if ( graphqlVersion === '17.0.0-alpha.2' && new Negotiator({ headers: { accept: acceptHeader } }).mediaType([ - MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL, - ]) !== MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2, + ]) !== MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2 ) { // The client ran an operation that would yield multiple parts, but // specified the wrong version. We return an error throw new BadRequestError( 'Apollo server received an operation that uses incremental delivery ' + '(@defer or @stream) with a spec version incompatible with the the ' + - `installed version of graphql.js. Please use the HTTP header 'Accept: ${MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL}'.`, + `installed version of graphql.js. Please use the HTTP header 'Accept: ${MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2}'.`, // Use 406 Not Accepted { extensions: { http: { status: 406 } } }, ); @@ -322,15 +322,15 @@ export async function runHttpQuery({ if ( graphqlVersion === '17.0.0-alpha.9' && new Negotiator({ headers: { accept: acceptHeader } }).mediaType([ - MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_JUNE_2023, - ]) !== MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_JUNE_2023 + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9, + ]) !== MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9 ) { // The client ran an operation that would yield multiple parts, but // specified the wrong version. We return an error throw new BadRequestError( 'Apollo server received an operation that uses incremental delivery ' + '(@defer or @stream) with a spec version incompatible with the the ' + - `installed version of graphql.js. Please use the HTTP header 'Accept: ${MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_JUNE_2023}'.`, + `installed version of graphql.js. Please use the HTTP header 'Accept: ${MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9}'.`, // Use 406 Not Accepted { extensions: { http: { status: 406 } } }, ); From 5aa75783399ec39c888f80f77f9fbb106daf35ca Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 18:22:43 -0600 Subject: [PATCH 22/93] Change value of alpha9 spec version --- packages/server/src/ApolloServer.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/server/src/ApolloServer.ts b/packages/server/src/ApolloServer.ts index 525a6e7aeed..48b7705b2b0 100644 --- a/packages/server/src/ApolloServer.ts +++ b/packages/server/src/ApolloServer.ts @@ -1408,8 +1408,9 @@ export const MEDIA_TYPES = { // delivery is part of the official GraphQL spec. MULTIPART_MIXED_NO_DEFER_SPEC: 'multipart/mixed', MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2: 'multipart/mixed; deferSpec=20220824', + // spec version represents the commit hash of 17.0.0-alpha.9 MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9: - 'multipart/mixed; incrementalDeliverySpec=20230621', + 'multipart/mixed; incrementalDeliverySpec=3283f8a', TEXT_HTML: 'text/html', }; From 48e25721fd37a7a3d42213b60ca00c077c55da43 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 18:23:05 -0600 Subject: [PATCH 23/93] Change output header depending on graphql version --- packages/server/src/runHttpQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index 9e94a072a9a..7110f5307a9 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -338,7 +338,7 @@ export async function runHttpQuery({ graphQLResponse.http.headers.set( 'content-type', - 'multipart/mixed; boundary="-"; deferSpec=20220824', + `multipart/mixed; boundary="-"; ${graphqlVersion === '17.0.0-alpha.2' ? 'deferSpec=20220824' : 'incrementalDeliverySpec=3283f8a'}`, ); return { ...graphQLResponse.http, From c2403d56c707d22ecdf503aff9fb2777f6c4da5f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 18:30:37 -0600 Subject: [PATCH 24/93] Swap order of conditional --- packages/server/src/runHttpQuery.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index 7110f5307a9..2be7f3dae87 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -296,7 +296,7 @@ export async function runHttpQuery({ 'Apollo server received an operation that uses incremental delivery ' + '(@defer or @stream), but the client does not accept multipart/mixed ' + 'HTTP responses. To enable incremental delivery support, add the HTTP ' + - `header 'Accept: ${graphqlVersion === '17.0.0-alpha.2' ? MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2 : MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9}'.`, + `header 'Accept: ${graphqlVersion === '17.0.0-alpha.9' ? MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9 : MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2}'.`, // Use 406 Not Accepted { extensions: { http: { status: 406 } } }, ); @@ -338,7 +338,7 @@ export async function runHttpQuery({ graphQLResponse.http.headers.set( 'content-type', - `multipart/mixed; boundary="-"; ${graphqlVersion === '17.0.0-alpha.2' ? 'deferSpec=20220824' : 'incrementalDeliverySpec=3283f8a'}`, + `multipart/mixed; boundary="-"; ${graphqlVersion === '17.0.0-alpha.9' ? 'incrementalDeliverySpec=3283f8a' : 'deferSpec=20220824'}`, ); return { ...graphQLResponse.http, From b7ce9743be3dd6dec38c863f4aa0c577e499c20d Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 18:46:35 -0600 Subject: [PATCH 25/93] Enable incremental delivery tests for alpha9 --- packages/integration-testsuite/src/httpServerTests.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index 75e3177afb0..2f5c6f9ccc9 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -18,6 +18,7 @@ import { GraphQLSchema, GraphQLString, type ValidationContext, + version as graphqlVersion, } from 'graphql'; import gql from 'graphql-tag'; import { @@ -55,6 +56,8 @@ import { ApolloServerErrorCode, unwrapResolverError, } from '@apollo/server/errors'; +const IS_GRAPHQL_17_ALPHA2 = graphqlVersion === '17.0.0-alpha.2'; + const QueryRootType = new GraphQLObjectType({ name: 'QueryRoot', From 5c7bed1d58794a400535d0d4562352255f1ebd89 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 18:47:58 -0600 Subject: [PATCH 26/93] Wrap existing tests in check for alpha2 --- .../src/httpServerTests.ts | 182 ++++++++++-------- 1 file changed, 107 insertions(+), 75 deletions(-) diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index 2f5c6f9ccc9..a60454634fe 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -56,8 +56,8 @@ import { ApolloServerErrorCode, unwrapResolverError, } from '@apollo/server/errors'; -const IS_GRAPHQL_17_ALPHA2 = graphqlVersion === '17.0.0-alpha.2'; +const IS_GRAPHQL_17_ALPHA2 = graphqlVersion === '17.0.0-alpha.2'; const QueryRootType = new GraphQLObjectType({ name: 'QueryRoot', @@ -2312,22 +2312,23 @@ export function defineIntegrationTestSuiteHttpServerTests( }, }; - it.each([ - [undefined], - ['application/json'], - ['multipart/mixed'], - ['multipart/mixed; deferSpec=12345'], - ])('errors when @defer is used with accept: %s', async (accept) => { - const app = await createApp({ typeDefs, resolvers }); - const req = request(app).post('/'); - if (accept) { - req.set('accept', accept); - } - const res = await req.send({ - query: '{ ... @defer { testString } }', - }); - expect(res.status).toEqual(406); - expect(res.body).toMatchInlineSnapshot(` + if (IS_GRAPHQL_17_ALPHA2) { + it.each([ + [undefined], + ['application/json'], + ['multipart/mixed'], + ['multipart/mixed; deferSpec=12345'], + ])('errors when @defer is used with accept: %s', async (accept) => { + const app = await createApp({ typeDefs, resolvers }); + const req = request(app).post('/'); + if (accept) { + req.set('accept', accept); + } + const res = await req.send({ + query: '{ ... @defer { testString } }', + }); + expect(res.status).toEqual(406); + expect(res.body).toMatchInlineSnapshot(` { "errors": [ { @@ -2339,27 +2340,27 @@ export function defineIntegrationTestSuiteHttpServerTests( ], } `); - }); + }); - it.each([ - ['multipart/mixed; deferSpec=20220824'], - ['multipart/mixed; deferSpec=20220824, application/json'], - ['application/json, multipart/mixed; deferSpec=20220824'], - ])('basic @defer working with accept: %s', async (accept) => { - const app = await createApp({ typeDefs, resolvers }); - const res = await request(app) - .post('/') - .set('accept', accept) - // disables supertest's use of formidable for multipart - .parse(superagent.parse.text) - .send({ - query: '{ first: testString ... @defer { testString } }', - }); - expect(res.status).toEqual(200); - expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; deferSpec=20220824"`, - ); - expect(res.text).toEqual(`\r + it.each([ + ['multipart/mixed; deferSpec=20220824'], + ['multipart/mixed; deferSpec=20220824, application/json'], + ['application/json, multipart/mixed; deferSpec=20220824'], + ])('basic @defer working with accept: %s', async (accept) => { + const app = await createApp({ typeDefs, resolvers }); + const res = await request(app) + .post('/') + .set('accept', accept) + // disables supertest's use of formidable for multipart + .parse(superagent.parse.text) + .send({ + query: '{ first: testString ... @defer { testString } }', + }); + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; deferSpec=20220824"`, + ); + expect(res.text).toEqual(`\r ---\r content-type: application/json; charset=utf-8\r \r @@ -2370,46 +2371,76 @@ content-type: application/json; charset=utf-8\r {"hasNext":false,"incremental":[{"path":[],"data":{"testString":"it works"}}]}\r -----\r `); - }); + }); - it('first payload sent while deferred field is blocking', async () => { - const app = await createApp({ typeDefs, resolvers }); - const gotFirstChunkBarrier = resolvable(); - const resPromise = request(app) - .post('/') - .set( - 'accept', - 'multipart/mixed; deferSpec=20220824, application/json', - ) - .parse((res, fn) => { - res.text = ''; - res.setEncoding('utf8'); - res.on('data', (chunk) => { - res.text += chunk; - if ( - res.text.includes('it works') && - res.text.endsWith('---\r\n') - ) { - gotFirstChunkBarrier.resolve(); - } + it('errors if incompatible with installed graphql.js version', async () => { + const app = await createApp({ typeDefs, resolvers }); + const res = await request(app) + .post('/') + .set( + 'accept', + 'multipart/mixed; incrementalDeliverySpec=3283f8a', + ) + // disables supertest's use of formidable for multipart + .parse(superagent.parse.text) + .send({ + query: '{ first: testString ... @defer { testString } }', }); - res.on('end', fn); - }) - .send({ query: '{ testString ... @defer { barrierString } }' }) - // believe it or not, superagent uses `.then` to decide to actually send the request - .then((r) => r); - - // We ensure that the `barrierString` resolver isn't allowed to resolve - // until after we've gotten back a chunk containing the value of testString. - await gotFirstChunkBarrier; - barrierStringBarrier.resolve(); + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; deferSpec=20220824"`, + ); + expect(res.text).toEqual(`\r +---\r +content-type: application/json; charset=utf-8\r +\r +{"hasNext":true,"data":{"first":"it works"}}\r +---\r +content-type: application/json; charset=utf-8\r +\r +{"hasNext":false,"incremental":[{"path":[],"data":{"testString":"it works"}}]}\r +-----\r +`); + }); - const res = await resPromise; - expect(res.status).toEqual(200); - expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; deferSpec=20220824"`, - ); - expect(res.text).toMatchInlineSnapshot(` + it('first payload sent while deferred field is blocking', async () => { + const app = await createApp({ typeDefs, resolvers }); + const gotFirstChunkBarrier = resolvable(); + const resPromise = request(app) + .post('/') + .set( + 'accept', + 'multipart/mixed; deferSpec=20220824, application/json', + ) + .parse((res, fn) => { + res.text = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => { + res.text += chunk; + if ( + res.text.includes('it works') && + res.text.endsWith('---\r\n') + ) { + gotFirstChunkBarrier.resolve(); + } + }); + res.on('end', fn); + }) + .send({ query: '{ testString ... @defer { barrierString } }' }) + // believe it or not, superagent uses `.then` to decide to actually send the request + .then((r) => r); + + // We ensure that the `barrierString` resolver isn't allowed to resolve + // until after we've gotten back a chunk containing the value of testString. + await gotFirstChunkBarrier; + barrierStringBarrier.resolve(); + + const res = await resPromise; + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; deferSpec=20220824"`, + ); + expect(res.text).toMatchInlineSnapshot(` " --- content-type: application/json; charset=utf-8 @@ -2422,7 +2453,8 @@ content-type: application/json; charset=utf-8\r ----- " `); - }); + }); + } }); }, ); From 7df7889cae9ad416e267eac4658157a83bcfac81 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 19:16:05 -0600 Subject: [PATCH 27/93] Add back field ordering --- packages/server/src/runHttpQuery.ts | 101 +++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index 2be7f3dae87..807cea85ee9 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -1,9 +1,13 @@ import type { BaseContext, + GraphQLExperimentalFormattedCompletedResultAlpha9, + GraphQLExperimentalFormattedIncrementalResultAlpha2, + GraphQLExperimentalFormattedIncrementalResultAlpha9, GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2, GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha9, GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2, GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha9, + GraphQLExperimentalPendingResultAlpha9, GraphQLRequest, HTTPGraphQLHead, HTTPGraphQLRequest, @@ -370,12 +374,12 @@ async function* writeMultipartBody( // iterator is finished until we do async work. yield `\r\n---\r\ncontent-type: application/json; charset=utf-8\r\n\r\n${JSON.stringify( - initialResult, + orderInitialIncrementalExecutionResultFields(initialResult), )}\r\n---${initialResult.hasNext ? '' : '--'}\r\n`; for await (const result of subsequentResults) { yield `content-type: application/json; charset=utf-8\r\n\r\n${JSON.stringify( - result, + orderSubsequentIncrementalExecutionResultFields(result), )}\r\n---${result.hasNext ? '' : '--'}\r\n`; } } @@ -392,6 +396,99 @@ function orderExecutionResultFields( }; } +function orderInitialIncrementalExecutionResultFields( + result: + | GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2 + | GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha9, +): + | GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2 + | GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha9 { + if ('pending' in result) { + return { + hasNext: result.hasNext, + errors: result.errors, + data: result.data, + pending: orderPendingResultFields(result.pending), + extensions: result.extensions, + }; + } + + return { + hasNext: result.hasNext, + errors: result.errors, + data: result.data, + incremental: orderIncrementalResultFields(result.incremental), + extensions: result.extensions, + }; +} + +function orderPendingResultFields( + pending: readonly GraphQLExperimentalPendingResultAlpha9[] | undefined, +): GraphQLExperimentalPendingResultAlpha9[] | undefined { + return pending?.map((p) => ({ + id: p.id, + path: p.path, + label: p.label, + })); +} + +function orderSubsequentIncrementalExecutionResultFields( + result: + | GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2 + | GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha9, +): + | GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2 + | GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha9 { + if ('pending' in result || 'completed' in result) { + return { + hasNext: result.hasNext, + pending: orderPendingResultFields(result.pending), + incremental: orderIncrementalResultFields(result.incremental), + completed: orderCompletedResultFields(result.completed), + extensions: result.extensions, + }; + } + + return { + hasNext: result.hasNext, + incremental: orderIncrementalResultFields(result.incremental), + extensions: result.extensions, + }; +} + +function orderCompletedResultFields( + result: + | readonly GraphQLExperimentalFormattedCompletedResultAlpha9[] + | undefined, +): GraphQLExperimentalFormattedCompletedResultAlpha9[] | undefined { + return result?.map((c) => ({ + errors: c.errors, + id: c.id, + })); +} + +function orderIncrementalResultFields( + incremental?: + | readonly GraphQLExperimentalFormattedIncrementalResultAlpha2[] + | readonly GraphQLExperimentalFormattedIncrementalResultAlpha9[], +): + | undefined + | GraphQLExperimentalFormattedIncrementalResultAlpha2[] + | GraphQLExperimentalFormattedIncrementalResultAlpha9[] { + return incremental?.map((i: any) => { + return { + errors: i.errors, + path: i.path, + subPath: i.subPath, + label: i.label, + id: i.id, + data: i.data, + items: i.items, + extensions: i.extensions, + }; + }); +} + // The result of a curl does not appear well in the terminal, so we add an extra new line export function prettyJSONStringify(value: FormattedExecutionResult) { return JSON.stringify(value) + '\n'; From 758f50fe8c53f79beb065f8adf26fd6d232e1d6c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 24 Sep 2025 19:19:21 -0600 Subject: [PATCH 28/93] Enable incremental delivery tests for alpha9 --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 091e2b22837..2639d0f84a6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -80,6 +80,7 @@ jobs: docker: - image: cimg/base:stable environment: + INCREMENTAL_DELIVERY_TESTS_ENABLED: t GRAPHQL_JS_VERSION: 17.0.0-alpha.9 steps: - setup-node: From c6566c4111728f2e3cdfc2f8230f3daf3307f9d4 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 09:47:47 -0600 Subject: [PATCH 29/93] Reverse demorgan's --- packages/server/src/runHttpQuery.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index 807cea85ee9..0b7dcd736f3 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -284,15 +284,17 @@ export async function runHttpQuery({ // anything has changed). const acceptHeader = httpRequest.headers.get('accept'); if ( - !acceptHeader || - new Negotiator({ headers: { accept: acceptHeader } }).mediaType([ - // mediaType() will return the first one that matches, so if the client - // doesn't include the deferSpec parameter it will match this one here, - // which isn't good enough. - MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC, - MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2, - MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9, - ]) === MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC + !( + acceptHeader && + new Negotiator({ headers: { accept: acceptHeader } }).mediaType([ + // mediaType() will return the first one that matches, so if the client + // doesn't include the deferSpec parameter it will match this one here, + // which isn't good enough. + MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC, + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2, + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9, + ]) !== MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC + ) ) { // The client ran an operation that would yield multiple parts, but didn't // specify `accept: multipart/mixed`. We return an error. From e34f9110fd847038d687e544816b565dfdf8ceff Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 09:56:03 -0600 Subject: [PATCH 30/93] Extract variable --- packages/server/src/runHttpQuery.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index 0b7dcd736f3..6a6c8219de6 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -283,10 +283,11 @@ export async function runHttpQuery({ // without `deferSpec` as well (perhaps with slightly different behavior if // anything has changed). const acceptHeader = httpRequest.headers.get('accept'); + const negotiator = new Negotiator({ headers: { accept: acceptHeader } }); if ( !( acceptHeader && - new Negotiator({ headers: { accept: acceptHeader } }).mediaType([ + negotiator.mediaType([ // mediaType() will return the first one that matches, so if the client // doesn't include the deferSpec parameter it will match this one here, // which isn't good enough. @@ -310,9 +311,8 @@ export async function runHttpQuery({ if ( graphqlVersion === '17.0.0-alpha.2' && - new Negotiator({ headers: { accept: acceptHeader } }).mediaType([ - MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2, - ]) !== MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2 + negotiator.mediaType([MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2]) !== + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2 ) { // The client ran an operation that would yield multiple parts, but // specified the wrong version. We return an error @@ -327,9 +327,8 @@ export async function runHttpQuery({ if ( graphqlVersion === '17.0.0-alpha.9' && - new Negotiator({ headers: { accept: acceptHeader } }).mediaType([ - MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9, - ]) !== MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9 + negotiator.mediaType([MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9]) !== + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9 ) { // The client ran an operation that would yield multiple parts, but // specified the wrong version. We return an error From 7b1f7af1bab0a8d2869e8f139332ddb81a198194 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 10:05:52 -0600 Subject: [PATCH 31/93] Update test --- .../src/httpServerTests.ts | 67 +++++++++---------- 1 file changed, 30 insertions(+), 37 deletions(-) diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index a60454634fe..bd3d3d86f27 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -2375,32 +2375,25 @@ content-type: application/json; charset=utf-8\r it('errors if incompatible with installed graphql.js version', async () => { const app = await createApp({ typeDefs, resolvers }); - const res = await request(app) + const req = request(app) .post('/') - .set( - 'accept', - 'multipart/mixed; incrementalDeliverySpec=3283f8a', - ) - // disables supertest's use of formidable for multipart - .parse(superagent.parse.text) - .send({ - query: '{ first: testString ... @defer { testString } }', - }); - expect(res.status).toEqual(200); - expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; deferSpec=20220824"`, - ); - expect(res.text).toEqual(`\r ----\r -content-type: application/json; charset=utf-8\r -\r -{"hasNext":true,"data":{"first":"it works"}}\r ----\r -content-type: application/json; charset=utf-8\r -\r -{"hasNext":false,"incremental":[{"path":[],"data":{"testString":"it works"}}]}\r ------\r -`); + .set('accept', 'incrementalDeliverySpec=3283f8a'); + const res = await req.send({ + query: '{ ... @defer { testString } }', + }); + expect(res.status).toEqual(406); + expect(res.body).toMatchInlineSnapshot(` + { + "errors": [ + { + "extensions": { + "code": "BAD_REQUEST", + }, + "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream) with a spec version incompatible with the the installed version of graphql.js. Please use the HTTP header 'Accept: multipart/mixed; deferSpec=20220824'.", + }, + ], + } + `); }); it('first payload sent while deferred field is blocking', async () => { @@ -2441,18 +2434,18 @@ content-type: application/json; charset=utf-8\r `"multipart/mixed; boundary="-"; deferSpec=20220824"`, ); expect(res.text).toMatchInlineSnapshot(` - " - --- - content-type: application/json; charset=utf-8 - - {"hasNext":true,"data":{"testString":"it works"}} - --- - content-type: application/json; charset=utf-8 - - {"hasNext":false,"incremental":[{"path":[],"data":{"barrierString":"we waited"}}]} - ----- - " - `); + " + --- + content-type: application/json; charset=utf-8 + + {"hasNext":true,"data":{"testString":"it works"}} + --- + content-type: application/json; charset=utf-8 + + {"hasNext":false,"incremental":[{"path":[],"data":{"barrierString":"we waited"}}]} + ----- + " + `); }); } }); From 6437c0e51df68051f68439d8ccfe48c616b7fb21 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 10:12:57 -0600 Subject: [PATCH 32/93] Fix condition --- packages/server/src/runHttpQuery.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index 6a6c8219de6..891e9cab89b 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -284,17 +284,25 @@ export async function runHttpQuery({ // anything has changed). const acceptHeader = httpRequest.headers.get('accept'); const negotiator = new Negotiator({ headers: { accept: acceptHeader } }); + const validMediaType = + graphqlVersion === '17.0.0-alpha.9' + ? MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9 + : MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2; + const preferredMediaType = negotiator.mediaType([ + // mediaType() will return the first one that matches, so if the client + // doesn't include the deferSpec parameter it will match this one here, + // which isn't good enough. + MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC, + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2, + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9, + ]); + if ( !( acceptHeader && - negotiator.mediaType([ - // mediaType() will return the first one that matches, so if the client - // doesn't include the deferSpec parameter it will match this one here, - // which isn't good enough. - MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC, - MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2, - MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9, - ]) !== MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC + (preferredMediaType === + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2 || + preferredMediaType === MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9) ) ) { // The client ran an operation that would yield multiple parts, but didn't From 14c92631b60e525c3cd151a02b37ee15e35d2b70 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 10:13:05 -0600 Subject: [PATCH 33/93] Combine conditionals --- packages/server/src/runHttpQuery.ts | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index 891e9cab89b..f1113b64242 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -311,39 +311,19 @@ export async function runHttpQuery({ 'Apollo server received an operation that uses incremental delivery ' + '(@defer or @stream), but the client does not accept multipart/mixed ' + 'HTTP responses. To enable incremental delivery support, add the HTTP ' + - `header 'Accept: ${graphqlVersion === '17.0.0-alpha.9' ? MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9 : MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2}'.`, + `header 'Accept: ${validMediaType}'.`, // Use 406 Not Accepted { extensions: { http: { status: 406 } } }, ); } - if ( - graphqlVersion === '17.0.0-alpha.2' && - negotiator.mediaType([MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2]) !== - MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2 - ) { - // The client ran an operation that would yield multiple parts, but - // specified the wrong version. We return an error - throw new BadRequestError( - 'Apollo server received an operation that uses incremental delivery ' + - '(@defer or @stream) with a spec version incompatible with the the ' + - `installed version of graphql.js. Please use the HTTP header 'Accept: ${MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2}'.`, - // Use 406 Not Accepted - { extensions: { http: { status: 406 } } }, - ); - } - - if ( - graphqlVersion === '17.0.0-alpha.9' && - negotiator.mediaType([MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9]) !== - MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9 - ) { + if (negotiator.mediaType([validMediaType]) !== validMediaType) { // The client ran an operation that would yield multiple parts, but // specified the wrong version. We return an error throw new BadRequestError( 'Apollo server received an operation that uses incremental delivery ' + '(@defer or @stream) with a spec version incompatible with the the ' + - `installed version of graphql.js. Please use the HTTP header 'Accept: ${MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9}'.`, + `installed version of graphql.js. Please use the HTTP header 'Accept: ${validMediaType}'.`, // Use 406 Not Accepted { extensions: { http: { status: 406 } } }, ); From b3469ab1af6d61956607d984d8befc1eb9cd0dba Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 10:13:17 -0600 Subject: [PATCH 34/93] Use valid media type value for response --- packages/server/src/runHttpQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index f1113b64242..b1094a99069 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -331,7 +331,7 @@ export async function runHttpQuery({ graphQLResponse.http.headers.set( 'content-type', - `multipart/mixed; boundary="-"; ${graphqlVersion === '17.0.0-alpha.9' ? 'incrementalDeliverySpec=3283f8a' : 'deferSpec=20220824'}`, + `multipart/mixed; boundary="-"; ${validMediaType.replace('multipart/mixed; ', '')}`, ); return { ...graphQLResponse.http, From e486ca10ac4130e2a5bd1196b9c0b97b3e65e3a1 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 10:19:31 -0600 Subject: [PATCH 35/93] DeMorgan's law --- packages/server/src/runHttpQuery.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index b1094a99069..b657368cabf 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -298,12 +298,9 @@ export async function runHttpQuery({ ]); if ( - !( - acceptHeader && - (preferredMediaType === - MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2 || - preferredMediaType === MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9) - ) + !acceptHeader || + (preferredMediaType !== MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2 && + preferredMediaType !== MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9) ) { // The client ran an operation that would yield multiple parts, but didn't // specify `accept: multipart/mixed`. We return an error. From c77e9f0a259c99bccb924cbb88ad635941dd8835 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 10:21:31 -0600 Subject: [PATCH 36/93] Fix incorrect header in test --- packages/integration-testsuite/src/httpServerTests.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index bd3d3d86f27..93dcfe4f779 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -2377,7 +2377,10 @@ content-type: application/json; charset=utf-8\r const app = await createApp({ typeDefs, resolvers }); const req = request(app) .post('/') - .set('accept', 'incrementalDeliverySpec=3283f8a'); + .set( + 'accept', + 'multipart/mixed; incrementalDeliverySpec=3283f8a', + ); const res = await req.send({ query: '{ ... @defer { testString } }', }); From ec029b02e99c1fe39d5d40f11a425d331d53c3da Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 10:22:09 -0600 Subject: [PATCH 37/93] Move test --- .../src/httpServerTests.ts | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index 93dcfe4f779..70f9ed8253f 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -2342,6 +2342,32 @@ export function defineIntegrationTestSuiteHttpServerTests( `); }); + it('errors if incompatible with installed graphql.js version', async () => { + const app = await createApp({ typeDefs, resolvers }); + const req = request(app) + .post('/') + .set( + 'accept', + 'multipart/mixed; incrementalDeliverySpec=3283f8a', + ); + const res = await req.send({ + query: '{ ... @defer { testString } }', + }); + expect(res.status).toEqual(406); + expect(res.body).toMatchInlineSnapshot(` + { + "errors": [ + { + "extensions": { + "code": "BAD_REQUEST", + }, + "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream) with a spec version incompatible with the the installed version of graphql.js. Please use the HTTP header 'Accept: multipart/mixed; deferSpec=20220824'.", + }, + ], + } + `); + }); + it.each([ ['multipart/mixed; deferSpec=20220824'], ['multipart/mixed; deferSpec=20220824, application/json'], @@ -2373,32 +2399,6 @@ content-type: application/json; charset=utf-8\r `); }); - it('errors if incompatible with installed graphql.js version', async () => { - const app = await createApp({ typeDefs, resolvers }); - const req = request(app) - .post('/') - .set( - 'accept', - 'multipart/mixed; incrementalDeliverySpec=3283f8a', - ); - const res = await req.send({ - query: '{ ... @defer { testString } }', - }); - expect(res.status).toEqual(406); - expect(res.body).toMatchInlineSnapshot(` - { - "errors": [ - { - "extensions": { - "code": "BAD_REQUEST", - }, - "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream) with a spec version incompatible with the the installed version of graphql.js. Please use the HTTP header 'Accept: multipart/mixed; deferSpec=20220824'.", - }, - ], - } - `); - }); - it('first payload sent while deferred field is blocking', async () => { const app = await createApp({ typeDefs, resolvers }); const gotFirstChunkBarrier = resolvable(); From c8932db71d0e9045dad220c637018d5621d931fe Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 10:23:32 -0600 Subject: [PATCH 38/93] Fix typo --- packages/server/src/runHttpQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index b657368cabf..882687120d3 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -319,7 +319,7 @@ export async function runHttpQuery({ // specified the wrong version. We return an error throw new BadRequestError( 'Apollo server received an operation that uses incremental delivery ' + - '(@defer or @stream) with a spec version incompatible with the the ' + + '(@defer or @stream) with a spec version incompatible with the ' + `installed version of graphql.js. Please use the HTTP header 'Accept: ${validMediaType}'.`, // Use 406 Not Accepted { extensions: { http: { status: 406 } } }, From 016fbb9cbf16556d1088481ce1ffa70a501ea3f5 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 10:25:09 -0600 Subject: [PATCH 39/93] Tweak error message --- packages/integration-testsuite/src/httpServerTests.ts | 2 +- packages/server/src/runHttpQuery.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index 70f9ed8253f..7ef7f320353 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -2361,7 +2361,7 @@ export function defineIntegrationTestSuiteHttpServerTests( "extensions": { "code": "BAD_REQUEST", }, - "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream) with a spec version incompatible with the the installed version of graphql.js. Please use the HTTP header 'Accept: multipart/mixed; deferSpec=20220824'.", + "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream) with a spec version incompatible with this server. Please use the HTTP header 'Accept: multipart/mixed; deferSpec=20220824'.", }, ], } diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index 882687120d3..cf0a3a3b096 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -319,8 +319,8 @@ export async function runHttpQuery({ // specified the wrong version. We return an error throw new BadRequestError( 'Apollo server received an operation that uses incremental delivery ' + - '(@defer or @stream) with a spec version incompatible with the ' + - `installed version of graphql.js. Please use the HTTP header 'Accept: ${validMediaType}'.`, + '(@defer or @stream) with a spec version incompatible with this server. ' + + `Please use the HTTP header 'Accept: ${validMediaType}'.`, // Use 406 Not Accepted { extensions: { http: { status: 406 } } }, ); From caada2785180926e1ce189330a6d02871b7f93ea Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 10:35:20 -0600 Subject: [PATCH 40/93] Use describe instead --- .../src/httpServerTests.ts | 192 +++++++++--------- 1 file changed, 100 insertions(+), 92 deletions(-) diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index 7ef7f320353..11c233e0db1 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -2312,23 +2312,27 @@ export function defineIntegrationTestSuiteHttpServerTests( }, }; - if (IS_GRAPHQL_17_ALPHA2) { - it.each([ - [undefined], - ['application/json'], - ['multipart/mixed'], - ['multipart/mixed; deferSpec=12345'], - ])('errors when @defer is used with accept: %s', async (accept) => { - const app = await createApp({ typeDefs, resolvers }); - const req = request(app).post('/'); - if (accept) { - req.set('accept', accept); - } - const res = await req.send({ - query: '{ ... @defer { testString } }', - }); - expect(res.status).toEqual(406); - expect(res.body).toMatchInlineSnapshot(` + (IS_GRAPHQL_17_ALPHA2 ? describe : describe.skip)( + 'tests that require graphql@17.0.0-alpha.2', + () => { + it.each([ + [undefined], + ['application/json'], + ['multipart/mixed'], + ['multipart/mixed; deferSpec=12345'], + ])( + 'errors when @defer is used with accept: %s', + async (accept) => { + const app = await createApp({ typeDefs, resolvers }); + const req = request(app).post('/'); + if (accept) { + req.set('accept', accept); + } + const res = await req.send({ + query: '{ ... @defer { testString } }', + }); + expect(res.status).toEqual(406); + expect(res.body).toMatchInlineSnapshot(` { "errors": [ { @@ -2340,21 +2344,22 @@ export function defineIntegrationTestSuiteHttpServerTests( ], } `); - }); + }, + ); - it('errors if incompatible with installed graphql.js version', async () => { - const app = await createApp({ typeDefs, resolvers }); - const req = request(app) - .post('/') - .set( - 'accept', - 'multipart/mixed; incrementalDeliverySpec=3283f8a', - ); - const res = await req.send({ - query: '{ ... @defer { testString } }', - }); - expect(res.status).toEqual(406); - expect(res.body).toMatchInlineSnapshot(` + it('errors if incompatible with installed graphql.js version', async () => { + const app = await createApp({ typeDefs, resolvers }); + const req = request(app) + .post('/') + .set( + 'accept', + 'multipart/mixed; incrementalDeliverySpec=3283f8a', + ); + const res = await req.send({ + query: '{ ... @defer { testString } }', + }); + expect(res.status).toEqual(406); + expect(res.body).toMatchInlineSnapshot(` { "errors": [ { @@ -2366,27 +2371,27 @@ export function defineIntegrationTestSuiteHttpServerTests( ], } `); - }); + }); - it.each([ - ['multipart/mixed; deferSpec=20220824'], - ['multipart/mixed; deferSpec=20220824, application/json'], - ['application/json, multipart/mixed; deferSpec=20220824'], - ])('basic @defer working with accept: %s', async (accept) => { - const app = await createApp({ typeDefs, resolvers }); - const res = await request(app) - .post('/') - .set('accept', accept) - // disables supertest's use of formidable for multipart - .parse(superagent.parse.text) - .send({ - query: '{ first: testString ... @defer { testString } }', - }); - expect(res.status).toEqual(200); - expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; deferSpec=20220824"`, - ); - expect(res.text).toEqual(`\r + it.each([ + ['multipart/mixed; deferSpec=20220824'], + ['multipart/mixed; deferSpec=20220824, application/json'], + ['application/json, multipart/mixed; deferSpec=20220824'], + ])('basic @defer working with accept: %s', async (accept) => { + const app = await createApp({ typeDefs, resolvers }); + const res = await request(app) + .post('/') + .set('accept', accept) + // disables supertest's use of formidable for multipart + .parse(superagent.parse.text) + .send({ + query: '{ first: testString ... @defer { testString } }', + }); + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; deferSpec=20220824"`, + ); + expect(res.text).toEqual(`\r ---\r content-type: application/json; charset=utf-8\r \r @@ -2397,46 +2402,48 @@ content-type: application/json; charset=utf-8\r {"hasNext":false,"incremental":[{"path":[],"data":{"testString":"it works"}}]}\r -----\r `); - }); + }); - it('first payload sent while deferred field is blocking', async () => { - const app = await createApp({ typeDefs, resolvers }); - const gotFirstChunkBarrier = resolvable(); - const resPromise = request(app) - .post('/') - .set( - 'accept', - 'multipart/mixed; deferSpec=20220824, application/json', - ) - .parse((res, fn) => { - res.text = ''; - res.setEncoding('utf8'); - res.on('data', (chunk) => { - res.text += chunk; - if ( - res.text.includes('it works') && - res.text.endsWith('---\r\n') - ) { - gotFirstChunkBarrier.resolve(); - } - }); - res.on('end', fn); - }) - .send({ query: '{ testString ... @defer { barrierString } }' }) - // believe it or not, superagent uses `.then` to decide to actually send the request - .then((r) => r); - - // We ensure that the `barrierString` resolver isn't allowed to resolve - // until after we've gotten back a chunk containing the value of testString. - await gotFirstChunkBarrier; - barrierStringBarrier.resolve(); - - const res = await resPromise; - expect(res.status).toEqual(200); - expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; deferSpec=20220824"`, - ); - expect(res.text).toMatchInlineSnapshot(` + it('first payload sent while deferred field is blocking', async () => { + const app = await createApp({ typeDefs, resolvers }); + const gotFirstChunkBarrier = resolvable(); + const resPromise = request(app) + .post('/') + .set( + 'accept', + 'multipart/mixed; deferSpec=20220824, application/json', + ) + .parse((res, fn) => { + res.text = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => { + res.text += chunk; + if ( + res.text.includes('it works') && + res.text.endsWith('---\r\n') + ) { + gotFirstChunkBarrier.resolve(); + } + }); + res.on('end', fn); + }) + .send({ + query: '{ testString ... @defer { barrierString } }', + }) + // believe it or not, superagent uses `.then` to decide to actually send the request + .then((r) => r); + + // We ensure that the `barrierString` resolver isn't allowed to resolve + // until after we've gotten back a chunk containing the value of testString. + await gotFirstChunkBarrier; + barrierStringBarrier.resolve(); + + const res = await resPromise; + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; deferSpec=20220824"`, + ); + expect(res.text).toMatchInlineSnapshot(` " --- content-type: application/json; charset=utf-8 @@ -2449,8 +2456,9 @@ content-type: application/json; charset=utf-8\r ----- " `); - }); - } + }); + }, + ); }); }, ); From 0610c858ca154e3f1be1bbbe448eb850ccb6140e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 10:38:04 -0600 Subject: [PATCH 41/93] Add tests for alpha.9 --- .../src/httpServerTests.ts | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index 11c233e0db1..2dd7a50fd00 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -58,6 +58,7 @@ import { } from '@apollo/server/errors'; const IS_GRAPHQL_17_ALPHA2 = graphqlVersion === '17.0.0-alpha.2'; +const IS_GRAPHQL_17_ALPHA9 = graphqlVersion === '17.0.0-alpha.9'; const QueryRootType = new GraphQLObjectType({ name: 'QueryRoot', @@ -2459,6 +2460,158 @@ content-type: application/json; charset=utf-8\r }); }, ); + + (IS_GRAPHQL_17_ALPHA9 ? describe : describe.skip)( + 'tests that require graphql@17.0.0-alpha.9', + () => { + it.each([ + [undefined], + ['application/json'], + ['multipart/mixed'], + ['multipart/mixed; deferSpec=12345'], + ])( + 'errors when @defer is used with accept: %s', + async (accept) => { + const app = await createApp({ typeDefs, resolvers }); + const req = request(app).post('/'); + if (accept) { + req.set('accept', accept); + } + const res = await req.send({ + query: '{ ... @defer { testString } }', + }); + expect(res.status).toEqual(406); + expect(res.body).toMatchInlineSnapshot(` + { + "errors": [ + { + "extensions": { + "code": "BAD_REQUEST", + }, + "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream), but the client does not accept multipart/mixed HTTP responses. To enable incremental delivery support, add the HTTP header 'Accept: multipart/mixed; incrementalDeliverySpec=3283f8a'.", + }, + ], + } + `); + }, + ); + + it('errors if incompatible with installed graphql.js version', async () => { + const app = await createApp({ typeDefs, resolvers }); + const req = request(app) + .post('/') + .set( + 'accept', + 'multipart/mixed; incrementalDeliverySpec=3283f8a', + ); + const res = await req.send({ + query: '{ ... @defer { testString } }', + }); + expect(res.status).toEqual(406); + expect(res.body).toMatchInlineSnapshot(` + { + "errors": [ + { + "extensions": { + "code": "BAD_REQUEST", + }, + "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream) with a spec version incompatible with this server. Please use the HTTP header 'Accept: multipart/mixed; incrementalDeliverySpec=3283f8a'.", + }, + ], + } + `); + }); + + it.each([ + ['multipart/mixed; incrementalDeliverySpec=3283f8a'], + [ + 'multipart/mixed; incrementalDeliverySpec=3283f8a, application/json', + ], + [ + 'application/json, multipart/mixed; incrementalDeliverySpec=3283f8a', + ], + ])('basic @defer working with accept: %s', async (accept) => { + const app = await createApp({ typeDefs, resolvers }); + const res = await request(app) + .post('/') + .set('accept', accept) + // disables supertest's use of formidable for multipart + .parse(superagent.parse.text) + .send({ + query: '{ first: testString ... @defer { testString } }', + }); + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, + ); + expect(res.text).toEqual(`\r +---\r +content-type: application/json; charset=utf-8\r +\r +{"hasNext":true,"data":{"first":"it works"}}\r +---\r +content-type: application/json; charset=utf-8\r +\r +{"hasNext":false,"incremental":[{"path":[],"data":{"testString":"it works"}}]}\r +-----\r +`); + }); + + it('first payload sent while deferred field is blocking', async () => { + const app = await createApp({ typeDefs, resolvers }); + const gotFirstChunkBarrier = resolvable(); + const resPromise = request(app) + .post('/') + .set( + 'accept', + 'multipart/mixed; incrementalDeliverySpec=3283f8a, application/json', + ) + .parse((res, fn) => { + res.text = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => { + res.text += chunk; + if ( + res.text.includes('it works') && + res.text.endsWith('---\r\n') + ) { + gotFirstChunkBarrier.resolve(); + } + }); + res.on('end', fn); + }) + .send({ + query: '{ testString ... @defer { barrierString } }', + }) + // believe it or not, superagent uses `.then` to decide to actually send the request + .then((r) => r); + + // We ensure that the `barrierString` resolver isn't allowed to resolve + // until after we've gotten back a chunk containing the value of testString. + await gotFirstChunkBarrier; + barrierStringBarrier.resolve(); + + const res = await resPromise; + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, + ); + expect(res.text).toMatchInlineSnapshot(` + " + --- + content-type: application/json; charset=utf-8 + + {"hasNext":true,"data":{"testString":"it works"}} + --- + content-type: application/json; charset=utf-8 + + {"hasNext":false,"incremental":[{"path":[],"data":{"barrierString":"we waited"}}]} + ----- + " + `); + }); + }, + ); }); }, ); From 7b8d287e3d86461793dd5b84338801b1e72bb849 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 10:44:46 -0600 Subject: [PATCH 42/93] Fix incorrect assertions for alpha.9 --- .../src/httpServerTests.ts | 76 +++++++++---------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index 2dd7a50fd00..8c4c723610f 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -2352,26 +2352,23 @@ export function defineIntegrationTestSuiteHttpServerTests( const app = await createApp({ typeDefs, resolvers }); const req = request(app) .post('/') - .set( - 'accept', - 'multipart/mixed; incrementalDeliverySpec=3283f8a', - ); + .set('accept', 'multipart/mixed; deferSpec=20220824'); const res = await req.send({ query: '{ ... @defer { testString } }', }); expect(res.status).toEqual(406); expect(res.body).toMatchInlineSnapshot(` - { - "errors": [ - { - "extensions": { - "code": "BAD_REQUEST", - }, - "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream) with a spec version incompatible with this server. Please use the HTTP header 'Accept: multipart/mixed; deferSpec=20220824'.", - }, - ], - } - `); + { + "errors": [ + { + "extensions": { + "code": "BAD_REQUEST", + }, + "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream) with a spec version incompatible with this server. Please use the HTTP header 'Accept: multipart/mixed; deferSpec=20220824'.", + }, + ], + } + `); }); it.each([ @@ -2500,26 +2497,23 @@ content-type: application/json; charset=utf-8\r const app = await createApp({ typeDefs, resolvers }); const req = request(app) .post('/') - .set( - 'accept', - 'multipart/mixed; incrementalDeliverySpec=3283f8a', - ); + .set('accept', 'multipart/mixed; deferSpec=20220824'); const res = await req.send({ query: '{ ... @defer { testString } }', }); expect(res.status).toEqual(406); expect(res.body).toMatchInlineSnapshot(` - { - "errors": [ - { - "extensions": { - "code": "BAD_REQUEST", - }, - "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream) with a spec version incompatible with this server. Please use the HTTP header 'Accept: multipart/mixed; incrementalDeliverySpec=3283f8a'.", - }, - ], - } - `); + { + "errors": [ + { + "extensions": { + "code": "BAD_REQUEST", + }, + "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream) with a spec version incompatible with this server. Please use the HTTP header 'Accept: multipart/mixed; incrementalDeliverySpec=3283f8a'.", + }, + ], + } + `); }); it.each([ @@ -2548,11 +2542,11 @@ content-type: application/json; charset=utf-8\r ---\r content-type: application/json; charset=utf-8\r \r -{"hasNext":true,"data":{"first":"it works"}}\r +{"hasNext":true,"data":{"first":"it works"},"pending":[{"id":"0","path":[]}]}\r ---\r content-type: application/json; charset=utf-8\r \r -{"hasNext":false,"incremental":[{"path":[],"data":{"testString":"it works"}}]}\r +{"hasNext":false,"incremental":[{"id":"0","data":{"testString":"it works"}}],"completed":[{"id":"0"}]}\r -----\r `); }); @@ -2597,17 +2591,17 @@ content-type: application/json; charset=utf-8\r `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, ); expect(res.text).toMatchInlineSnapshot(` - " - --- - content-type: application/json; charset=utf-8 + " + --- + content-type: application/json; charset=utf-8 - {"hasNext":true,"data":{"testString":"it works"}} - --- - content-type: application/json; charset=utf-8 + {"hasNext":true,"data":{"testString":"it works"},"pending":[{"id":"0","path":[]}]} + --- + content-type: application/json; charset=utf-8 - {"hasNext":false,"incremental":[{"path":[],"data":{"barrierString":"we waited"}}]} - ----- - " + {"hasNext":false,"incremental":[{"id":"0","data":{"barrierString":"we waited"}}],"completed":[{"id":"0"}]} + ----- + " `); }); }, From ecd31cbfec0a88622c61b441568b50f29683c17f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 10:53:28 -0600 Subject: [PATCH 43/93] Update test for alpha.9 --- .../src/httpServerTests.ts | 210 ++++++++++++------ 1 file changed, 144 insertions(+), 66 deletions(-) diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index 8c4c723610f..e33c940029b 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -2218,75 +2218,153 @@ export function defineIntegrationTestSuiteHttpServerTests( `); }); - it('first payload sent while deferred field is blocking', async () => { - const gotFirstChunkBarrier = resolvable(); - const sendSecondChunkBarrier = resolvable(); - const app = await createApp({ - typeDefs, - __testing_incrementalExecutionResults: { - initialResult: { - hasNext: true, - data: { testString: 'it works' }, + (IS_GRAPHQL_17_ALPHA2 ? it : it.skip)( + 'first payload sent while deferred field is blocking graphql@17.0.0-alpha.2', + async () => { + const gotFirstChunkBarrier = resolvable(); + const sendSecondChunkBarrier = resolvable(); + const app = await createApp({ + typeDefs, + __testing_incrementalExecutionResults: { + initialResult: { + hasNext: true, + data: { testString: 'it works' }, + }, + subsequentResults: (async function* () { + await sendSecondChunkBarrier; + yield { + hasNext: false, + incremental: [ + { path: [], data: { barrierString: 'we waited' } }, + ], + }; + })(), }, - subsequentResults: (async function* () { - await sendSecondChunkBarrier; - yield { - hasNext: false, - incremental: [ - { path: [], data: { barrierString: 'we waited' } }, - ], - }; - })(), - }, - }); - const resPromise = request(app) - .post('/') - .set( - 'accept', - 'multipart/mixed; deferSpec=20220824, application/json', - ) - .parse((res, fn) => { - res.text = ''; - res.setEncoding('utf8'); - res.on('data', (chunk) => { - res.text += chunk; - if ( - res.text.includes('it works') && - res.text.endsWith('---\r\n') - ) { - gotFirstChunkBarrier.resolve(); - } - }); - res.on('end', fn); - }) - .send({ query: '{ testString ... @defer { barrierString } }' }) - // believe it or not, superagent uses `.then` to decide to actually send the request - .then((r) => r); - - // We ensure that the second chunk can't be sent until after we've - // gotten back a chunk containing the value of testString. - await gotFirstChunkBarrier; - sendSecondChunkBarrier.resolve(); - - const res = await resPromise; - expect(res.status).toEqual(200); - expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; deferSpec=20220824"`, - ); - expect(res.text).toMatchInlineSnapshot(` - " - --- - content-type: application/json; charset=utf-8 + }); + const resPromise = request(app) + .post('/') + .set( + 'accept', + `multipart/mixed; deferSpec=20220824, application/json`, + ) + .parse((res, fn) => { + res.text = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => { + res.text += chunk; + if ( + res.text.includes('it works') && + res.text.endsWith('---\r\n') + ) { + gotFirstChunkBarrier.resolve(); + } + }); + res.on('end', fn); + }) + .send({ query: '{ testString ... @defer { barrierString } }' }) + // believe it or not, superagent uses `.then` to decide to actually send the request + .then((r) => r); + + // We ensure that the second chunk can't be sent until after we've + // gotten back a chunk containing the value of testString. + await gotFirstChunkBarrier; + sendSecondChunkBarrier.resolve(); + + const res = await resPromise; + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; deferSpec=20220824"`, + ); + expect(res.text).toMatchInlineSnapshot(` + " + --- + content-type: application/json; charset=utf-8 - {"hasNext":true,"data":{"testString":"it works"}} - --- - content-type: application/json; charset=utf-8 + {"hasNext":true,"data":{"testString":"it works"}} + --- + content-type: application/json; charset=utf-8 - {"hasNext":false,"incremental":[{"path":[],"data":{"barrierString":"we waited"}}]} - ----- - " - `); - }); + {"hasNext":false,"incremental":[{"path":[],"data":{"barrierString":"we waited"}}]} + ----- + " + `); + }, + ); + + (IS_GRAPHQL_17_ALPHA9 ? it : it.skip)( + 'first payload sent while deferred field is blocking graphql@17.0.0-alpha.9', + async () => { + const gotFirstChunkBarrier = resolvable(); + const sendSecondChunkBarrier = resolvable(); + const app = await createApp({ + typeDefs, + __testing_incrementalExecutionResults: { + initialResult: { + hasNext: true, + pending: [{ id: '0', path: [] }], + data: { testString: 'it works' }, + }, + subsequentResults: (async function* () { + await sendSecondChunkBarrier; + yield { + hasNext: false, + incremental: [ + { id: '0', data: { barrierString: 'we waited' } }, + ], + completed: [{ id: '0' }], + }; + })(), + }, + }); + const resPromise = request(app) + .post('/') + .set( + 'accept', + `multipart/mixed; incrementalDeliverySpec=3283f8a, application/json`, + ) + .parse((res, fn) => { + res.text = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => { + res.text += chunk; + if ( + res.text.includes('it works') && + res.text.endsWith('---\r\n') + ) { + gotFirstChunkBarrier.resolve(); + } + }); + res.on('end', fn); + }) + .send({ query: '{ testString ... @defer { barrierString } }' }) + // believe it or not, superagent uses `.then` to decide to actually send the request + .then((r) => r); + + // We ensure that the second chunk can't be sent until after we've + // gotten back a chunk containing the value of testString. + await gotFirstChunkBarrier; + sendSecondChunkBarrier.resolve(); + + const res = await resPromise; + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, + ); + expect(res.text).toMatchInlineSnapshot(` + " + --- + content-type: application/json; charset=utf-8 + + {"hasNext":true,"data":{"testString":"it works"},"pending":[{"id":"0","path":[]}]} + --- + content-type: application/json; charset=utf-8 + + {"hasNext":false,"incremental":[{"id":"0","data":{"barrierString":"we waited"}}],"completed":[{"id":"0"}]} + ----- + " + `); + }, + ); }); // These tests actually execute incremental delivery operations with From fa0c4ee8cb833941532ef2238238cdc9a15e677a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 10:57:17 -0600 Subject: [PATCH 44/93] Update another test to work with alpha.9 --- .../src/httpServerTests.ts | 146 ++++++++++++------ 1 file changed, 101 insertions(+), 45 deletions(-) diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index e33c940029b..f94940333b8 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -2170,53 +2170,109 @@ export function defineIntegrationTestSuiteHttpServerTests( // These tests mock out execution, so that we can test the incremental // delivery transport even if we're built against graphql@16. describe('mocked execution', () => { - it('basic @defer working', async () => { - const app = await createApp({ - typeDefs, - __testing_incrementalExecutionResults: { - initialResult: { - hasNext: true, - data: { first: 'it works' }, + (IS_GRAPHQL_17_ALPHA2 ? it : it.skip)( + 'basic @defer working graphql@17.0.0-alpha.2', + async () => { + const app = await createApp({ + typeDefs, + __testing_incrementalExecutionResults: { + initialResult: { + hasNext: true, + data: { first: 'it works' }, + }, + subsequentResults: (async function* () { + yield { + hasNext: false, + incremental: [ + { path: [], data: { testString: 'it works' } }, + ], + }; + })(), }, - subsequentResults: (async function* () { - yield { - hasNext: false, - incremental: [ - { path: [], data: { testString: 'it works' } }, - ], - }; - })(), - }, - }); - const res = await request(app) - .post('/') - .set( - 'accept', - 'multipart/mixed; deferSpec=20220824, application/json', - ) - // disables supertest's use of formidable for multipart - .parse(superagent.parse.text) - .send({ - query: '{ first: testString ... @defer { testString } }', }); - expect(res.status).toEqual(200); - expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; deferSpec=20220824"`, - ); - expect(res.text).toMatchInlineSnapshot(` - " - --- - content-type: application/json; charset=utf-8 - - {"hasNext":true,"data":{"first":"it works"}} - --- - content-type: application/json; charset=utf-8 - - {"hasNext":false,"incremental":[{"path":[],"data":{"testString":"it works"}}]} - ----- - " - `); - }); + const res = await request(app) + .post('/') + .set( + 'accept', + 'multipart/mixed; deferSpec=20220824, application/json', + ) + // disables supertest's use of formidable for multipart + .parse(superagent.parse.text) + .send({ + query: '{ first: testString ... @defer { testString } }', + }); + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; deferSpec=20220824"`, + ); + expect(res.text).toMatchInlineSnapshot(` + " + --- + content-type: application/json; charset=utf-8 + + {"hasNext":true,"data":{"first":"it works"}} + --- + content-type: application/json; charset=utf-8 + + {"hasNext":false,"incremental":[{"path":[],"data":{"testString":"it works"}}]} + ----- + " + `); + }, + ); + + (IS_GRAPHQL_17_ALPHA9 ? it : it.skip)( + 'basic @defer working graphql@17.0.0-alpha.9', + async () => { + const app = await createApp({ + typeDefs, + __testing_incrementalExecutionResults: { + initialResult: { + hasNext: true, + pending: [{ id: '0', path: [] }], + data: { first: 'it works' }, + }, + subsequentResults: (async function* () { + yield { + hasNext: false, + incremental: [ + { id: '0', data: { testString: 'it works' } }, + ], + completed: [{ id: '0' }], + }; + })(), + }, + }); + const res = await request(app) + .post('/') + .set( + 'accept', + 'multipart/mixed; incrementalDeliverySpec=3283f8a, application/json', + ) + // disables supertest's use of formidable for multipart + .parse(superagent.parse.text) + .send({ + query: '{ first: testString ... @defer { testString } }', + }); + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, + ); + expect(res.text).toMatchInlineSnapshot(` + " + --- + content-type: application/json; charset=utf-8 + + {"hasNext":true,"data":{"first":"it works"},"pending":[{"id":"0","path":[]}]} + --- + content-type: application/json; charset=utf-8 + + {"hasNext":false,"incremental":[{"id":"0","data":{"testString":"it works"}}],"completed":[{"id":"0"}]} + ----- + " + `); + }, + ); (IS_GRAPHQL_17_ALPHA2 ? it : it.skip)( 'first payload sent while deferred field is blocking graphql@17.0.0-alpha.2', From fc8c3a680169d7160a3b84ce8d712ce26d67964b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 11:01:55 -0600 Subject: [PATCH 45/93] Update test in apolloServerTests for alpha.9 --- .../src/apolloServerTests.ts | 142 ++++++++++++------ 1 file changed, 96 insertions(+), 46 deletions(-) diff --git a/packages/integration-testsuite/src/apolloServerTests.ts b/packages/integration-testsuite/src/apolloServerTests.ts index 93ec9a34590..faa3a061964 100644 --- a/packages/integration-testsuite/src/apolloServerTests.ts +++ b/packages/integration-testsuite/src/apolloServerTests.ts @@ -16,6 +16,7 @@ import { type FieldNode, type GraphQLFormattedError, GraphQLScalarType, + version as graphqlVersion, } from 'graphql'; // Note that by doing deep imports here we don't need to install React. @@ -1178,52 +1179,101 @@ export function defineIntegrationTestSuiteApolloServerTests( expect(Object.keys(reports[0].tracesPerQuery)[0]).toMatch(/^# -\n/); }); - (process.env.INCREMENTAL_DELIVERY_TESTS_ENABLED ? it : it.skip)( - 'includes all fields with defer', - async () => { - await setupApolloServerAndFetchPair({}, {}, [], true); - const response = await fetch(uri, { - method: 'POST', - headers: { - 'content-type': 'application/json', - accept: 'multipart/mixed; deferSpec=20220824', - }, - body: JSON.stringify({ - query: '{ justAField ...@defer { delayedFoo { bar} } }', - }), - }); - expect(response.status).toBe(200); - expect( - response.headers.get('content-type'), - ).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; deferSpec=20220824"`, - ); - expect(await response.text()).toMatchInlineSnapshot(` - " - --- - content-type: application/json; charset=utf-8 - - {"hasNext":true,"data":{"justAField":"a string"}} - --- - content-type: application/json; charset=utf-8 - - {"hasNext":false,"incremental":[{"path":[],"data":{"delayedFoo":{"bar":"hi"}}}]} - ----- - " - `); - const reports = await reportIngress.promiseOfReports; - expect(reports.length).toBe(1); - expect(Object.keys(reports[0].tracesPerQuery)).toHaveLength(1); - const trace = Object.values(reports[0].tracesPerQuery)[0] - .trace?.[0] as Trace; - expect(trace).toBeDefined(); - expect(trace?.root?.child?.[0].responseName).toBe('justAField'); - expect(trace?.root?.child?.[1].responseName).toBe('delayedFoo'); - expect(trace?.root?.child?.[1].child?.[0].responseName).toBe( - 'bar', - ); - }, - ); + if (process.env.INCREMENTAL_DELIVERY_TESTS_ENABLED) { + (graphqlVersion === '17.0.0-alpha.2' ? it : it.skip)( + 'includes all fields with defer', + async () => { + await setupApolloServerAndFetchPair({}, {}, [], true); + const response = await fetch(uri, { + method: 'POST', + headers: { + 'content-type': 'application/json', + accept: 'multipart/mixed; deferSpec=20220824', + }, + body: JSON.stringify({ + query: '{ justAField ...@defer { delayedFoo { bar} } }', + }), + }); + expect(response.status).toBe(200); + expect( + response.headers.get('content-type'), + ).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; deferSpec=20220824"`, + ); + expect(await response.text()).toMatchInlineSnapshot(` + " + --- + content-type: application/json; charset=utf-8 + + {"hasNext":true,"data":{"justAField":"a string"}} + --- + content-type: application/json; charset=utf-8 + + {"hasNext":false,"incremental":[{"path":[],"data":{"delayedFoo":{"bar":"hi"}}}]} + ----- + " + `); + const reports = await reportIngress.promiseOfReports; + expect(reports.length).toBe(1); + expect(Object.keys(reports[0].tracesPerQuery)).toHaveLength(1); + const trace = Object.values(reports[0].tracesPerQuery)[0] + .trace?.[0] as Trace; + expect(trace).toBeDefined(); + expect(trace?.root?.child?.[0].responseName).toBe('justAField'); + expect(trace?.root?.child?.[1].responseName).toBe('delayedFoo'); + expect(trace?.root?.child?.[1].child?.[0].responseName).toBe( + 'bar', + ); + }, + ); + + (graphqlVersion === '17.0.0-alpha.9' ? it : it.skip)( + 'includes all fields with defer', + async () => { + await setupApolloServerAndFetchPair({}, {}, [], true); + const response = await fetch(uri, { + method: 'POST', + headers: { + 'content-type': 'application/json', + accept: 'multipart/mixed; incrementalDeliverySpec=3283f8a', + }, + body: JSON.stringify({ + query: '{ justAField ...@defer { delayedFoo { bar} } }', + }), + }); + expect(response.status).toBe(200); + expect( + response.headers.get('content-type'), + ).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, + ); + expect(await response.text()).toMatchInlineSnapshot(` + " + --- + content-type: application/json; charset=utf-8 + + {"hasNext":true,"data":{"justAField":"a string"},"pending":[{"id":"0","path":[]}]} + --- + content-type: application/json; charset=utf-8 + + {"hasNext":false,"incremental":[{"id":"0","data":{"delayedFoo":{"bar":"hi"}}}],"completed":[{"id":"0"}]} + ----- + " + `); + const reports = await reportIngress.promiseOfReports; + expect(reports.length).toBe(1); + expect(Object.keys(reports[0].tracesPerQuery)).toHaveLength(1); + const trace = Object.values(reports[0].tracesPerQuery)[0] + .trace?.[0] as Trace; + expect(trace).toBeDefined(); + expect(trace?.root?.child?.[0].responseName).toBe('justAField'); + expect(trace?.root?.child?.[1].responseName).toBe('delayedFoo'); + expect(trace?.root?.child?.[1].child?.[0].responseName).toBe( + 'bar', + ); + }, + ); + } it("doesn't resort to query body signature on `didResolveOperation` error", async () => { await setupApolloServerAndFetchPair({}, {}, [ From f5965e1d53f561e069644916cd7a09a21c0fb50b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 11:03:18 -0600 Subject: [PATCH 46/93] Update test description --- packages/integration-testsuite/src/apolloServerTests.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/integration-testsuite/src/apolloServerTests.ts b/packages/integration-testsuite/src/apolloServerTests.ts index faa3a061964..ee64b2771f8 100644 --- a/packages/integration-testsuite/src/apolloServerTests.ts +++ b/packages/integration-testsuite/src/apolloServerTests.ts @@ -1181,7 +1181,7 @@ export function defineIntegrationTestSuiteApolloServerTests( if (process.env.INCREMENTAL_DELIVERY_TESTS_ENABLED) { (graphqlVersion === '17.0.0-alpha.2' ? it : it.skip)( - 'includes all fields with defer', + 'includes all fields with defer graphql@17.0.0-alpha.2', async () => { await setupApolloServerAndFetchPair({}, {}, [], true); const response = await fetch(uri, { @@ -1228,7 +1228,7 @@ export function defineIntegrationTestSuiteApolloServerTests( ); (graphqlVersion === '17.0.0-alpha.9' ? it : it.skip)( - 'includes all fields with defer', + 'includes all fields with defer graphql@17.0.0-alpha.9', async () => { await setupApolloServerAndFetchPair({}, {}, [], true); const response = await fetch(uri, { From 109c4a04c2c3f5c63410cc7382bead93cbf19347 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 11:05:05 -0600 Subject: [PATCH 47/93] Fix incorrect assertion --- packages/integration-testsuite/src/httpServerTests.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index f94940333b8..035e25a5731 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -2486,7 +2486,10 @@ export function defineIntegrationTestSuiteHttpServerTests( const app = await createApp({ typeDefs, resolvers }); const req = request(app) .post('/') - .set('accept', 'multipart/mixed; deferSpec=20220824'); + .set( + 'accept', + 'multipart/mixed; incrementalDeliverySpec=3283f8a', + ); const res = await req.send({ query: '{ ... @defer { testString } }', }); From 84862b9a4ee946119459966d0a79ac80afaef5dd Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 11:15:35 -0600 Subject: [PATCH 48/93] Update smoke test for alpha.9 --- smoke-test/smoke-test.cjs | 47 ++++++++++++++++++++++++++++----------- smoke-test/smoke-test.mjs | 47 ++++++++++++++++++++++++++++----------- 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/smoke-test/smoke-test.cjs b/smoke-test/smoke-test.cjs index 4cd4af07182..54ba607a4d1 100644 --- a/smoke-test/smoke-test.cjs +++ b/smoke-test/smoke-test.cjs @@ -2,6 +2,7 @@ const { ApolloServer } = require('@apollo/server'); const { startStandaloneServer } = require('@apollo/server/standalone'); const fetch = require('make-fetch-happen'); const assert = require('assert'); +const { version: graphqlVersion } = require('graphql'); async function validateAllImports() { require('@apollo/server'); @@ -49,35 +50,55 @@ async function smokeTest() { } if (process.env.INCREMENTAL_DELIVERY_TESTS_ENABLED) { + const specVersion = + graphqlVersion === '17.0.0-alpha.9' + ? 'incrementalDeliverySpec=3283f8a' + : 'deferSpec=20220824'; const response = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', - accept: 'multipart/mixed; deferSpec=20220824, application/json', + accept: `multipart/mixed; ${specVersion}, application/json`, }, body: JSON.stringify({ query: '{h1: hello ...@defer{ h2: hello }}' }), }); assert.strictEqual( response.headers.get('content-type'), - 'multipart/mixed; boundary="-"; deferSpec=20220824', + `multipart/mixed; boundary="-"; ${specVersion}`, ); const body = await response.text(); - assert.strictEqual( - body, - '\r\n' + - '---\r\n' + - 'content-type: application/json; charset=utf-8\r\n' + + if (graphqlVersion === '17.0.0-alpha.2') { + assert.strictEqual( + body, '\r\n' + - '{"hasNext":true,"data":{"h1":"world"}}\r\n' + - '---\r\n' + - 'content-type: application/json; charset=utf-8\r\n' + + '---\r\n' + + 'content-type: application/json; charset=utf-8\r\n' + + '\r\n' + + '{"hasNext":true,"data":{"h1":"world"}}\r\n' + + '---\r\n' + + 'content-type: application/json; charset=utf-8\r\n' + + '\r\n' + + '{"hasNext":false,"incremental":[{"path":[],"data":{"h2":"world"}}]}\r\n' + + '-----\r\n', + ); + } else { + assert.strictEqual( + body, '\r\n' + - '{"hasNext":false,"incremental":[{"path":[],"data":{"h2":"world"}}]}\r\n' + - '-----\r\n', - ); + '---\r\n' + + 'content-type: application/json; charset=utf-8\r\n' + + '\r\n' + + '{"hasNext":true,"data":{"h1":"world"},"pending":[{"id":"0","path":[]}]}\r\n' + + '---\r\n' + + 'content-type: application/json; charset=utf-8\r\n' + + '\r\n' + + '{"hasNext":false,"incremental":[{"id":"0","data":{"h2":"world"}}],"completed":[{"id":"0"}]}\r\n' + + '-----\r\n', + ); + } } await s.stop(); diff --git a/smoke-test/smoke-test.mjs b/smoke-test/smoke-test.mjs index 5cb20d9bc48..8875515fb2f 100644 --- a/smoke-test/smoke-test.mjs +++ b/smoke-test/smoke-test.mjs @@ -2,6 +2,7 @@ import { ApolloServer } from '@apollo/server'; import { startStandaloneServer } from '@apollo/server/standalone'; import fetch from 'make-fetch-happen'; import assert from 'assert'; +import { version as graphqlVersion } from 'graphql'; // validate all deep imports await import('@apollo/server'); @@ -45,35 +46,55 @@ const { url } = await startStandaloneServer(s, { listen: { port: 0 } }); } if (process.env.INCREMENTAL_DELIVERY_TESTS_ENABLED) { + const specVersion = + graphqlVersion === '17.0.0-alpha.9' + ? 'incrementalDeliverySpec=3283f8a' + : 'deferSpec=20220824'; const response = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', - accept: 'multipart/mixed; deferSpec=20220824, application/json', + accept: `multipart/mixed; ${specVersion}, application/json`, }, body: JSON.stringify({ query: '{h1: hello ...@defer{ h2: hello }}' }), }); assert.strictEqual( response.headers.get('content-type'), - 'multipart/mixed; boundary="-"; deferSpec=20220824', + `multipart/mixed; boundary="-"; ${specVersion}`, ); const body = await response.text(); - assert.strictEqual( - body, - '\r\n' + - '---\r\n' + - 'content-type: application/json; charset=utf-8\r\n' + + if (graphqlVersion === '17.0.0-alpha.2') { + assert.strictEqual( + body, '\r\n' + - '{"hasNext":true,"data":{"h1":"world"}}\r\n' + - '---\r\n' + - 'content-type: application/json; charset=utf-8\r\n' + + '---\r\n' + + 'content-type: application/json; charset=utf-8\r\n' + + '\r\n' + + '{"hasNext":true,"data":{"h1":"world"}}\r\n' + + '---\r\n' + + 'content-type: application/json; charset=utf-8\r\n' + + '\r\n' + + '{"hasNext":false,"incremental":[{"path":[],"data":{"h2":"world"}}]}\r\n' + + '-----\r\n', + ); + } else { + assert.strictEqual( + body, '\r\n' + - '{"hasNext":false,"incremental":[{"path":[],"data":{"h2":"world"}}]}\r\n' + - '-----\r\n', - ); + '---\r\n' + + 'content-type: application/json; charset=utf-8\r\n' + + '\r\n' + + '{"hasNext":true,"data":{"h1":"world"},"pending":[{"id":"0","path":[]}]}\r\n' + + '---\r\n' + + 'content-type: application/json; charset=utf-8\r\n' + + '\r\n' + + '{"hasNext":false,"incremental":[{"id":"0","data":{"h2":"world"}}],"completed":[{"id":"0"}]}\r\n' + + '-----\r\n', + ); + } } await s.stop(); From c0d24bdb1f26851c10153d2c25f237217d40fea1 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 13:35:22 -0600 Subject: [PATCH 49/93] Add basic tests for @stream --- .../src/httpServerTests.ts | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index 035e25a5731..941211b2dc8 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -2539,6 +2539,70 @@ content-type: application/json; charset=utf-8\r `); }); + it.each([ + ['multipart/mixed; deferSpec=20220824'], + ['multipart/mixed; deferSpec=20220824, application/json'], + ['application/json, multipart/mixed; deferSpec=20220824'], + ])('basic @stream working with accept: %s', async (accept) => { + const app = await createApp({ + typeDefs: `#graphql + directive @stream(if: Boolean! = true, label: String, initialCount: Int! = 0) on FIELD + type Query { + testStrings: [String] + } + `, + resolvers: { + Query: { + testStrings: async function* () { + await new Promise((r) => setTimeout(r, 10)); + yield 'it works'; + + await new Promise((r) => setTimeout(r, 10)); + yield 'it works again'; + + await new Promise((r) => setTimeout(r, 10)); + yield 'it works again again'; + }, + }, + }, + }); + const res = await request(app) + .post('/') + .set('accept', accept) + // disables supertest's use of formidable for multipart + .parse(superagent.parse.text) + .send({ + query: '{ testStrings @stream }', + }); + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; deferSpec=20220824"`, + ); + expect(res.text).toEqual(`\r +---\r +content-type: application/json; charset=utf-8\r +\r +{"hasNext":true,"data":{"testStrings":[]}}\r +---\r +content-type: application/json; charset=utf-8\r +\r +{"hasNext":true,"incremental":[{"path":["testStrings",0],"items":["it works"]}]}\r +---\r +content-type: application/json; charset=utf-8\r +\r +{"hasNext":true,"incremental":[{"path":["testStrings",1],"items":["it works again"]}]}\r +---\r +content-type: application/json; charset=utf-8\r +\r +{"hasNext":true,"incremental":[{"path":["testStrings",2],"items":["it works again again"]}]}\r +---\r +content-type: application/json; charset=utf-8\r +\r +{"hasNext":false}\r +-----\r +`); + }); + it('first payload sent while deferred field is blocking', async () => { const app = await createApp({ typeDefs, resolvers }); const gotFirstChunkBarrier = resolvable(); @@ -2688,6 +2752,74 @@ content-type: application/json; charset=utf-8\r `); }); + it.each([ + ['multipart/mixed; incrementalDeliverySpec=3283f8a'], + [ + 'multipart/mixed; incrementalDeliverySpec=3283f8a, application/json', + ], + [ + 'application/json, multipart/mixed; incrementalDeliverySpec=3283f8a', + ], + ])('basic @stream working with accept: %s', async (accept) => { + const app = await createApp({ + typeDefs: `#graphql + directive @stream(if: Boolean! = true, label: String, initialCount: Int! = 0) on FIELD + type Query { + testStrings: [String] + } + `, + resolvers: { + Query: { + testStrings: async function* () { + await new Promise((r) => setTimeout(r, 10)); + yield 'it works'; + + await new Promise((r) => setTimeout(r, 10)); + yield 'it works again'; + + await new Promise((r) => setTimeout(r, 10)); + yield 'it works again again'; + }, + }, + }, + }); + const res = await request(app) + .post('/') + .set('accept', accept) + // disables supertest's use of formidable for multipart + .parse(superagent.parse.text) + .send({ + query: '{ testStrings @stream }', + }); + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, + ); + expect(res.text).toEqual(`\r +---\r +content-type: application/json; charset=utf-8\r +\r +{"hasNext":true,"data":{"testStrings":[]},"pending":[{"id":"0","path":["testStrings"]}]}\r +---\r +content-type: application/json; charset=utf-8\r +\r +{"hasNext":true,"incremental":[{"id":"0","items":["it works"]}]}\r +---\r +content-type: application/json; charset=utf-8\r +\r +{"hasNext":true,"incremental":[{"id":"0","items":["it works again"]}]}\r +---\r +content-type: application/json; charset=utf-8\r +\r +{"hasNext":true,"incremental":[{"id":"0","items":["it works again again"]}]}\r +---\r +content-type: application/json; charset=utf-8\r +\r +{"hasNext":false,"completed":[{"id":"0"}]}\r +-----\r +`); + }); + it('first payload sent while deferred field is blocking', async () => { const app = await createApp({ typeDefs, resolvers }); const gotFirstChunkBarrier = resolvable(); From 756456afae77f2da1c3a84199983a2ce6f047551 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 13:44:46 -0600 Subject: [PATCH 50/93] Ordering --- packages/server/src/ApolloServer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/ApolloServer.ts b/packages/server/src/ApolloServer.ts index 48b7705b2b0..2e6380a783c 100644 --- a/packages/server/src/ApolloServer.ts +++ b/packages/server/src/ApolloServer.ts @@ -1210,8 +1210,8 @@ export class ApolloServer { // by the order in the list we provide, so we put text/html last. MEDIA_TYPES.APPLICATION_JSON, MEDIA_TYPES.APPLICATION_GRAPHQL_RESPONSE_JSON, - MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9, MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2, + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9, MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC, MEDIA_TYPES.TEXT_HTML, ]) === MEDIA_TYPES.TEXT_HTML From 80f06e9796e7bfdee555c4e94ae76a93931cabaa Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 13:45:53 -0600 Subject: [PATCH 51/93] Ensure tests are skipped instead of hidden --- .../src/apolloServerTests.ts | 144 +++++++++--------- 1 file changed, 74 insertions(+), 70 deletions(-) diff --git a/packages/integration-testsuite/src/apolloServerTests.ts b/packages/integration-testsuite/src/apolloServerTests.ts index ee64b2771f8..b50d77805c1 100644 --- a/packages/integration-testsuite/src/apolloServerTests.ts +++ b/packages/integration-testsuite/src/apolloServerTests.ts @@ -1179,28 +1179,30 @@ export function defineIntegrationTestSuiteApolloServerTests( expect(Object.keys(reports[0].tracesPerQuery)[0]).toMatch(/^# -\n/); }); - if (process.env.INCREMENTAL_DELIVERY_TESTS_ENABLED) { - (graphqlVersion === '17.0.0-alpha.2' ? it : it.skip)( - 'includes all fields with defer graphql@17.0.0-alpha.2', - async () => { - await setupApolloServerAndFetchPair({}, {}, [], true); - const response = await fetch(uri, { - method: 'POST', - headers: { - 'content-type': 'application/json', - accept: 'multipart/mixed; deferSpec=20220824', - }, - body: JSON.stringify({ - query: '{ justAField ...@defer { delayedFoo { bar} } }', - }), - }); - expect(response.status).toBe(200); - expect( - response.headers.get('content-type'), - ).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; deferSpec=20220824"`, - ); - expect(await response.text()).toMatchInlineSnapshot(` + (process.env.INCREMENTAL_DELIVERY_TESTS_ENABLED && + graphqlVersion === '17.0.0-alpha.2' + ? it + : it.skip)( + 'includes all fields with defer graphql@17.0.0-alpha.2', + async () => { + await setupApolloServerAndFetchPair({}, {}, [], true); + const response = await fetch(uri, { + method: 'POST', + headers: { + 'content-type': 'application/json', + accept: 'multipart/mixed; deferSpec=20220824', + }, + body: JSON.stringify({ + query: '{ justAField ...@defer { delayedFoo { bar} } }', + }), + }); + expect(response.status).toBe(200); + expect( + response.headers.get('content-type'), + ).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; deferSpec=20220824"`, + ); + expect(await response.text()).toMatchInlineSnapshot(` " --- content-type: application/json; charset=utf-8 @@ -1213,41 +1215,44 @@ export function defineIntegrationTestSuiteApolloServerTests( ----- " `); - const reports = await reportIngress.promiseOfReports; - expect(reports.length).toBe(1); - expect(Object.keys(reports[0].tracesPerQuery)).toHaveLength(1); - const trace = Object.values(reports[0].tracesPerQuery)[0] - .trace?.[0] as Trace; - expect(trace).toBeDefined(); - expect(trace?.root?.child?.[0].responseName).toBe('justAField'); - expect(trace?.root?.child?.[1].responseName).toBe('delayedFoo'); - expect(trace?.root?.child?.[1].child?.[0].responseName).toBe( - 'bar', - ); - }, - ); + const reports = await reportIngress.promiseOfReports; + expect(reports.length).toBe(1); + expect(Object.keys(reports[0].tracesPerQuery)).toHaveLength(1); + const trace = Object.values(reports[0].tracesPerQuery)[0] + .trace?.[0] as Trace; + expect(trace).toBeDefined(); + expect(trace?.root?.child?.[0].responseName).toBe('justAField'); + expect(trace?.root?.child?.[1].responseName).toBe('delayedFoo'); + expect(trace?.root?.child?.[1].child?.[0].responseName).toBe( + 'bar', + ); + }, + ); - (graphqlVersion === '17.0.0-alpha.9' ? it : it.skip)( - 'includes all fields with defer graphql@17.0.0-alpha.9', - async () => { - await setupApolloServerAndFetchPair({}, {}, [], true); - const response = await fetch(uri, { - method: 'POST', - headers: { - 'content-type': 'application/json', - accept: 'multipart/mixed; incrementalDeliverySpec=3283f8a', - }, - body: JSON.stringify({ - query: '{ justAField ...@defer { delayedFoo { bar} } }', - }), - }); - expect(response.status).toBe(200); - expect( - response.headers.get('content-type'), - ).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, - ); - expect(await response.text()).toMatchInlineSnapshot(` + (process.env.INCREMENTAL_DELIVERY_TESTS_ENABLED && + graphqlVersion === '17.0.0-alpha.9' + ? it + : it.skip)( + 'includes all fields with defer graphql@17.0.0-alpha.9', + async () => { + await setupApolloServerAndFetchPair({}, {}, [], true); + const response = await fetch(uri, { + method: 'POST', + headers: { + 'content-type': 'application/json', + accept: 'multipart/mixed; incrementalDeliverySpec=3283f8a', + }, + body: JSON.stringify({ + query: '{ justAField ...@defer { delayedFoo { bar} } }', + }), + }); + expect(response.status).toBe(200); + expect( + response.headers.get('content-type'), + ).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, + ); + expect(await response.text()).toMatchInlineSnapshot(` " --- content-type: application/json; charset=utf-8 @@ -1260,20 +1265,19 @@ export function defineIntegrationTestSuiteApolloServerTests( ----- " `); - const reports = await reportIngress.promiseOfReports; - expect(reports.length).toBe(1); - expect(Object.keys(reports[0].tracesPerQuery)).toHaveLength(1); - const trace = Object.values(reports[0].tracesPerQuery)[0] - .trace?.[0] as Trace; - expect(trace).toBeDefined(); - expect(trace?.root?.child?.[0].responseName).toBe('justAField'); - expect(trace?.root?.child?.[1].responseName).toBe('delayedFoo'); - expect(trace?.root?.child?.[1].child?.[0].responseName).toBe( - 'bar', - ); - }, - ); - } + const reports = await reportIngress.promiseOfReports; + expect(reports.length).toBe(1); + expect(Object.keys(reports[0].tracesPerQuery)).toHaveLength(1); + const trace = Object.values(reports[0].tracesPerQuery)[0] + .trace?.[0] as Trace; + expect(trace).toBeDefined(); + expect(trace?.root?.child?.[0].responseName).toBe('justAField'); + expect(trace?.root?.child?.[1].responseName).toBe('delayedFoo'); + expect(trace?.root?.child?.[1].child?.[0].responseName).toBe( + 'bar', + ); + }, + ); it("doesn't resort to query body signature on `didResolveOperation` error", async () => { await setupApolloServerAndFetchPair({}, {}, [ From 5184e4c73a45117106a28745dbfecfab43873690 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 13:47:50 -0600 Subject: [PATCH 52/93] Revert quotes --- packages/integration-testsuite/src/httpServerTests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index 941211b2dc8..fd2136a7070 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -2301,7 +2301,7 @@ export function defineIntegrationTestSuiteHttpServerTests( .post('/') .set( 'accept', - `multipart/mixed; deferSpec=20220824, application/json`, + 'multipart/mixed; deferSpec=20220824, application/json', ) .parse((res, fn) => { res.text = ''; From 586b1024bc7fc1584ff5a532cd71e0696ce3c252 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 14:05:33 -0600 Subject: [PATCH 53/93] Add changesets --- .changeset/fruity-ways-tell.md | 5 ++++ .changeset/yellow-worlds-start.md | 38 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 .changeset/fruity-ways-tell.md create mode 100644 .changeset/yellow-worlds-start.md diff --git a/.changeset/fruity-ways-tell.md b/.changeset/fruity-ways-tell.md new file mode 100644 index 00000000000..97c57bc4831 --- /dev/null +++ b/.changeset/fruity-ways-tell.md @@ -0,0 +1,5 @@ +--- +'@apollo/server': minor +--- + +Add support for the `graphql@17.0.0-alpha.9` `@defer` and `@stream` incremental delivery protocol. When `graphql@17.0.0-alpha.9` is installed, clients must send the `Accept` header with a value of `multipart/mixed; incrementalDeliverySpec=3283f8a` to specify the new format. If the `Accept` header is not compatible with the installed version of `graphql`, an error is returned to the client. diff --git a/.changeset/yellow-worlds-start.md b/.changeset/yellow-worlds-start.md new file mode 100644 index 00000000000..a9bcbc4a6bb --- /dev/null +++ b/.changeset/yellow-worlds-start.md @@ -0,0 +1,38 @@ +--- +'@apollo/server': minor +--- + +Add `graphql@17.0-0-alpha.9` incremental delivery types and rename the existing incremental delievery types by adding an `Alpha2` suffix. If you import these types in your code, you will need to add the `Alpha2` suffix. + +```diff +import type { +- GraphQLExperimentalFormattedInitialIncrementalExecutionResult, ++ GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2, + +- GraphQLExperimentalFormattedSubsequentIncrementalExecutionResult, ++ GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2, + +- GraphQLExperimentalFormattedIncrementalResult, ++ GraphQLExperimentalFormattedIncrementalResultAlpha2, + +- GraphQLExperimentalFormattedIncrementalDeferResult, ++ GraphQLExperimentalFormattedIncrementalDeferResultAlpha2, + +- GraphQLExperimentalFormattedIncrementalStreamResult, ++ GraphQLExperimentalFormattedIncrementalStreamResultAlpha2, +} from '@apollo/server'; +``` + +Incremental delivery types for the `graphql@17.0.0-alpha.9` version are now available using the `Alpha9` suffix: + +```ts +import type { + GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha9, + GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha9, + GraphQLExperimentalFormattedIncrementalResultAlpha9, + GraphQLExperimentalFormattedIncrementalDeferResultAlpha9, + GraphQLExperimentalFormattedIncrementalStreamResultAlpha9, + GraphQLExperimentalFormattedCompletedResultAlpha9, + GraphQLExperimentalPendingResultAlpha9, +} from '@apollo/server'; +``` From 93534d0cb3801904778973a6232f73102922391f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 14:31:46 -0600 Subject: [PATCH 54/93] Update docs --- docs/source/workflow/requests.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/workflow/requests.md b/docs/source/workflow/requests.md index 54cc7c6ae26..6f228a4f916 100644 --- a/docs/source/workflow/requests.md +++ b/docs/source/workflow/requests.md @@ -91,7 +91,7 @@ For more details, see [the CSRF prevention documentation](../security/cors#preve ## Incremental delivery (experimental) -Incremental delivery is a [Stage 2: Draft Proposal](https://github.com/graphql/graphql-spec/pull/742) to the GraphQL specification which adds `@defer` and `@stream` executable directives. These directives allow clients to specify that parts of an operation can be sent after an initial response, so that slower fields do not delay all other fields. As of June 2025, the `graphql` library (also known as `graphql-js`) upon which Apollo Server is built implements incremental delivery only in the unreleased major version 17. If a pre-release of `graphql@17.0.0-alpha.2` is installed in your server, Apollo Server can execute these incremental delivery directives and provide streaming [`multipart/mixed`](https://github.com/graphql/graphql-over-http/blob/main/rfcs/IncrementalDelivery.md) responses. +Incremental delivery is a [Stage 1: Proposal](https://github.com/graphql/graphql-spec/pull/1110) to the GraphQL specification which adds `@defer` and `@stream` executable directives. These directives allow clients to specify that parts of an operation can be sent after an initial response, so that slower fields do not delay all other fields. As of June 2025, the `graphql` library (also known as `graphql-js`) upon which Apollo Server is built implements incremental delivery only in the unreleased major version 17. If a pre-release of `graphql@17.0.0-alpha.2` or `graphql@17.0.0-alpha.9` is installed in your server, Apollo Server can execute these incremental delivery directives and provide streaming [`multipart/mixed`](https://github.com/graphql/graphql-over-http/blob/main/rfcs/IncrementalDelivery.md) responses. Support for incremental delivery in graphql version 17 is [opt-in](https://github.com/robrichard/defer-stream-wg/discussions/12), meaning the directives are not defined by default. In order to use `@defer` or `@stream`, you must provide the appropriate definition(s) in your SDL. The definitions below can be pasted into your schema as-is: @@ -119,8 +119,8 @@ const schema = new GraphQLSchema({ }); ``` -Clients sending operations with incremental delivery directives need to explicitly indicate that they are expecting to receive `multipart/mixed` responses in an `accept` header. Moreover, because incremental delivery has not yet been finalized in the GraphQL spec and may change before the final version, they need to specify that they expect the particular response format that Apollo Server produces today via a `deferSpec` parameter. Specifically, clients prepared to accept incremental delivery responses should send an `accept` header like `multipart/mixed; deferSpec=20220824`. Note that this header implies that *only* multipart responses should be accepted; typically, clients will send an accept header like `multipart/mixed; deferSpec=20220824, application/json` indicating that either multipart or single-part responses are acceptable. +Clients sending operations with incremental delivery directives need to explicitly indicate that they are expecting to receive `multipart/mixed` responses in an `accept` header. Moreover, because incremental delivery has not yet been finalized in the GraphQL spec and may change before the final version, they need to specify that they expect the particular response format that Apollo Server produces today via a `deferSpec` parameter (for `17.0.0-alpha.2`) or `incrementalDeliverySpec` parameter (for `17.0.0-alpha.9`). Specifically, clients prepared to accept incremental delivery responses should send an `accept` header like `multipart/mixed; deferSpec=20220824` or `multipart/mixed; incrementalDeliverySpec=3283f8a`. Note that this header implies that *only* multipart responses should be accepted; typically, clients will send an accept header like `multipart/mixed; deferSpec=20220824, application/json` indicating that either multipart or single-part responses are acceptable. -> Apollo Server *only* supports the specific pre-release `graphql@17.0.0-alpha.2`. Newer alpha versions of `graphql` v17 support a slightly different format for incremental delivery, which Apollo Server does not yet support. Apollo Server 5 checks the version of `graphql` you have installed and will not attempt to support incremental delivery unless it is precisely `17.0.0-alpha.2`. We hope to support the newer incremental delivery protocol in a future release, using a different `deferSpec` value. +> Apollo Server *only* supports the specific pre-release `graphql@17.0.0-alpha.2` or `graphql@17.0.0-alpha.9`. Apollo Server 5 checks the version of `graphql` you have installed and will not attempt to support incremental delivery unless it is precisely `17.0.0-alpha.2` or `17.0.0-alpha.9`. You cannot combine [batching](#batching) with incremental delivery in the same request. From d865b575afbdb2f8a04a9be5fc0aceef48db04cf Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 25 Sep 2025 14:38:09 -0600 Subject: [PATCH 55/93] Fix typo --- .changeset/yellow-worlds-start.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/yellow-worlds-start.md b/.changeset/yellow-worlds-start.md index a9bcbc4a6bb..19f4596c9da 100644 --- a/.changeset/yellow-worlds-start.md +++ b/.changeset/yellow-worlds-start.md @@ -2,7 +2,7 @@ '@apollo/server': minor --- -Add `graphql@17.0-0-alpha.9` incremental delivery types and rename the existing incremental delievery types by adding an `Alpha2` suffix. If you import these types in your code, you will need to add the `Alpha2` suffix. +Add `graphql@17.0-0-alpha.9` incremental delivery types and rename the existing incremental delivery types by adding an `Alpha2` suffix. If you import these types in your code, you will need to add the `Alpha2` suffix. ```diff import type { From 62e1ccaa4df3cc8fece5956deb3996dbe009d1b7 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Sun, 28 Sep 2025 20:51:52 -0600 Subject: [PATCH 56/93] Add @yaacovcr/transform as optional peer dep --- package-lock.json | 6 ++++++ packages/server/package.json | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 5b05aad937b..a7d4a707e50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14940,7 +14940,13 @@ "node": ">=20" }, "peerDependencies": { + "@yaacovcr/transform": "^0.0.8", "graphql": "^16.11.0" + }, + "peerDependenciesMeta": { + "@yaacovcr/transform": { + "optional": true + } } }, "packages/server/node_modules/@apollo/utils.fetcher": { diff --git a/packages/server/package.json b/packages/server/package.json index b2897bf675d..4ad4df30ae9 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -147,6 +147,12 @@ "whatwg-mimetype": "^4.0.0" }, "peerDependencies": { - "graphql": "^16.11.0" + "graphql": "^16.11.0", + "@yaacovcr/transform": "^0.0.8" + }, + "peerDependenciesMeta": { + "@yaacovcr/transform": { + "optional": true + } } } From ef8da6c47994a07187ea24a0bb3a68b9eff274d9 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Sun, 28 Sep 2025 20:53:25 -0600 Subject: [PATCH 57/93] Install transform in circle --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2639d0f84a6..adca9cd27c9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -88,7 +88,7 @@ jobs: # Install a newer prerelease of graphql-js 17; we do not yet support # its incremental delivery format. # --legacy-peer-deps because nothing expects v17 yet. - - run: npm i --legacy-peer-deps "graphql@${GRAPHQL_JS_VERSION}" + - run: npm i --legacy-peer-deps "graphql@${GRAPHQL_JS_VERSION}" @yaacovcr/transform - run: npm run test:ci - run: npm run test:smoke From 601cf6a66467125d218354fd13381802269c0874 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Sun, 28 Sep 2025 21:10:37 -0600 Subject: [PATCH 58/93] Add flag to load legacy incremental --- .../server/src/incrementalDeliveryPolyfill.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/server/src/incrementalDeliveryPolyfill.ts b/packages/server/src/incrementalDeliveryPolyfill.ts index a44e55dabfe..9bd800c858b 100644 --- a/packages/server/src/incrementalDeliveryPolyfill.ts +++ b/packages/server/src/incrementalDeliveryPolyfill.ts @@ -191,6 +191,15 @@ let graphqlExperimentalExecuteIncrementally: | null | undefined = undefined; +async function tryToLoadLegacyExecuteIncrementally() { + try { + const { legacyExecuteIncrementally } = await import('@yaacovcr/transform'); + graphqlExperimentalExecuteIncrementally = legacyExecuteIncrementally; + } catch { + return; + } +} + async function tryToLoadGraphQL17() { if (graphqlExperimentalExecuteIncrementally !== undefined) { return; @@ -213,13 +222,17 @@ async function tryToLoadGraphQL17() { } } -export async function executeIncrementally( - args: ExecutionArgs, -): Promise< +export async function executeIncrementally({ + useLegacyIncremental, + ...args +}: ExecutionArgs & { useLegacyIncremental?: boolean }): Promise< | ExecutionResult | GraphQLExperimentalIncrementalExecutionResultsAlpha2 | GraphQLExperimentalIncrementalExecutionResultsAlpha9 > { + if (useLegacyIncremental) { + await tryToLoadLegacyExecuteIncrementally(); + } await tryToLoadGraphQL17(); if (graphqlExperimentalExecuteIncrementally) { return graphqlExperimentalExecuteIncrementally(args); From 426026d68bfa43e9f16a021ca2e892f840175753 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Sun, 28 Sep 2025 21:10:57 -0600 Subject: [PATCH 59/93] Remove check for graphql alpha.2 --- packages/server/src/incrementalDeliveryPolyfill.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/server/src/incrementalDeliveryPolyfill.ts b/packages/server/src/incrementalDeliveryPolyfill.ts index 9bd800c858b..cfc9f748d25 100644 --- a/packages/server/src/incrementalDeliveryPolyfill.ts +++ b/packages/server/src/incrementalDeliveryPolyfill.ts @@ -206,12 +206,6 @@ async function tryToLoadGraphQL17() { } const graphql = await import('graphql'); if ( - graphql.version === '17.0.0-alpha.2' && - 'experimentalExecuteIncrementally' in graphql - ) { - graphqlExperimentalExecuteIncrementally = (graphql as any) - .experimentalExecuteIncrementally; - } else if ( graphql.version === '17.0.0-alpha.9' && 'experimentalExecuteIncrementally' in graphql ) { From 45f35c14424fab8e4298f52547b4a298a3f03008 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Sun, 28 Sep 2025 21:25:54 -0600 Subject: [PATCH 60/93] Prefer alpha.9 format --- packages/server/src/runHttpQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index cf0a3a3b096..b98ddc5c04d 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -293,8 +293,8 @@ export async function runHttpQuery({ // doesn't include the deferSpec parameter it will match this one here, // which isn't good enough. MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC, - MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2, MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9, + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2, ]); if ( From ece5991002aa3c0f7dfae265e32df7366469d883 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Sun, 28 Sep 2025 21:31:14 -0600 Subject: [PATCH 61/93] Don't error with incompatible graphql.js version --- packages/server/src/runHttpQuery.ts | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index b98ddc5c04d..373c9542c90 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -21,11 +21,7 @@ import { MEDIA_TYPES, type SchemaDerivedData, } from './ApolloServer.js'; -import { - type FormattedExecutionResult, - Kind, - version as graphqlVersion, -} from 'graphql'; +import { type FormattedExecutionResult, Kind } from 'graphql'; import { BadRequestError } from './internalErrorClasses.js'; import Negotiator from 'negotiator'; import { HeaderMap } from './utils/HeaderMap.js'; @@ -284,10 +280,6 @@ export async function runHttpQuery({ // anything has changed). const acceptHeader = httpRequest.headers.get('accept'); const negotiator = new Negotiator({ headers: { accept: acceptHeader } }); - const validMediaType = - graphqlVersion === '17.0.0-alpha.9' - ? MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9 - : MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2; const preferredMediaType = negotiator.mediaType([ // mediaType() will return the first one that matches, so if the client // doesn't include the deferSpec parameter it will match this one here, @@ -308,19 +300,7 @@ export async function runHttpQuery({ 'Apollo server received an operation that uses incremental delivery ' + '(@defer or @stream), but the client does not accept multipart/mixed ' + 'HTTP responses. To enable incremental delivery support, add the HTTP ' + - `header 'Accept: ${validMediaType}'.`, - // Use 406 Not Accepted - { extensions: { http: { status: 406 } } }, - ); - } - - if (negotiator.mediaType([validMediaType]) !== validMediaType) { - // The client ran an operation that would yield multiple parts, but - // specified the wrong version. We return an error - throw new BadRequestError( - 'Apollo server received an operation that uses incremental delivery ' + - '(@defer or @stream) with a spec version incompatible with this server. ' + - `Please use the HTTP header 'Accept: ${validMediaType}'.`, + `header 'Accept: ${MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9}'.`, // Use 406 Not Accepted { extensions: { http: { status: 406 } } }, ); @@ -328,7 +308,7 @@ export async function runHttpQuery({ graphQLResponse.http.headers.set( 'content-type', - `multipart/mixed; boundary="-"; ${validMediaType.replace('multipart/mixed; ', '')}`, + `multipart/mixed; boundary="-"; ${preferredMediaType.replace('multipart/mixed; ', '')}`, ); return { ...graphQLResponse.http, From e8a6d0a724c7932d074078bd7aa0a19558610366 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Sun, 28 Sep 2025 21:31:53 -0600 Subject: [PATCH 62/93] Remove checks for versions in tests --- .../src/httpServerTests.ts | 907 ++++++++---------- 1 file changed, 415 insertions(+), 492 deletions(-) diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index fd2136a7070..1ca9008cd92 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -18,7 +18,6 @@ import { GraphQLSchema, GraphQLString, type ValidationContext, - version as graphqlVersion, } from 'graphql'; import gql from 'graphql-tag'; import { @@ -57,9 +56,6 @@ import { unwrapResolverError, } from '@apollo/server/errors'; -const IS_GRAPHQL_17_ALPHA2 = graphqlVersion === '17.0.0-alpha.2'; -const IS_GRAPHQL_17_ALPHA9 = graphqlVersion === '17.0.0-alpha.9'; - const QueryRootType = new GraphQLObjectType({ name: 'QueryRoot', fields: { @@ -2170,42 +2166,40 @@ export function defineIntegrationTestSuiteHttpServerTests( // These tests mock out execution, so that we can test the incremental // delivery transport even if we're built against graphql@16. describe('mocked execution', () => { - (IS_GRAPHQL_17_ALPHA2 ? it : it.skip)( - 'basic @defer working graphql@17.0.0-alpha.2', - async () => { - const app = await createApp({ - typeDefs, - __testing_incrementalExecutionResults: { - initialResult: { - hasNext: true, - data: { first: 'it works' }, - }, - subsequentResults: (async function* () { - yield { - hasNext: false, - incremental: [ - { path: [], data: { testString: 'it works' } }, - ], - }; - })(), + it('basic @defer working legacy incremental', async () => { + const app = await createApp({ + typeDefs, + __testing_incrementalExecutionResults: { + initialResult: { + hasNext: true, + data: { first: 'it works' }, }, + subsequentResults: (async function* () { + yield { + hasNext: false, + incremental: [ + { path: [], data: { testString: 'it works' } }, + ], + }; + })(), + }, + }); + const res = await request(app) + .post('/') + .set( + 'accept', + 'multipart/mixed; deferSpec=20220824, application/json', + ) + // disables supertest's use of formidable for multipart + .parse(superagent.parse.text) + .send({ + query: '{ first: testString ... @defer { testString } }', }); - const res = await request(app) - .post('/') - .set( - 'accept', - 'multipart/mixed; deferSpec=20220824, application/json', - ) - // disables supertest's use of formidable for multipart - .parse(superagent.parse.text) - .send({ - query: '{ first: testString ... @defer { testString } }', - }); - expect(res.status).toEqual(200); - expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; deferSpec=20220824"`, - ); - expect(res.text).toMatchInlineSnapshot(` + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; deferSpec=20220824"`, + ); + expect(res.text).toMatchInlineSnapshot(` " --- content-type: application/json; charset=utf-8 @@ -2218,47 +2212,44 @@ export function defineIntegrationTestSuiteHttpServerTests( ----- " `); - }, - ); + }); - (IS_GRAPHQL_17_ALPHA9 ? it : it.skip)( - 'basic @defer working graphql@17.0.0-alpha.9', - async () => { - const app = await createApp({ - typeDefs, - __testing_incrementalExecutionResults: { - initialResult: { - hasNext: true, - pending: [{ id: '0', path: [] }], - data: { first: 'it works' }, - }, - subsequentResults: (async function* () { - yield { - hasNext: false, - incremental: [ - { id: '0', data: { testString: 'it works' } }, - ], - completed: [{ id: '0' }], - }; - })(), + it('basic @defer working graphql@17.0.0-alpha.9', async () => { + const app = await createApp({ + typeDefs, + __testing_incrementalExecutionResults: { + initialResult: { + hasNext: true, + pending: [{ id: '0', path: [] }], + data: { first: 'it works' }, }, + subsequentResults: (async function* () { + yield { + hasNext: false, + incremental: [ + { id: '0', data: { testString: 'it works' } }, + ], + completed: [{ id: '0' }], + }; + })(), + }, + }); + const res = await request(app) + .post('/') + .set( + 'accept', + 'multipart/mixed; incrementalDeliverySpec=3283f8a, application/json', + ) + // disables supertest's use of formidable for multipart + .parse(superagent.parse.text) + .send({ + query: '{ first: testString ... @defer { testString } }', }); - const res = await request(app) - .post('/') - .set( - 'accept', - 'multipart/mixed; incrementalDeliverySpec=3283f8a, application/json', - ) - // disables supertest's use of formidable for multipart - .parse(superagent.parse.text) - .send({ - query: '{ first: testString ... @defer { testString } }', - }); - expect(res.status).toEqual(200); - expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, - ); - expect(res.text).toMatchInlineSnapshot(` + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, + ); + expect(res.text).toMatchInlineSnapshot(` " --- content-type: application/json; charset=utf-8 @@ -2271,67 +2262,64 @@ export function defineIntegrationTestSuiteHttpServerTests( ----- " `); - }, - ); + }); - (IS_GRAPHQL_17_ALPHA2 ? it : it.skip)( - 'first payload sent while deferred field is blocking graphql@17.0.0-alpha.2', - async () => { - const gotFirstChunkBarrier = resolvable(); - const sendSecondChunkBarrier = resolvable(); - const app = await createApp({ - typeDefs, - __testing_incrementalExecutionResults: { - initialResult: { - hasNext: true, - data: { testString: 'it works' }, - }, - subsequentResults: (async function* () { - await sendSecondChunkBarrier; - yield { - hasNext: false, - incremental: [ - { path: [], data: { barrierString: 'we waited' } }, - ], - }; - })(), + it('first payload sent while deferred field is blocking legacy incremental', async () => { + const gotFirstChunkBarrier = resolvable(); + const sendSecondChunkBarrier = resolvable(); + const app = await createApp({ + typeDefs, + __testing_incrementalExecutionResults: { + initialResult: { + hasNext: true, + data: { testString: 'it works' }, }, - }); - const resPromise = request(app) - .post('/') - .set( - 'accept', - 'multipart/mixed; deferSpec=20220824, application/json', - ) - .parse((res, fn) => { - res.text = ''; - res.setEncoding('utf8'); - res.on('data', (chunk) => { - res.text += chunk; - if ( - res.text.includes('it works') && - res.text.endsWith('---\r\n') - ) { - gotFirstChunkBarrier.resolve(); - } - }); - res.on('end', fn); - }) - .send({ query: '{ testString ... @defer { barrierString } }' }) - // believe it or not, superagent uses `.then` to decide to actually send the request - .then((r) => r); - - // We ensure that the second chunk can't be sent until after we've - // gotten back a chunk containing the value of testString. - await gotFirstChunkBarrier; - sendSecondChunkBarrier.resolve(); - - const res = await resPromise; - expect(res.status).toEqual(200); - expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; deferSpec=20220824"`, - ); - expect(res.text).toMatchInlineSnapshot(` + subsequentResults: (async function* () { + await sendSecondChunkBarrier; + yield { + hasNext: false, + incremental: [ + { path: [], data: { barrierString: 'we waited' } }, + ], + }; + })(), + }, + }); + const resPromise = request(app) + .post('/') + .set( + 'accept', + 'multipart/mixed; deferSpec=20220824, application/json', + ) + .parse((res, fn) => { + res.text = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => { + res.text += chunk; + if ( + res.text.includes('it works') && + res.text.endsWith('---\r\n') + ) { + gotFirstChunkBarrier.resolve(); + } + }); + res.on('end', fn); + }) + .send({ query: '{ testString ... @defer { barrierString } }' }) + // believe it or not, superagent uses `.then` to decide to actually send the request + .then((r) => r); + + // We ensure that the second chunk can't be sent until after we've + // gotten back a chunk containing the value of testString. + await gotFirstChunkBarrier; + sendSecondChunkBarrier.resolve(); + + const res = await resPromise; + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; deferSpec=20220824"`, + ); + expect(res.text).toMatchInlineSnapshot(` " --- content-type: application/json; charset=utf-8 @@ -2344,69 +2332,66 @@ export function defineIntegrationTestSuiteHttpServerTests( ----- " `); - }, - ); + }); - (IS_GRAPHQL_17_ALPHA9 ? it : it.skip)( - 'first payload sent while deferred field is blocking graphql@17.0.0-alpha.9', - async () => { - const gotFirstChunkBarrier = resolvable(); - const sendSecondChunkBarrier = resolvable(); - const app = await createApp({ - typeDefs, - __testing_incrementalExecutionResults: { - initialResult: { - hasNext: true, - pending: [{ id: '0', path: [] }], - data: { testString: 'it works' }, - }, - subsequentResults: (async function* () { - await sendSecondChunkBarrier; - yield { - hasNext: false, - incremental: [ - { id: '0', data: { barrierString: 'we waited' } }, - ], - completed: [{ id: '0' }], - }; - })(), + it('first payload sent while deferred field is blocking graphql@17.0.0-alpha.9', async () => { + const gotFirstChunkBarrier = resolvable(); + const sendSecondChunkBarrier = resolvable(); + const app = await createApp({ + typeDefs, + __testing_incrementalExecutionResults: { + initialResult: { + hasNext: true, + pending: [{ id: '0', path: [] }], + data: { testString: 'it works' }, }, - }); - const resPromise = request(app) - .post('/') - .set( - 'accept', - `multipart/mixed; incrementalDeliverySpec=3283f8a, application/json`, - ) - .parse((res, fn) => { - res.text = ''; - res.setEncoding('utf8'); - res.on('data', (chunk) => { - res.text += chunk; - if ( - res.text.includes('it works') && - res.text.endsWith('---\r\n') - ) { - gotFirstChunkBarrier.resolve(); - } - }); - res.on('end', fn); - }) - .send({ query: '{ testString ... @defer { barrierString } }' }) - // believe it or not, superagent uses `.then` to decide to actually send the request - .then((r) => r); - - // We ensure that the second chunk can't be sent until after we've - // gotten back a chunk containing the value of testString. - await gotFirstChunkBarrier; - sendSecondChunkBarrier.resolve(); - - const res = await resPromise; - expect(res.status).toEqual(200); - expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, - ); - expect(res.text).toMatchInlineSnapshot(` + subsequentResults: (async function* () { + await sendSecondChunkBarrier; + yield { + hasNext: false, + incremental: [ + { id: '0', data: { barrierString: 'we waited' } }, + ], + completed: [{ id: '0' }], + }; + })(), + }, + }); + const resPromise = request(app) + .post('/') + .set( + 'accept', + `multipart/mixed; incrementalDeliverySpec=3283f8a, application/json`, + ) + .parse((res, fn) => { + res.text = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => { + res.text += chunk; + if ( + res.text.includes('it works') && + res.text.endsWith('---\r\n') + ) { + gotFirstChunkBarrier.resolve(); + } + }); + res.on('end', fn); + }) + .send({ query: '{ testString ... @defer { barrierString } }' }) + // believe it or not, superagent uses `.then` to decide to actually send the request + .then((r) => r); + + // We ensure that the second chunk can't be sent until after we've + // gotten back a chunk containing the value of testString. + await gotFirstChunkBarrier; + sendSecondChunkBarrier.resolve(); + + const res = await resPromise; + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, + ); + expect(res.text).toMatchInlineSnapshot(` " --- content-type: application/json; charset=utf-8 @@ -2419,8 +2404,7 @@ export function defineIntegrationTestSuiteHttpServerTests( ----- " `); - }, - ); + }); }); // These tests actually execute incremental delivery operations with @@ -2447,27 +2431,23 @@ export function defineIntegrationTestSuiteHttpServerTests( }, }; - (IS_GRAPHQL_17_ALPHA2 ? describe : describe.skip)( - 'tests that require graphql@17.0.0-alpha.2', - () => { - it.each([ - [undefined], - ['application/json'], - ['multipart/mixed'], - ['multipart/mixed; deferSpec=12345'], - ])( - 'errors when @defer is used with accept: %s', - async (accept) => { - const app = await createApp({ typeDefs, resolvers }); - const req = request(app).post('/'); - if (accept) { - req.set('accept', accept); - } - const res = await req.send({ - query: '{ ... @defer { testString } }', - }); - expect(res.status).toEqual(406); - expect(res.body).toMatchInlineSnapshot(` + describe('tests that require graphql@17.0.0-alpha.2', () => { + it.each([ + [undefined], + ['application/json'], + ['multipart/mixed'], + ['multipart/mixed; deferSpec=12345'], + ])('errors when @defer is used with accept: %s', async (accept) => { + const app = await createApp({ typeDefs, resolvers }); + const req = request(app).post('/'); + if (accept) { + req.set('accept', accept); + } + const res = await req.send({ + query: '{ ... @defer { testString } }', + }); + expect(res.status).toEqual(406); + expect(res.body).toMatchInlineSnapshot(` { "errors": [ { @@ -2479,54 +2459,27 @@ export function defineIntegrationTestSuiteHttpServerTests( ], } `); - }, - ); - - it('errors if incompatible with installed graphql.js version', async () => { - const app = await createApp({ typeDefs, resolvers }); - const req = request(app) - .post('/') - .set( - 'accept', - 'multipart/mixed; incrementalDeliverySpec=3283f8a', - ); - const res = await req.send({ - query: '{ ... @defer { testString } }', + }); + + it.each([ + ['multipart/mixed; deferSpec=20220824'], + ['multipart/mixed; deferSpec=20220824, application/json'], + ['application/json, multipart/mixed; deferSpec=20220824'], + ])('basic @defer working with accept: %s', async (accept) => { + const app = await createApp({ typeDefs, resolvers }); + const res = await request(app) + .post('/') + .set('accept', accept) + // disables supertest's use of formidable for multipart + .parse(superagent.parse.text) + .send({ + query: '{ first: testString ... @defer { testString } }', }); - expect(res.status).toEqual(406); - expect(res.body).toMatchInlineSnapshot(` - { - "errors": [ - { - "extensions": { - "code": "BAD_REQUEST", - }, - "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream) with a spec version incompatible with this server. Please use the HTTP header 'Accept: multipart/mixed; deferSpec=20220824'.", - }, - ], - } - `); - }); - - it.each([ - ['multipart/mixed; deferSpec=20220824'], - ['multipart/mixed; deferSpec=20220824, application/json'], - ['application/json, multipart/mixed; deferSpec=20220824'], - ])('basic @defer working with accept: %s', async (accept) => { - const app = await createApp({ typeDefs, resolvers }); - const res = await request(app) - .post('/') - .set('accept', accept) - // disables supertest's use of formidable for multipart - .parse(superagent.parse.text) - .send({ - query: '{ first: testString ... @defer { testString } }', - }); - expect(res.status).toEqual(200); - expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; deferSpec=20220824"`, - ); - expect(res.text).toEqual(`\r + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; deferSpec=20220824"`, + ); + expect(res.text).toEqual(`\r ---\r content-type: application/json; charset=utf-8\r \r @@ -2537,48 +2490,48 @@ content-type: application/json; charset=utf-8\r {"hasNext":false,"incremental":[{"path":[],"data":{"testString":"it works"}}]}\r -----\r `); - }); + }); - it.each([ - ['multipart/mixed; deferSpec=20220824'], - ['multipart/mixed; deferSpec=20220824, application/json'], - ['application/json, multipart/mixed; deferSpec=20220824'], - ])('basic @stream working with accept: %s', async (accept) => { - const app = await createApp({ - typeDefs: `#graphql + it.each([ + ['multipart/mixed; deferSpec=20220824'], + ['multipart/mixed; deferSpec=20220824, application/json'], + ['application/json, multipart/mixed; deferSpec=20220824'], + ])('basic @stream working with accept: %s', async (accept) => { + const app = await createApp({ + typeDefs: `#graphql directive @stream(if: Boolean! = true, label: String, initialCount: Int! = 0) on FIELD type Query { testStrings: [String] } `, - resolvers: { - Query: { - testStrings: async function* () { - await new Promise((r) => setTimeout(r, 10)); - yield 'it works'; - - await new Promise((r) => setTimeout(r, 10)); - yield 'it works again'; - - await new Promise((r) => setTimeout(r, 10)); - yield 'it works again again'; - }, + resolvers: { + Query: { + testStrings: async function* () { + await new Promise((r) => setTimeout(r, 10)); + yield 'it works'; + + await new Promise((r) => setTimeout(r, 10)); + yield 'it works again'; + + await new Promise((r) => setTimeout(r, 10)); + yield 'it works again again'; }, }, + }, + }); + const res = await request(app) + .post('/') + .set('accept', accept) + // disables supertest's use of formidable for multipart + .parse(superagent.parse.text) + .send({ + query: '{ testStrings @stream }', }); - const res = await request(app) - .post('/') - .set('accept', accept) - // disables supertest's use of formidable for multipart - .parse(superagent.parse.text) - .send({ - query: '{ testStrings @stream }', - }); - expect(res.status).toEqual(200); - expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; deferSpec=20220824"`, - ); - expect(res.text).toEqual(`\r + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; deferSpec=20220824"`, + ); + expect(res.text).toEqual(`\r ---\r content-type: application/json; charset=utf-8\r \r @@ -2601,48 +2554,48 @@ content-type: application/json; charset=utf-8\r {"hasNext":false}\r -----\r `); - }); + }); - it('first payload sent while deferred field is blocking', async () => { - const app = await createApp({ typeDefs, resolvers }); - const gotFirstChunkBarrier = resolvable(); - const resPromise = request(app) - .post('/') - .set( - 'accept', - 'multipart/mixed; deferSpec=20220824, application/json', - ) - .parse((res, fn) => { - res.text = ''; - res.setEncoding('utf8'); - res.on('data', (chunk) => { - res.text += chunk; - if ( - res.text.includes('it works') && - res.text.endsWith('---\r\n') - ) { - gotFirstChunkBarrier.resolve(); - } - }); - res.on('end', fn); - }) - .send({ - query: '{ testString ... @defer { barrierString } }', - }) - // believe it or not, superagent uses `.then` to decide to actually send the request - .then((r) => r); - - // We ensure that the `barrierString` resolver isn't allowed to resolve - // until after we've gotten back a chunk containing the value of testString. - await gotFirstChunkBarrier; - barrierStringBarrier.resolve(); - - const res = await resPromise; - expect(res.status).toEqual(200); - expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; deferSpec=20220824"`, - ); - expect(res.text).toMatchInlineSnapshot(` + it('first payload sent while deferred field is blocking', async () => { + const app = await createApp({ typeDefs, resolvers }); + const gotFirstChunkBarrier = resolvable(); + const resPromise = request(app) + .post('/') + .set( + 'accept', + 'multipart/mixed; deferSpec=20220824, application/json', + ) + .parse((res, fn) => { + res.text = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => { + res.text += chunk; + if ( + res.text.includes('it works') && + res.text.endsWith('---\r\n') + ) { + gotFirstChunkBarrier.resolve(); + } + }); + res.on('end', fn); + }) + .send({ + query: '{ testString ... @defer { barrierString } }', + }) + // believe it or not, superagent uses `.then` to decide to actually send the request + .then((r) => r); + + // We ensure that the `barrierString` resolver isn't allowed to resolve + // until after we've gotten back a chunk containing the value of testString. + await gotFirstChunkBarrier; + barrierStringBarrier.resolve(); + + const res = await resPromise; + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; deferSpec=20220824"`, + ); + expect(res.text).toMatchInlineSnapshot(` " --- content-type: application/json; charset=utf-8 @@ -2655,31 +2608,26 @@ content-type: application/json; charset=utf-8\r ----- " `); - }); - }, - ); + }); + }); - (IS_GRAPHQL_17_ALPHA9 ? describe : describe.skip)( - 'tests that require graphql@17.0.0-alpha.9', - () => { - it.each([ - [undefined], - ['application/json'], - ['multipart/mixed'], - ['multipart/mixed; deferSpec=12345'], - ])( - 'errors when @defer is used with accept: %s', - async (accept) => { - const app = await createApp({ typeDefs, resolvers }); - const req = request(app).post('/'); - if (accept) { - req.set('accept', accept); - } - const res = await req.send({ - query: '{ ... @defer { testString } }', - }); - expect(res.status).toEqual(406); - expect(res.body).toMatchInlineSnapshot(` + describe('tests that require graphql@17.0.0-alpha.9', () => { + it.each([ + [undefined], + ['application/json'], + ['multipart/mixed'], + ['multipart/mixed; deferSpec=12345'], + ])('errors when @defer is used with accept: %s', async (accept) => { + const app = await createApp({ typeDefs, resolvers }); + const req = request(app).post('/'); + if (accept) { + req.set('accept', accept); + } + const res = await req.send({ + query: '{ ... @defer { testString } }', + }); + expect(res.status).toEqual(406); + expect(res.body).toMatchInlineSnapshot(` { "errors": [ { @@ -2691,55 +2639,31 @@ content-type: application/json; charset=utf-8\r ], } `); - }, - ); + }); - it('errors if incompatible with installed graphql.js version', async () => { - const app = await createApp({ typeDefs, resolvers }); - const req = request(app) - .post('/') - .set('accept', 'multipart/mixed; deferSpec=20220824'); - const res = await req.send({ - query: '{ ... @defer { testString } }', + it.each([ + ['multipart/mixed; incrementalDeliverySpec=3283f8a'], + [ + 'multipart/mixed; incrementalDeliverySpec=3283f8a, application/json', + ], + [ + 'application/json, multipart/mixed; incrementalDeliverySpec=3283f8a', + ], + ])('basic @defer working with accept: %s', async (accept) => { + const app = await createApp({ typeDefs, resolvers }); + const res = await request(app) + .post('/') + .set('accept', accept) + // disables supertest's use of formidable for multipart + .parse(superagent.parse.text) + .send({ + query: '{ first: testString ... @defer { testString } }', }); - expect(res.status).toEqual(406); - expect(res.body).toMatchInlineSnapshot(` - { - "errors": [ - { - "extensions": { - "code": "BAD_REQUEST", - }, - "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream) with a spec version incompatible with this server. Please use the HTTP header 'Accept: multipart/mixed; incrementalDeliverySpec=3283f8a'.", - }, - ], - } - `); - }); - - it.each([ - ['multipart/mixed; incrementalDeliverySpec=3283f8a'], - [ - 'multipart/mixed; incrementalDeliverySpec=3283f8a, application/json', - ], - [ - 'application/json, multipart/mixed; incrementalDeliverySpec=3283f8a', - ], - ])('basic @defer working with accept: %s', async (accept) => { - const app = await createApp({ typeDefs, resolvers }); - const res = await request(app) - .post('/') - .set('accept', accept) - // disables supertest's use of formidable for multipart - .parse(superagent.parse.text) - .send({ - query: '{ first: testString ... @defer { testString } }', - }); - expect(res.status).toEqual(200); - expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, - ); - expect(res.text).toEqual(`\r + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, + ); + expect(res.text).toEqual(`\r ---\r content-type: application/json; charset=utf-8\r \r @@ -2750,52 +2674,52 @@ content-type: application/json; charset=utf-8\r {"hasNext":false,"incremental":[{"id":"0","data":{"testString":"it works"}}],"completed":[{"id":"0"}]}\r -----\r `); - }); + }); - it.each([ - ['multipart/mixed; incrementalDeliverySpec=3283f8a'], - [ - 'multipart/mixed; incrementalDeliverySpec=3283f8a, application/json', - ], - [ - 'application/json, multipart/mixed; incrementalDeliverySpec=3283f8a', - ], - ])('basic @stream working with accept: %s', async (accept) => { - const app = await createApp({ - typeDefs: `#graphql + it.each([ + ['multipart/mixed; incrementalDeliverySpec=3283f8a'], + [ + 'multipart/mixed; incrementalDeliverySpec=3283f8a, application/json', + ], + [ + 'application/json, multipart/mixed; incrementalDeliverySpec=3283f8a', + ], + ])('basic @stream working with accept: %s', async (accept) => { + const app = await createApp({ + typeDefs: `#graphql directive @stream(if: Boolean! = true, label: String, initialCount: Int! = 0) on FIELD type Query { testStrings: [String] } `, - resolvers: { - Query: { - testStrings: async function* () { - await new Promise((r) => setTimeout(r, 10)); - yield 'it works'; - - await new Promise((r) => setTimeout(r, 10)); - yield 'it works again'; - - await new Promise((r) => setTimeout(r, 10)); - yield 'it works again again'; - }, + resolvers: { + Query: { + testStrings: async function* () { + await new Promise((r) => setTimeout(r, 10)); + yield 'it works'; + + await new Promise((r) => setTimeout(r, 10)); + yield 'it works again'; + + await new Promise((r) => setTimeout(r, 10)); + yield 'it works again again'; }, }, + }, + }); + const res = await request(app) + .post('/') + .set('accept', accept) + // disables supertest's use of formidable for multipart + .parse(superagent.parse.text) + .send({ + query: '{ testStrings @stream }', }); - const res = await request(app) - .post('/') - .set('accept', accept) - // disables supertest's use of formidable for multipart - .parse(superagent.parse.text) - .send({ - query: '{ testStrings @stream }', - }); - expect(res.status).toEqual(200); - expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, - ); - expect(res.text).toEqual(`\r + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, + ); + expect(res.text).toEqual(`\r ---\r content-type: application/json; charset=utf-8\r \r @@ -2818,48 +2742,48 @@ content-type: application/json; charset=utf-8\r {"hasNext":false,"completed":[{"id":"0"}]}\r -----\r `); - }); + }); - it('first payload sent while deferred field is blocking', async () => { - const app = await createApp({ typeDefs, resolvers }); - const gotFirstChunkBarrier = resolvable(); - const resPromise = request(app) - .post('/') - .set( - 'accept', - 'multipart/mixed; incrementalDeliverySpec=3283f8a, application/json', - ) - .parse((res, fn) => { - res.text = ''; - res.setEncoding('utf8'); - res.on('data', (chunk) => { - res.text += chunk; - if ( - res.text.includes('it works') && - res.text.endsWith('---\r\n') - ) { - gotFirstChunkBarrier.resolve(); - } - }); - res.on('end', fn); - }) - .send({ - query: '{ testString ... @defer { barrierString } }', - }) - // believe it or not, superagent uses `.then` to decide to actually send the request - .then((r) => r); - - // We ensure that the `barrierString` resolver isn't allowed to resolve - // until after we've gotten back a chunk containing the value of testString. - await gotFirstChunkBarrier; - barrierStringBarrier.resolve(); - - const res = await resPromise; - expect(res.status).toEqual(200); - expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, - ); - expect(res.text).toMatchInlineSnapshot(` + it('first payload sent while deferred field is blocking', async () => { + const app = await createApp({ typeDefs, resolvers }); + const gotFirstChunkBarrier = resolvable(); + const resPromise = request(app) + .post('/') + .set( + 'accept', + 'multipart/mixed; incrementalDeliverySpec=3283f8a, application/json', + ) + .parse((res, fn) => { + res.text = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => { + res.text += chunk; + if ( + res.text.includes('it works') && + res.text.endsWith('---\r\n') + ) { + gotFirstChunkBarrier.resolve(); + } + }); + res.on('end', fn); + }) + .send({ + query: '{ testString ... @defer { barrierString } }', + }) + // believe it or not, superagent uses `.then` to decide to actually send the request + .then((r) => r); + + // We ensure that the `barrierString` resolver isn't allowed to resolve + // until after we've gotten back a chunk containing the value of testString. + await gotFirstChunkBarrier; + barrierStringBarrier.resolve(); + + const res = await resPromise; + expect(res.status).toEqual(200); + expect(res.header['content-type']).toMatchInlineSnapshot( + `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, + ); + expect(res.text).toMatchInlineSnapshot(` " --- content-type: application/json; charset=utf-8 @@ -2872,9 +2796,8 @@ content-type: application/json; charset=utf-8\r ----- " `); - }); - }, - ); + }); + }); }); }, ); From 6c005a78453aebb4245c544454514f01230786dc Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Sun, 28 Sep 2025 21:44:54 -0600 Subject: [PATCH 63/93] Update error message --- packages/server/src/runHttpQuery.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index 373c9542c90..3bff22deee8 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -300,7 +300,10 @@ export async function runHttpQuery({ 'Apollo server received an operation that uses incremental delivery ' + '(@defer or @stream), but the client does not accept multipart/mixed ' + 'HTTP responses. To enable incremental delivery support, add the HTTP ' + - `header 'Accept: ${MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9}'.`, + `header 'Accept: ${MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9}' ` + + 'if your client supports the modern incremental format or ' + + `'Accept: ${MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2}' if your ` + + 'client supports the legacy incremental format', // Use 406 Not Accepted { extensions: { http: { status: 406 } } }, ); From 01ac540ed19249091d940e839352384d033fa7a1 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Sun, 28 Sep 2025 22:08:11 -0600 Subject: [PATCH 64/93] Track legacyExecute and executeIncrementally in separate vars --- .../server/src/incrementalDeliveryPolyfill.ts | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/server/src/incrementalDeliveryPolyfill.ts b/packages/server/src/incrementalDeliveryPolyfill.ts index cfc9f748d25..fceb1e6322b 100644 --- a/packages/server/src/incrementalDeliveryPolyfill.ts +++ b/packages/server/src/incrementalDeliveryPolyfill.ts @@ -184,31 +184,44 @@ let graphqlExperimentalExecuteIncrementally: | (( args: ExecutionArgs, ) => PromiseOrValue< - | ExecutionResult - | GraphQLExperimentalIncrementalExecutionResultsAlpha2 - | GraphQLExperimentalIncrementalExecutionResultsAlpha9 + ExecutionResult | GraphQLExperimentalIncrementalExecutionResultsAlpha9 >) | null | undefined = undefined; +let legacyExecuteIncrementally: + | (( + args: ExecutionArgs, + ) => PromiseOrValue< + ExecutionResult | GraphQLExperimentalIncrementalExecutionResultsAlpha2 + >) + | null + | undefined; + async function tryToLoadLegacyExecuteIncrementally() { try { - const { legacyExecuteIncrementally } = await import('@yaacovcr/transform'); - graphqlExperimentalExecuteIncrementally = legacyExecuteIncrementally; + const transform = await import('@yaacovcr/transform'); + legacyExecuteIncrementally = transform.legacyExecuteIncrementally; } catch { - return; + legacyExecuteIncrementally = null; } } async function tryToLoadGraphQL17() { - if (graphqlExperimentalExecuteIncrementally !== undefined) { + if ( + graphqlExperimentalExecuteIncrementally !== undefined || + legacyExecuteIncrementally !== undefined + ) { return; } + const graphql = await import('graphql'); if ( graphql.version === '17.0.0-alpha.9' && 'experimentalExecuteIncrementally' in graphql ) { + await tryToLoadLegacyExecuteIncrementally(); + graphqlExperimentalExecuteIncrementally = (graphql as any) .experimentalExecuteIncrementally; } else { @@ -224,10 +237,12 @@ export async function executeIncrementally({ | GraphQLExperimentalIncrementalExecutionResultsAlpha2 | GraphQLExperimentalIncrementalExecutionResultsAlpha9 > { - if (useLegacyIncremental) { - await tryToLoadLegacyExecuteIncrementally(); - } await tryToLoadGraphQL17(); + + if (useLegacyIncremental && legacyExecuteIncrementally) { + return legacyExecuteIncrementally(args); + } + if (graphqlExperimentalExecuteIncrementally) { return graphqlExperimentalExecuteIncrementally(args); } From 6ffc9aa033b5e4567ffc6d395f2d54f4c8fc5264 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Sun, 28 Sep 2025 22:11:09 -0600 Subject: [PATCH 65/93] Use legacy format when header used --- packages/server/src/requestPipeline.ts | 33 ++++++++++++++++++-------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/server/src/requestPipeline.ts b/packages/server/src/requestPipeline.ts index 1542075f318..3befb9e95c1 100644 --- a/packages/server/src/requestPipeline.ts +++ b/packages/server/src/requestPipeline.ts @@ -53,10 +53,11 @@ import { import { makeGatewayGraphQLRequestContext } from './utils/makeGatewayGraphQLRequestContext.js'; import { mergeHTTPGraphQLHead, newHTTPGraphQLHead } from './runHttpQuery.js'; -import type { - ApolloServer, - ApolloServerInternals, - SchemaDerivedData, +import { + MEDIA_TYPES, + type ApolloServer, + type ApolloServerInternals, + type SchemaDerivedData, } from './ApolloServer.js'; import { isDefined } from './utils/isDefined.js'; import type { @@ -71,6 +72,7 @@ import { type GraphQLExperimentalSubsequentIncrementalExecutionResultAlpha2, } from './incrementalDeliveryPolyfill.js'; import { HeaderMap } from './utils/HeaderMap.js'; +import Negotiator from 'negotiator'; export const APQ_CACHE_PREFIX = 'apq:'; @@ -448,9 +450,16 @@ export async function processGraphQLRequest( } try { - const fullResult = await execute( - requestContext as GraphQLRequestContextExecutionDidStart, - ); + const fullResult = await execute({ + ...requestContext, + useLegacyIncremental: + new Negotiator({ + headers: { accept: request.http?.headers.get('accept') }, + }).mediaType([ + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9, + MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2, + ]) === MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2, + } as GraphQLRequestContextExecutionDidStart); const result = 'singleResult' in fullResult ? fullResult.singleResult @@ -547,9 +556,12 @@ export async function processGraphQLRequest( } return requestContext.response as GraphQLResponse; // cast checked on previous line - async function execute( - requestContext: GraphQLRequestContextExecutionDidStart, - ): Promise { + async function execute({ + useLegacyIncremental, + ...requestContext + }: GraphQLRequestContextExecutionDidStart & { + useLegacyIncremental?: boolean; + }): Promise { const { request, document } = requestContext; if (internals.__testing_incrementalExecutionResults) { @@ -571,6 +583,7 @@ export async function processGraphQLRequest( variableValues: request.variables, operationName: request.operationName, fieldResolver: internals.fieldResolver, + useLegacyIncremental, }); if ('initialResult' in resultOrResults) { return { From 023425b82e6f2cc33a8f06c351f9b3c06453f0fc Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Sun, 28 Sep 2025 22:12:33 -0600 Subject: [PATCH 66/93] Update error snapshots --- .../src/httpServerTests.ts | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index 1ca9008cd92..25f1b30aae3 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -2448,17 +2448,17 @@ export function defineIntegrationTestSuiteHttpServerTests( }); expect(res.status).toEqual(406); expect(res.body).toMatchInlineSnapshot(` - { - "errors": [ - { - "extensions": { - "code": "BAD_REQUEST", - }, - "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream), but the client does not accept multipart/mixed HTTP responses. To enable incremental delivery support, add the HTTP header 'Accept: multipart/mixed; deferSpec=20220824'.", - }, - ], - } - `); + { + "errors": [ + { + "extensions": { + "code": "BAD_REQUEST", + }, + "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream), but the client does not accept multipart/mixed HTTP responses. To enable incremental delivery support, add the HTTP header 'Accept: multipart/mixed; incrementalDeliverySpec=3283f8a' if your client supports the modern incremental format or 'Accept: multipart/mixed; deferSpec=20220824' if your client supports the legacy incremental format", + }, + ], + } + `); }); it.each([ @@ -2628,17 +2628,17 @@ content-type: application/json; charset=utf-8\r }); expect(res.status).toEqual(406); expect(res.body).toMatchInlineSnapshot(` - { - "errors": [ - { - "extensions": { - "code": "BAD_REQUEST", - }, - "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream), but the client does not accept multipart/mixed HTTP responses. To enable incremental delivery support, add the HTTP header 'Accept: multipart/mixed; incrementalDeliverySpec=3283f8a'.", - }, - ], - } - `); + { + "errors": [ + { + "extensions": { + "code": "BAD_REQUEST", + }, + "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream), but the client does not accept multipart/mixed HTTP responses. To enable incremental delivery support, add the HTTP header 'Accept: multipart/mixed; incrementalDeliverySpec=3283f8a' if your client supports the modern incremental format or 'Accept: multipart/mixed; deferSpec=20220824' if your client supports the legacy incremental format", + }, + ], + } + `); }); it.each([ From 18175deb19478de373505896d1505b8757970966 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Sun, 28 Sep 2025 22:13:27 -0600 Subject: [PATCH 67/93] Rename test descriptions --- packages/integration-testsuite/src/httpServerTests.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index 25f1b30aae3..c3f031ebc2d 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -2431,7 +2431,7 @@ export function defineIntegrationTestSuiteHttpServerTests( }, }; - describe('tests that require graphql@17.0.0-alpha.2', () => { + describe('tests that require legacy incremental format', () => { it.each([ [undefined], ['application/json'], @@ -2611,7 +2611,7 @@ content-type: application/json; charset=utf-8\r }); }); - describe('tests that require graphql@17.0.0-alpha.9', () => { + describe('tests that require modern incremental format', () => { it.each([ [undefined], ['application/json'], From aff5045f7b3df18bc39c3c659ae2db42b67b6933 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Sun, 28 Sep 2025 22:15:36 -0600 Subject: [PATCH 68/93] Update apolloServerTests to remove check on version --- .../src/apolloServerTests.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/integration-testsuite/src/apolloServerTests.ts b/packages/integration-testsuite/src/apolloServerTests.ts index b50d77805c1..d5e51055a09 100644 --- a/packages/integration-testsuite/src/apolloServerTests.ts +++ b/packages/integration-testsuite/src/apolloServerTests.ts @@ -16,7 +16,6 @@ import { type FieldNode, type GraphQLFormattedError, GraphQLScalarType, - version as graphqlVersion, } from 'graphql'; // Note that by doing deep imports here we don't need to install React. @@ -1179,11 +1178,8 @@ export function defineIntegrationTestSuiteApolloServerTests( expect(Object.keys(reports[0].tracesPerQuery)[0]).toMatch(/^# -\n/); }); - (process.env.INCREMENTAL_DELIVERY_TESTS_ENABLED && - graphqlVersion === '17.0.0-alpha.2' - ? it - : it.skip)( - 'includes all fields with defer graphql@17.0.0-alpha.2', + (process.env.INCREMENTAL_DELIVERY_TESTS_ENABLED ? it : it.skip)( + 'includes all fields with defer legacy', async () => { await setupApolloServerAndFetchPair({}, {}, [], true); const response = await fetch(uri, { @@ -1229,11 +1225,8 @@ export function defineIntegrationTestSuiteApolloServerTests( }, ); - (process.env.INCREMENTAL_DELIVERY_TESTS_ENABLED && - graphqlVersion === '17.0.0-alpha.9' - ? it - : it.skip)( - 'includes all fields with defer graphql@17.0.0-alpha.9', + (process.env.INCREMENTAL_DELIVERY_TESTS_ENABLED ? it : it.skip)( + 'includes all fields with defer modern', async () => { await setupApolloServerAndFetchPair({}, {}, [], true); const response = await fetch(uri, { From b151633977016e27d9529255a63165b077bc7adf Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Sun, 28 Sep 2025 22:17:37 -0600 Subject: [PATCH 69/93] Only run alpha.9 tests in circleci --- .circleci/config.yml | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index adca9cd27c9..1d43fdb640a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -60,23 +60,7 @@ jobs: - setup-node - run: npm run test:smoke - Full incremental delivery tests with graphql 17 alpha 2: - docker: - - image: cimg/base:stable - environment: - INCREMENTAL_DELIVERY_TESTS_ENABLED: t - GRAPHQL_JS_VERSION: 17.0.0-alpha.2 - steps: - - setup-node: - node-version: "20" - # Install a prerelease of graphql-js 17 with incremental delivery support. - # --legacy-peer-deps because nothing expects v17 yet. - - run: npm i --legacy-peer-deps "graphql@${GRAPHQL_JS_VERSION}" - - run: npm run test:ci - - maybe-upload-coverage - - run: npm run test:smoke - - Test with recent graphql-js alpha: + Full incremental delivery tests with graphql 17 alpha 9: docker: - image: cimg/base:stable environment: @@ -85,11 +69,11 @@ jobs: steps: - setup-node: node-version: "20" - # Install a newer prerelease of graphql-js 17; we do not yet support - # its incremental delivery format. + # Install a prerelease of graphql-js 17 with incremental delivery support. # --legacy-peer-deps because nothing expects v17 yet. - run: npm i --legacy-peer-deps "graphql@${GRAPHQL_JS_VERSION}" @yaacovcr/transform - run: npm run test:ci + - maybe-upload-coverage - run: npm run test:smoke Prettier: From 025d29264a67f0605f6b422cd04a082448d30636 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Sun, 28 Sep 2025 22:21:52 -0600 Subject: [PATCH 70/93] Update smoke test to check both versions --- smoke-test/smoke-test.cjs | 53 +++++++++++++++++++++++--------------- smoke-test/smoke-test.mjs | 54 ++++++++++++++++++++++++--------------- 2 files changed, 67 insertions(+), 40 deletions(-) diff --git a/smoke-test/smoke-test.cjs b/smoke-test/smoke-test.cjs index 54ba607a4d1..54cbac99ea2 100644 --- a/smoke-test/smoke-test.cjs +++ b/smoke-test/smoke-test.cjs @@ -2,7 +2,6 @@ const { ApolloServer } = require('@apollo/server'); const { startStandaloneServer } = require('@apollo/server/standalone'); const fetch = require('make-fetch-happen'); const assert = require('assert'); -const { version: graphqlVersion } = require('graphql'); async function validateAllImports() { require('@apollo/server'); @@ -50,27 +49,23 @@ async function smokeTest() { } if (process.env.INCREMENTAL_DELIVERY_TESTS_ENABLED) { - const specVersion = - graphqlVersion === '17.0.0-alpha.9' - ? 'incrementalDeliverySpec=3283f8a' - : 'deferSpec=20220824'; - const response = await fetch(url, { - method: 'POST', - headers: { - 'content-type': 'application/json', - accept: `multipart/mixed; ${specVersion}, application/json`, - }, - body: JSON.stringify({ query: '{h1: hello ...@defer{ h2: hello }}' }), - }); + { + const response = await fetch(url, { + method: 'POST', + headers: { + 'content-type': 'application/json', + accept: `multipart/mixed; deferSpec=20220824, application/json`, + }, + body: JSON.stringify({ query: '{h1: hello ...@defer{ h2: hello }}' }), + }); - assert.strictEqual( - response.headers.get('content-type'), - `multipart/mixed; boundary="-"; ${specVersion}`, - ); + assert.strictEqual( + response.headers.get('content-type'), + `multipart/mixed; boundary="-"; deferSpec=20220824`, + ); - const body = await response.text(); + const body = await response.text(); - if (graphqlVersion === '17.0.0-alpha.2') { assert.strictEqual( body, '\r\n' + @@ -84,7 +79,25 @@ async function smokeTest() { '{"hasNext":false,"incremental":[{"path":[],"data":{"h2":"world"}}]}\r\n' + '-----\r\n', ); - } else { + } + + { + const response = await fetch(url, { + method: 'POST', + headers: { + 'content-type': 'application/json', + accept: `multipart/mixed; incrementalDeliverySpec=3283f8a, application/json`, + }, + body: JSON.stringify({ query: '{h1: hello ...@defer{ h2: hello }}' }), + }); + + assert.strictEqual( + response.headers.get('content-type'), + `multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a`, + ); + + const body = await response.text(); + assert.strictEqual( body, '\r\n' + diff --git a/smoke-test/smoke-test.mjs b/smoke-test/smoke-test.mjs index 8875515fb2f..52a46af8bda 100644 --- a/smoke-test/smoke-test.mjs +++ b/smoke-test/smoke-test.mjs @@ -2,7 +2,6 @@ import { ApolloServer } from '@apollo/server'; import { startStandaloneServer } from '@apollo/server/standalone'; import fetch from 'make-fetch-happen'; import assert from 'assert'; -import { version as graphqlVersion } from 'graphql'; // validate all deep imports await import('@apollo/server'); @@ -46,27 +45,23 @@ const { url } = await startStandaloneServer(s, { listen: { port: 0 } }); } if (process.env.INCREMENTAL_DELIVERY_TESTS_ENABLED) { - const specVersion = - graphqlVersion === '17.0.0-alpha.9' - ? 'incrementalDeliverySpec=3283f8a' - : 'deferSpec=20220824'; - const response = await fetch(url, { - method: 'POST', - headers: { - 'content-type': 'application/json', - accept: `multipart/mixed; ${specVersion}, application/json`, - }, - body: JSON.stringify({ query: '{h1: hello ...@defer{ h2: hello }}' }), - }); + { + const response = await fetch(url, { + method: 'POST', + headers: { + 'content-type': 'application/json', + accept: `multipart/mixed; deferSpec=20220824, application/json`, + }, + body: JSON.stringify({ query: '{h1: hello ...@defer{ h2: hello }}' }), + }); - assert.strictEqual( - response.headers.get('content-type'), - `multipart/mixed; boundary="-"; ${specVersion}`, - ); + assert.strictEqual( + response.headers.get('content-type'), + `multipart/mixed; boundary="-"; deferSpec=20220824`, + ); - const body = await response.text(); + const body = await response.text(); - if (graphqlVersion === '17.0.0-alpha.2') { assert.strictEqual( body, '\r\n' + @@ -80,7 +75,25 @@ if (process.env.INCREMENTAL_DELIVERY_TESTS_ENABLED) { '{"hasNext":false,"incremental":[{"path":[],"data":{"h2":"world"}}]}\r\n' + '-----\r\n', ); - } else { + } + + { + const response = await fetch(url, { + method: 'POST', + headers: { + 'content-type': 'application/json', + accept: `multipart/mixed; incrementalDeliverySpec=3283f8a, application/json`, + }, + body: JSON.stringify({ query: '{h1: hello ...@defer{ h2: hello }}' }), + }); + + assert.strictEqual( + response.headers.get('content-type'), + `multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a`, + ); + + const body = await response.text(); + assert.strictEqual( body, '\r\n' + @@ -96,6 +109,7 @@ if (process.env.INCREMENTAL_DELIVERY_TESTS_ENABLED) { ); } } + await s.stop(); console.log('ESM smoke test passed!'); From a17ffff6e205860611367adedf9a9d7b899fa447 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Sun, 28 Sep 2025 22:28:36 -0600 Subject: [PATCH 71/93] Fix ref in circle config --- .circleci/config.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1d43fdb640a..f357ed88393 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -157,8 +157,7 @@ workflows: - Spell check - Codegen check - Smoke test built package - - Full incremental delivery tests with graphql 17 alpha 2 - - Test with recent graphql-js alpha + - Full incremental delivery tests with graphql 17 alpha 9 - Changesets security-scans: jobs: From 1ad20666d27e87d059bc64323dc835d374b66153 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 29 Sep 2025 08:55:08 -0600 Subject: [PATCH 72/93] Add ts-ignore --- packages/server/src/incrementalDeliveryPolyfill.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/server/src/incrementalDeliveryPolyfill.ts b/packages/server/src/incrementalDeliveryPolyfill.ts index fceb1e6322b..0559e974406 100644 --- a/packages/server/src/incrementalDeliveryPolyfill.ts +++ b/packages/server/src/incrementalDeliveryPolyfill.ts @@ -200,6 +200,7 @@ let legacyExecuteIncrementally: async function tryToLoadLegacyExecuteIncrementally() { try { + // @ts-ignore `@yaacovcr/transform` is an optional peer dependency const transform = await import('@yaacovcr/transform'); legacyExecuteIncrementally = transform.legacyExecuteIncrementally; } catch { From 6f1e3e6717bc66487ba4dbfa08ee8645b185beac Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 29 Sep 2025 11:55:55 -0600 Subject: [PATCH 73/93] Throw if old format requested but not supported --- .../server/src/incrementalDeliveryPolyfill.ts | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/server/src/incrementalDeliveryPolyfill.ts b/packages/server/src/incrementalDeliveryPolyfill.ts index 0559e974406..011bfe95beb 100644 --- a/packages/server/src/incrementalDeliveryPolyfill.ts +++ b/packages/server/src/incrementalDeliveryPolyfill.ts @@ -4,6 +4,8 @@ import { type ExecutionResult, type GraphQLError, } from 'graphql'; +import { BadRequestError } from './internalErrorClasses.js'; +import { MEDIA_TYPES } from './ApolloServer.js'; // This file "polyfills" graphql@17's experimentalExecuteIncrementally (by // returning a function that does not understand incremental directives if @@ -240,8 +242,25 @@ export async function executeIncrementally({ > { await tryToLoadGraphQL17(); - if (useLegacyIncremental && legacyExecuteIncrementally) { - return legacyExecuteIncrementally(args); + if (useLegacyIncremental) { + if (legacyExecuteIncrementally) { + return legacyExecuteIncrementally(args); + } + + // Only throw if the server supports incremental delivery with the new + // format, but not the legacy foramt. We don't want to accidentally send + // alpha.9 format when the client requested the legacy format. + if (graphqlExperimentalExecuteIncrementally) { + throw new BadRequestError( + 'Apollo Server received an operation that uses incremental delivery ' + + '(@defer or @stream) with the legacy incremental format, but the server ' + + 'does not support the legacy incremental delivery format. Add the HTTP ' + + `header: 'Accept: ${MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9}' ` + + 'to use the modern incremental delivery format', + // Use 406 Not Accepted + { extensions: { http: { status: 406 } } }, + ); + } } if (graphqlExperimentalExecuteIncrementally) { From b7e1661ec894283810e4590de8bc916e3ce8573e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 29 Sep 2025 12:06:30 -0600 Subject: [PATCH 74/93] Install @yaacovcr/transform in prepare.sh --- smoke-test/prepare.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/smoke-test/prepare.sh b/smoke-test/prepare.sh index 5490c315f81..0452c82cb51 100755 --- a/smoke-test/prepare.sh +++ b/smoke-test/prepare.sh @@ -21,7 +21,8 @@ npm i if [[ -n "${GRAPHQL_JS_VERSION:-}" ]]; then npm i --no-save --legacy-peer-deps \ "$TARBALL_DIR"/*.tgz \ - "graphql@${GRAPHQL_JS_VERSION}" + "graphql@${GRAPHQL_JS_VERSION}" \ + "@yaacovcr/transform" else npm i --no-save "$TARBALL_DIR"/*.tgz fi From 0f8d32daf1225c0a7c12549af0bc020da0174893 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 29 Sep 2025 12:09:50 -0600 Subject: [PATCH 75/93] Add yaacovcr to cspell dict --- cspell-dict.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/cspell-dict.txt b/cspell-dict.txt index 312dbf1da1a..e321a69ee67 100644 --- a/cspell-dict.txt +++ b/cspell-dict.txt @@ -221,4 +221,5 @@ whatwg Wheelock's withrequired xorby +yaacovcr YOURNAME From 1d101dc0dfb6e1a636ed2b0cad60cd2d7f6762e8 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 29 Sep 2025 12:10:00 -0600 Subject: [PATCH 76/93] Fix typo --- packages/server/src/incrementalDeliveryPolyfill.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/incrementalDeliveryPolyfill.ts b/packages/server/src/incrementalDeliveryPolyfill.ts index 011bfe95beb..aaba54ac08b 100644 --- a/packages/server/src/incrementalDeliveryPolyfill.ts +++ b/packages/server/src/incrementalDeliveryPolyfill.ts @@ -248,7 +248,7 @@ export async function executeIncrementally({ } // Only throw if the server supports incremental delivery with the new - // format, but not the legacy foramt. We don't want to accidentally send + // format, but not the legacy format. We don't want to accidentally send // alpha.9 format when the client requested the legacy format. if (graphqlExperimentalExecuteIncrementally) { throw new BadRequestError( From 13f005c04bc7b9307bcf9dcd4549a997198e368b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 29 Sep 2025 13:49:33 -0600 Subject: [PATCH 77/93] Update changeset --- .changeset/fruity-ways-tell.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.changeset/fruity-ways-tell.md b/.changeset/fruity-ways-tell.md index 97c57bc4831..2b739763ad3 100644 --- a/.changeset/fruity-ways-tell.md +++ b/.changeset/fruity-ways-tell.md @@ -2,4 +2,6 @@ '@apollo/server': minor --- -Add support for the `graphql@17.0.0-alpha.9` `@defer` and `@stream` incremental delivery protocol. When `graphql@17.0.0-alpha.9` is installed, clients must send the `Accept` header with a value of `multipart/mixed; incrementalDeliverySpec=3283f8a` to specify the new format. If the `Accept` header is not compatible with the installed version of `graphql`, an error is returned to the client. +Add support for the modern modern incremental delivery protocol (`@defer` and `@stream`) that ships with `graphql@17.0.0-alpha.9`. To use the modern protocol, clients must send the `Accept` header with a value of `multipart/mixed; incrementalDeliverySpec=3283f8a`. + +Support for the legacy incremental delivery protocol is still possible by installing the [`@yaacovcr/transform`](https://github.com/yaacovCR/transform) package. Apollo Server will attempt to load this module when the client specifies an `Accept` header with a value of `multipart/mixed; deferSpec=20220824`. If this package is not installed, an error is returned by the server. From a7cbae68b86b9a0e5188f80825b712c2399d4807 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 29 Sep 2025 13:51:50 -0600 Subject: [PATCH 78/93] Add another changeset --- .changeset/wild-candies-deny.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wild-candies-deny.md diff --git a/.changeset/wild-candies-deny.md b/.changeset/wild-candies-deny.md new file mode 100644 index 00000000000..150dcacba29 --- /dev/null +++ b/.changeset/wild-candies-deny.md @@ -0,0 +1,5 @@ +--- +'@apollo/server': minor +--- + +Apollo Server now requires `graphql@17.0.0-alpha.9` in order to use the incremental delivery protocol. Please upgrade from `graphql@17.0.0-alpha.2`. From f697a2c5bd946a3d98f60daf09dcba5109da3c26 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 29 Sep 2025 13:54:21 -0600 Subject: [PATCH 79/93] Update documentation --- docs/source/workflow/requests.md | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/docs/source/workflow/requests.md b/docs/source/workflow/requests.md index 6f228a4f916..ae712508cf1 100644 --- a/docs/source/workflow/requests.md +++ b/docs/source/workflow/requests.md @@ -91,7 +91,7 @@ For more details, see [the CSRF prevention documentation](../security/cors#preve ## Incremental delivery (experimental) -Incremental delivery is a [Stage 1: Proposal](https://github.com/graphql/graphql-spec/pull/1110) to the GraphQL specification which adds `@defer` and `@stream` executable directives. These directives allow clients to specify that parts of an operation can be sent after an initial response, so that slower fields do not delay all other fields. As of June 2025, the `graphql` library (also known as `graphql-js`) upon which Apollo Server is built implements incremental delivery only in the unreleased major version 17. If a pre-release of `graphql@17.0.0-alpha.2` or `graphql@17.0.0-alpha.9` is installed in your server, Apollo Server can execute these incremental delivery directives and provide streaming [`multipart/mixed`](https://github.com/graphql/graphql-over-http/blob/main/rfcs/IncrementalDelivery.md) responses. +Incremental delivery is a [Stage 1: Proposal](https://github.com/graphql/graphql-spec/pull/1110) to the GraphQL specification which adds `@defer` and `@stream` executable directives. These directives allow clients to specify that parts of an operation can be sent after an initial response, so that slower fields do not delay all other fields. As of June 2025, the `graphql` library (also known as `graphql-js`) upon which Apollo Server is built implements incremental delivery only in the unreleased major version 17. If a pre-release of `graphql@17.0.0-alpha.9` is installed in your server, Apollo Server can execute these incremental delivery directives and provide streaming [`multipart/mixed`](https://github.com/graphql/graphql-over-http/blob/main/rfcs/IncrementalDelivery.md) responses. Support for incremental delivery in graphql version 17 is [opt-in](https://github.com/robrichard/defer-stream-wg/discussions/12), meaning the directives are not defined by default. In order to use `@defer` or `@stream`, you must provide the appropriate definition(s) in your SDL. The definitions below can be pasted into your schema as-is: @@ -119,8 +119,30 @@ const schema = new GraphQLSchema({ }); ``` -Clients sending operations with incremental delivery directives need to explicitly indicate that they are expecting to receive `multipart/mixed` responses in an `accept` header. Moreover, because incremental delivery has not yet been finalized in the GraphQL spec and may change before the final version, they need to specify that they expect the particular response format that Apollo Server produces today via a `deferSpec` parameter (for `17.0.0-alpha.2`) or `incrementalDeliverySpec` parameter (for `17.0.0-alpha.9`). Specifically, clients prepared to accept incremental delivery responses should send an `accept` header like `multipart/mixed; deferSpec=20220824` or `multipart/mixed; incrementalDeliverySpec=3283f8a`. Note that this header implies that *only* multipart responses should be accepted; typically, clients will send an accept header like `multipart/mixed; deferSpec=20220824, application/json` indicating that either multipart or single-part responses are acceptable. +Clients sending operations with incremental delivery directives need to explicitly indicate that they are expecting to receive `multipart/mixed` responses in an `accept` header. Moreover, because incremental delivery has not yet been finalized in the GraphQL spec and may change before the final version, they need to specify that they expect the particular response format that Apollo Server produces today via a `incrementalDeliverySpec` parameter. Specifically, clients prepared to accept incremental delivery responses should send an `accept` header like `multipart/mixed; incrementalDeliverySpec=3283f8a`. Note that this header implies that *only* multipart responses should be accepted; typically, clients will send an accept header like `multipart/mixed; incrementalDeliverySpec=3283f8a, application/json` indicating that either multipart or single-part responses are acceptable. -> Apollo Server *only* supports the specific pre-release `graphql@17.0.0-alpha.2` or `graphql@17.0.0-alpha.9`. Apollo Server 5 checks the version of `graphql` you have installed and will not attempt to support incremental delivery unless it is precisely `17.0.0-alpha.2` or `17.0.0-alpha.9`. +> Apollo Server *only* supports the specific pre-release `graphql@17.0.0-alpha.9`. Apollo Server 5 checks the version of `graphql` you have installed and will not attempt to support incremental delivery unless it is precisely `17.0.0-alpha.9`. You cannot combine [batching](#batching) with incremental delivery in the same request. + + + +Apollo Server 5.1 changed the required pre-release `graphql` version from `17.0.0-alpha-2` to `17.0.0-alpha.9`. If you are using 5.0 or below, use `graphql` version `17.0.0-alpha.2` instead. + + + + + +### Add support for the legacy incremental delivery protocol + + + +Clients may request the legacy incremental delivery protocol by specifing an `accept` header with a value of `multipart/mixed; deferSpec=20220824`. Apollo Server does not support the legacy incremental delivery protocol by default and an error will be returned to the client. + +You may choose to support the legacy incremental delivery protocol by installing the [`@yaacovcr/transform` package](https://github.com/yaacovCR/transform) which provides the needed utilities to format the incremental result using the legacy protocol. + +```sh +npm install @yaacovcr/transform +``` + +There is nothing else to configure. Apollo Server will load the necessary utility if this package is installed. From ef1938d0c164b4d9bf34c82245238869a38b027b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 29 Sep 2025 14:09:19 -0600 Subject: [PATCH 80/93] Fix typo --- docs/source/workflow/requests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/workflow/requests.md b/docs/source/workflow/requests.md index ae712508cf1..57378256401 100644 --- a/docs/source/workflow/requests.md +++ b/docs/source/workflow/requests.md @@ -137,7 +137,7 @@ Apollo Server 5.1 changed the required pre-release `graphql` version from `17.0. -Clients may request the legacy incremental delivery protocol by specifing an `accept` header with a value of `multipart/mixed; deferSpec=20220824`. Apollo Server does not support the legacy incremental delivery protocol by default and an error will be returned to the client. +Clients may request the legacy incremental delivery protocol by specifying an `accept` header with a value of `multipart/mixed; deferSpec=20220824`. Apollo Server does not support the legacy incremental delivery protocol by default and an error will be returned to the client. You may choose to support the legacy incremental delivery protocol by installing the [`@yaacovcr/transform` package](https://github.com/yaacovCR/transform) which provides the needed utilities to format the incremental result using the legacy protocol. From 18a9868d93008565a693b0ca119f4ef6c9488f36 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 29 Sep 2025 14:18:56 -0600 Subject: [PATCH 81/93] Use && instead of || --- packages/server/src/incrementalDeliveryPolyfill.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/incrementalDeliveryPolyfill.ts b/packages/server/src/incrementalDeliveryPolyfill.ts index aaba54ac08b..b3704ba901e 100644 --- a/packages/server/src/incrementalDeliveryPolyfill.ts +++ b/packages/server/src/incrementalDeliveryPolyfill.ts @@ -212,7 +212,7 @@ async function tryToLoadLegacyExecuteIncrementally() { async function tryToLoadGraphQL17() { if ( - graphqlExperimentalExecuteIncrementally !== undefined || + graphqlExperimentalExecuteIncrementally !== undefined && legacyExecuteIncrementally !== undefined ) { return; From fc889d89612f7b250bb41ee6d05f024cb8ed45f0 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 1 Oct 2025 14:51:34 -0600 Subject: [PATCH 82/93] Remove ordering on incremental fields --- .../src/apolloServerTests.ts | 48 +++---- .../src/httpServerTests.ts | 136 +++++++++--------- packages/server/src/runHttpQuery.ts | 101 +------------ smoke-test/smoke-test.cjs | 8 +- smoke-test/smoke-test.mjs | 8 +- 5 files changed, 102 insertions(+), 199 deletions(-) diff --git a/packages/integration-testsuite/src/apolloServerTests.ts b/packages/integration-testsuite/src/apolloServerTests.ts index d5e51055a09..d699fafa029 100644 --- a/packages/integration-testsuite/src/apolloServerTests.ts +++ b/packages/integration-testsuite/src/apolloServerTests.ts @@ -1199,18 +1199,18 @@ export function defineIntegrationTestSuiteApolloServerTests( `"multipart/mixed; boundary="-"; deferSpec=20220824"`, ); expect(await response.text()).toMatchInlineSnapshot(` - " - --- - content-type: application/json; charset=utf-8 - - {"hasNext":true,"data":{"justAField":"a string"}} - --- - content-type: application/json; charset=utf-8 - - {"hasNext":false,"incremental":[{"path":[],"data":{"delayedFoo":{"bar":"hi"}}}]} - ----- - " - `); + " + --- + content-type: application/json; charset=utf-8 + + {"data":{"justAField":"a string"},"hasNext":true} + --- + content-type: application/json; charset=utf-8 + + {"hasNext":false,"incremental":[{"data":{"delayedFoo":{"bar":"hi"}},"path":[]}]} + ----- + " + `); const reports = await reportIngress.promiseOfReports; expect(reports.length).toBe(1); expect(Object.keys(reports[0].tracesPerQuery)).toHaveLength(1); @@ -1246,18 +1246,18 @@ export function defineIntegrationTestSuiteApolloServerTests( `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, ); expect(await response.text()).toMatchInlineSnapshot(` - " - --- - content-type: application/json; charset=utf-8 - - {"hasNext":true,"data":{"justAField":"a string"},"pending":[{"id":"0","path":[]}]} - --- - content-type: application/json; charset=utf-8 - - {"hasNext":false,"incremental":[{"id":"0","data":{"delayedFoo":{"bar":"hi"}}}],"completed":[{"id":"0"}]} - ----- - " - `); + " + --- + content-type: application/json; charset=utf-8 + + {"data":{"justAField":"a string"},"pending":[{"id":"0","path":[]}],"hasNext":true} + --- + content-type: application/json; charset=utf-8 + + {"hasNext":false,"incremental":[{"data":{"delayedFoo":{"bar":"hi"}},"id":"0"}],"completed":[{"id":"0"}]} + ----- + " + `); const reports = await reportIngress.promiseOfReports; expect(reports.length).toBe(1); expect(Object.keys(reports[0].tracesPerQuery)).toHaveLength(1); diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index c3f031ebc2d..662057dbf0d 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -958,19 +958,19 @@ export function defineIntegrationTestSuiteHttpServerTests( .send([{ query: '{ten}' }, { query: '{twenty}' }]); expect(res.status).toEqual(200); expect(res.body).toMatchInlineSnapshot(` - [ - { - "data": { - "ten": null, - }, - }, - { - "data": { - "twenty": null, - }, - }, - ] - `); + [ + { + "data": { + "ten": null, + }, + }, + { + "data": { + "twenty": null, + }, + }, + ] + `); expect(res.headers['cache-control']).toEqual('max-age=10, private'); } { @@ -2250,18 +2250,18 @@ export function defineIntegrationTestSuiteHttpServerTests( `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, ); expect(res.text).toMatchInlineSnapshot(` - " - --- - content-type: application/json; charset=utf-8 - - {"hasNext":true,"data":{"first":"it works"},"pending":[{"id":"0","path":[]}]} - --- - content-type: application/json; charset=utf-8 - - {"hasNext":false,"incremental":[{"id":"0","data":{"testString":"it works"}}],"completed":[{"id":"0"}]} - ----- - " - `); + " + --- + content-type: application/json; charset=utf-8 + + {"hasNext":true,"pending":[{"id":"0","path":[]}],"data":{"first":"it works"}} + --- + content-type: application/json; charset=utf-8 + + {"hasNext":false,"incremental":[{"id":"0","data":{"testString":"it works"}}],"completed":[{"id":"0"}]} + ----- + " + `); }); it('first payload sent while deferred field is blocking legacy incremental', async () => { @@ -2392,18 +2392,18 @@ export function defineIntegrationTestSuiteHttpServerTests( `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, ); expect(res.text).toMatchInlineSnapshot(` - " - --- - content-type: application/json; charset=utf-8 - - {"hasNext":true,"data":{"testString":"it works"},"pending":[{"id":"0","path":[]}]} - --- - content-type: application/json; charset=utf-8 - - {"hasNext":false,"incremental":[{"id":"0","data":{"barrierString":"we waited"}}],"completed":[{"id":"0"}]} - ----- - " - `); + " + --- + content-type: application/json; charset=utf-8 + + {"hasNext":true,"pending":[{"id":"0","path":[]}],"data":{"testString":"it works"}} + --- + content-type: application/json; charset=utf-8 + + {"hasNext":false,"incremental":[{"id":"0","data":{"barrierString":"we waited"}}],"completed":[{"id":"0"}]} + ----- + " + `); }); }); @@ -2483,11 +2483,11 @@ export function defineIntegrationTestSuiteHttpServerTests( ---\r content-type: application/json; charset=utf-8\r \r -{"hasNext":true,"data":{"first":"it works"}}\r +{"data":{"first":"it works"},"hasNext":true}\r ---\r content-type: application/json; charset=utf-8\r \r -{"hasNext":false,"incremental":[{"path":[],"data":{"testString":"it works"}}]}\r +{"hasNext":false,"incremental":[{"data":{"testString":"it works"},"path":[]}]}\r -----\r `); }); @@ -2535,19 +2535,19 @@ content-type: application/json; charset=utf-8\r ---\r content-type: application/json; charset=utf-8\r \r -{"hasNext":true,"data":{"testStrings":[]}}\r +{"data":{"testStrings":[]},"hasNext":true}\r ---\r content-type: application/json; charset=utf-8\r \r -{"hasNext":true,"incremental":[{"path":["testStrings",0],"items":["it works"]}]}\r +{"hasNext":true,"incremental":[{"items":["it works"],"path":["testStrings",0]}]}\r ---\r content-type: application/json; charset=utf-8\r \r -{"hasNext":true,"incremental":[{"path":["testStrings",1],"items":["it works again"]}]}\r +{"hasNext":true,"incremental":[{"items":["it works again"],"path":["testStrings",1]}]}\r ---\r content-type: application/json; charset=utf-8\r \r -{"hasNext":true,"incremental":[{"path":["testStrings",2],"items":["it works again again"]}]}\r +{"hasNext":true,"incremental":[{"items":["it works again again"],"path":["testStrings",2]}]}\r ---\r content-type: application/json; charset=utf-8\r \r @@ -2596,18 +2596,18 @@ content-type: application/json; charset=utf-8\r `"multipart/mixed; boundary="-"; deferSpec=20220824"`, ); expect(res.text).toMatchInlineSnapshot(` - " - --- - content-type: application/json; charset=utf-8 + " + --- + content-type: application/json; charset=utf-8 - {"hasNext":true,"data":{"testString":"it works"}} - --- - content-type: application/json; charset=utf-8 + {"data":{"testString":"it works"},"hasNext":true} + --- + content-type: application/json; charset=utf-8 - {"hasNext":false,"incremental":[{"path":[],"data":{"barrierString":"we waited"}}]} - ----- - " - `); + {"hasNext":false,"incremental":[{"data":{"barrierString":"we waited"},"path":[]}]} + ----- + " + `); }); }); @@ -2667,11 +2667,11 @@ content-type: application/json; charset=utf-8\r ---\r content-type: application/json; charset=utf-8\r \r -{"hasNext":true,"data":{"first":"it works"},"pending":[{"id":"0","path":[]}]}\r +{"data":{"first":"it works"},"pending":[{"id":"0","path":[]}],"hasNext":true}\r ---\r content-type: application/json; charset=utf-8\r \r -{"hasNext":false,"incremental":[{"id":"0","data":{"testString":"it works"}}],"completed":[{"id":"0"}]}\r +{"hasNext":false,"incremental":[{"data":{"testString":"it works"},"id":"0"}],"completed":[{"id":"0"}]}\r -----\r `); }); @@ -2723,7 +2723,7 @@ content-type: application/json; charset=utf-8\r ---\r content-type: application/json; charset=utf-8\r \r -{"hasNext":true,"data":{"testStrings":[]},"pending":[{"id":"0","path":["testStrings"]}]}\r +{"data":{"testStrings":[]},"pending":[{"id":"0","path":["testStrings"]}],"hasNext":true}\r ---\r content-type: application/json; charset=utf-8\r \r @@ -2784,18 +2784,18 @@ content-type: application/json; charset=utf-8\r `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, ); expect(res.text).toMatchInlineSnapshot(` - " - --- - content-type: application/json; charset=utf-8 - - {"hasNext":true,"data":{"testString":"it works"},"pending":[{"id":"0","path":[]}]} - --- - content-type: application/json; charset=utf-8 - - {"hasNext":false,"incremental":[{"id":"0","data":{"barrierString":"we waited"}}],"completed":[{"id":"0"}]} - ----- - " - `); + " + --- + content-type: application/json; charset=utf-8 + + {"data":{"testString":"it works"},"pending":[{"id":"0","path":[]}],"hasNext":true} + --- + content-type: application/json; charset=utf-8 + + {"hasNext":false,"incremental":[{"data":{"barrierString":"we waited"},"id":"0"}],"completed":[{"id":"0"}]} + ----- + " + `); }); }); }); diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index 3bff22deee8..769ae4e1225 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -1,13 +1,9 @@ import type { BaseContext, - GraphQLExperimentalFormattedCompletedResultAlpha9, - GraphQLExperimentalFormattedIncrementalResultAlpha2, - GraphQLExperimentalFormattedIncrementalResultAlpha9, GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2, GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha9, GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2, GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha9, - GraphQLExperimentalPendingResultAlpha9, GraphQLRequest, HTTPGraphQLHead, HTTPGraphQLRequest, @@ -343,12 +339,12 @@ async function* writeMultipartBody( // iterator is finished until we do async work. yield `\r\n---\r\ncontent-type: application/json; charset=utf-8\r\n\r\n${JSON.stringify( - orderInitialIncrementalExecutionResultFields(initialResult), + initialResult, )}\r\n---${initialResult.hasNext ? '' : '--'}\r\n`; for await (const result of subsequentResults) { yield `content-type: application/json; charset=utf-8\r\n\r\n${JSON.stringify( - orderSubsequentIncrementalExecutionResultFields(result), + result, )}\r\n---${result.hasNext ? '' : '--'}\r\n`; } } @@ -365,99 +361,6 @@ function orderExecutionResultFields( }; } -function orderInitialIncrementalExecutionResultFields( - result: - | GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2 - | GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha9, -): - | GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2 - | GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha9 { - if ('pending' in result) { - return { - hasNext: result.hasNext, - errors: result.errors, - data: result.data, - pending: orderPendingResultFields(result.pending), - extensions: result.extensions, - }; - } - - return { - hasNext: result.hasNext, - errors: result.errors, - data: result.data, - incremental: orderIncrementalResultFields(result.incremental), - extensions: result.extensions, - }; -} - -function orderPendingResultFields( - pending: readonly GraphQLExperimentalPendingResultAlpha9[] | undefined, -): GraphQLExperimentalPendingResultAlpha9[] | undefined { - return pending?.map((p) => ({ - id: p.id, - path: p.path, - label: p.label, - })); -} - -function orderSubsequentIncrementalExecutionResultFields( - result: - | GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2 - | GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha9, -): - | GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2 - | GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha9 { - if ('pending' in result || 'completed' in result) { - return { - hasNext: result.hasNext, - pending: orderPendingResultFields(result.pending), - incremental: orderIncrementalResultFields(result.incremental), - completed: orderCompletedResultFields(result.completed), - extensions: result.extensions, - }; - } - - return { - hasNext: result.hasNext, - incremental: orderIncrementalResultFields(result.incremental), - extensions: result.extensions, - }; -} - -function orderCompletedResultFields( - result: - | readonly GraphQLExperimentalFormattedCompletedResultAlpha9[] - | undefined, -): GraphQLExperimentalFormattedCompletedResultAlpha9[] | undefined { - return result?.map((c) => ({ - errors: c.errors, - id: c.id, - })); -} - -function orderIncrementalResultFields( - incremental?: - | readonly GraphQLExperimentalFormattedIncrementalResultAlpha2[] - | readonly GraphQLExperimentalFormattedIncrementalResultAlpha9[], -): - | undefined - | GraphQLExperimentalFormattedIncrementalResultAlpha2[] - | GraphQLExperimentalFormattedIncrementalResultAlpha9[] { - return incremental?.map((i: any) => { - return { - errors: i.errors, - path: i.path, - subPath: i.subPath, - label: i.label, - id: i.id, - data: i.data, - items: i.items, - extensions: i.extensions, - }; - }); -} - // The result of a curl does not appear well in the terminal, so we add an extra new line export function prettyJSONStringify(value: FormattedExecutionResult) { return JSON.stringify(value) + '\n'; diff --git a/smoke-test/smoke-test.cjs b/smoke-test/smoke-test.cjs index 54cbac99ea2..32a7710f00d 100644 --- a/smoke-test/smoke-test.cjs +++ b/smoke-test/smoke-test.cjs @@ -72,11 +72,11 @@ async function smokeTest() { '---\r\n' + 'content-type: application/json; charset=utf-8\r\n' + '\r\n' + - '{"hasNext":true,"data":{"h1":"world"}}\r\n' + + '{"data":{"h1":"world"},"hasNext":true}\r\n' + '---\r\n' + 'content-type: application/json; charset=utf-8\r\n' + '\r\n' + - '{"hasNext":false,"incremental":[{"path":[],"data":{"h2":"world"}}]}\r\n' + + '{"hasNext":false,"incremental":[{"data":{"h2":"world"},"path":[]}]}\r\n' + '-----\r\n', ); } @@ -104,11 +104,11 @@ async function smokeTest() { '---\r\n' + 'content-type: application/json; charset=utf-8\r\n' + '\r\n' + - '{"hasNext":true,"data":{"h1":"world"},"pending":[{"id":"0","path":[]}]}\r\n' + + '{"data":{"h1":"world"},"pending":[{"id":"0","path":[]}],"hasNext":true}\r\n' + '---\r\n' + 'content-type: application/json; charset=utf-8\r\n' + '\r\n' + - '{"hasNext":false,"incremental":[{"id":"0","data":{"h2":"world"}}],"completed":[{"id":"0"}]}\r\n' + + '{"hasNext":false,"incremental":[{"data":{"h2":"world"},"id":"0"}],"completed":[{"id":"0"}]}\r\n' + '-----\r\n', ); } diff --git a/smoke-test/smoke-test.mjs b/smoke-test/smoke-test.mjs index 52a46af8bda..ada2b4ebad9 100644 --- a/smoke-test/smoke-test.mjs +++ b/smoke-test/smoke-test.mjs @@ -68,11 +68,11 @@ if (process.env.INCREMENTAL_DELIVERY_TESTS_ENABLED) { '---\r\n' + 'content-type: application/json; charset=utf-8\r\n' + '\r\n' + - '{"hasNext":true,"data":{"h1":"world"}}\r\n' + + '{"data":{"h1":"world"},"hasNext":true}\r\n' + '---\r\n' + 'content-type: application/json; charset=utf-8\r\n' + '\r\n' + - '{"hasNext":false,"incremental":[{"path":[],"data":{"h2":"world"}}]}\r\n' + + '{"hasNext":false,"incremental":[{"data":{"h2":"world"},"path":[]}]}\r\n' + '-----\r\n', ); } @@ -100,11 +100,11 @@ if (process.env.INCREMENTAL_DELIVERY_TESTS_ENABLED) { '---\r\n' + 'content-type: application/json; charset=utf-8\r\n' + '\r\n' + - '{"hasNext":true,"data":{"h1":"world"},"pending":[{"id":"0","path":[]}]}\r\n' + + '{"data":{"h1":"world"},"pending":[{"id":"0","path":[]}],"hasNext":true}\r\n' + '---\r\n' + 'content-type: application/json; charset=utf-8\r\n' + '\r\n' + - '{"hasNext":false,"incremental":[{"id":"0","data":{"h2":"world"}}],"completed":[{"id":"0"}]}\r\n' + + '{"hasNext":false,"incremental":[{"data":{"h2":"world"},"id":"0"}],"completed":[{"id":"0"}]}\r\n' + '-----\r\n', ); } From ce4d8d12261da134af31b414872e407f185fef02 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 1 Oct 2025 15:05:52 -0600 Subject: [PATCH 83/93] Allow v17.0.0-alpha.9 --- package-lock.json | 2 +- packages/server/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index a7d4a707e50..0e450d9bd97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14941,7 +14941,7 @@ }, "peerDependencies": { "@yaacovcr/transform": "^0.0.8", - "graphql": "^16.11.0" + "graphql": "^16.11.0 || 17.0.0-alpha.9" }, "peerDependenciesMeta": { "@yaacovcr/transform": { diff --git a/packages/server/package.json b/packages/server/package.json index 4ad4df30ae9..4fc798441c7 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -147,7 +147,7 @@ "whatwg-mimetype": "^4.0.0" }, "peerDependencies": { - "graphql": "^16.11.0", + "graphql": "^16.11.0 || 17.0.0-alpha.9", "@yaacovcr/transform": "^0.0.8" }, "peerDependenciesMeta": { From c9c8c786f599f79593063e2a962144f1fd07ad3d Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 1 Oct 2025 15:35:11 -0600 Subject: [PATCH 84/93] Combine all changesets --- .changeset/fruity-ways-tell.md | 51 +++++++++++++++++++++++++++++-- .changeset/wild-candies-deny.md | 5 --- .changeset/yellow-worlds-start.md | 38 ----------------------- 3 files changed, 49 insertions(+), 45 deletions(-) delete mode 100644 .changeset/wild-candies-deny.md delete mode 100644 .changeset/yellow-worlds-start.md diff --git a/.changeset/fruity-ways-tell.md b/.changeset/fruity-ways-tell.md index 2b739763ad3..fe87675c400 100644 --- a/.changeset/fruity-ways-tell.md +++ b/.changeset/fruity-ways-tell.md @@ -2,6 +2,53 @@ '@apollo/server': minor --- -Add support for the modern modern incremental delivery protocol (`@defer` and `@stream`) that ships with `graphql@17.0.0-alpha.9`. To use the modern protocol, clients must send the `Accept` header with a value of `multipart/mixed; incrementalDeliverySpec=3283f8a`. +Apollo Server now supports the modern incremental delivery protocol (`@defer` and `@stream`) that ships with `graphql@17.0.0-alpha.9`. To use the modern protocol, clients must send the `Accept` header with a value of `multipart/mixed; incrementalDeliverySpec=3283f8a`. -Support for the legacy incremental delivery protocol is still possible by installing the [`@yaacovcr/transform`](https://github.com/yaacovCR/transform) package. Apollo Server will attempt to load this module when the client specifies an `Accept` header with a value of `multipart/mixed; deferSpec=20220824`. If this package is not installed, an error is returned by the server. +To upgrade to 5.1 will depend on what version of `graphql` you have installed and whether you already support the incremental delivery protocol. + +## I use `graphql@16` without incremental delivery + +Continue using `graphql` v16 with no additional changes. Incremental delivery won't be available. + +## I use `graphql@16` but would like to add support for incremental delivery + +Install `graphql@17.0.0-alpha.9` and follow the ["Incremental delivery" guide](https://www.apollographql.com/docs/apollo-server/workflow/requests#incremental-delivery-experimental) to add the `@defer` and `@stream` directives to your schema. Clients should send the `Accept` header with a value of `multipart/mixed; incrementalDeliverySpec=3283f8a` to get multipart responses. + +## I use `graphql@17.0.0-alpha.2` and use incremental delivery + +You must upgrade to `graphql@17.0.0-alpha.9` to continue using incremental delivery. If you'd like to continue providing support for the legacy incremental protocol, install the [`@yaacovcr/transform`](https://github.com/yaacovCR/transform) package. Apollo Server will attempt to load this module when the client specifies an `Accept` header with a value of `multipart/mixed; deferSpec=20220824`. If this package is not installed, an error is returned by the server. + +Because Apollo Server now supports multiple versions of the incremental delivery types, the existing incremental delivery types have been renamed with an `Alpha2` suffix. If you import these types in your code, you will need to add the `Alpha2` suffix. + +```diff +import type { +- GraphQLExperimentalFormattedInitialIncrementalExecutionResult, ++ GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2, + +- GraphQLExperimentalFormattedSubsequentIncrementalExecutionResult, ++ GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2, + +- GraphQLExperimentalFormattedIncrementalResult, ++ GraphQLExperimentalFormattedIncrementalResultAlpha2, + +- GraphQLExperimentalFormattedIncrementalDeferResult, ++ GraphQLExperimentalFormattedIncrementalDeferResultAlpha2, + +- GraphQLExperimentalFormattedIncrementalStreamResult, ++ GraphQLExperimentalFormattedIncrementalStreamResultAlpha2, +} from '@apollo/server'; +``` + +Incremental delivery types for the more modern `graphql@17.0.0-alpha.9` version are now available using the `Alpha9` suffix: + +```ts +import type { + GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha9, + GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha9, + GraphQLExperimentalFormattedIncrementalResultAlpha9, + GraphQLExperimentalFormattedIncrementalDeferResultAlpha9, + GraphQLExperimentalFormattedIncrementalStreamResultAlpha9, + GraphQLExperimentalFormattedCompletedResultAlpha9, + GraphQLExperimentalPendingResultAlpha9, +} from '@apollo/server'; +``` diff --git a/.changeset/wild-candies-deny.md b/.changeset/wild-candies-deny.md deleted file mode 100644 index 150dcacba29..00000000000 --- a/.changeset/wild-candies-deny.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@apollo/server': minor ---- - -Apollo Server now requires `graphql@17.0.0-alpha.9` in order to use the incremental delivery protocol. Please upgrade from `graphql@17.0.0-alpha.2`. diff --git a/.changeset/yellow-worlds-start.md b/.changeset/yellow-worlds-start.md deleted file mode 100644 index 19f4596c9da..00000000000 --- a/.changeset/yellow-worlds-start.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@apollo/server': minor ---- - -Add `graphql@17.0-0-alpha.9` incremental delivery types and rename the existing incremental delivery types by adding an `Alpha2` suffix. If you import these types in your code, you will need to add the `Alpha2` suffix. - -```diff -import type { -- GraphQLExperimentalFormattedInitialIncrementalExecutionResult, -+ GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha2, - -- GraphQLExperimentalFormattedSubsequentIncrementalExecutionResult, -+ GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha2, - -- GraphQLExperimentalFormattedIncrementalResult, -+ GraphQLExperimentalFormattedIncrementalResultAlpha2, - -- GraphQLExperimentalFormattedIncrementalDeferResult, -+ GraphQLExperimentalFormattedIncrementalDeferResultAlpha2, - -- GraphQLExperimentalFormattedIncrementalStreamResult, -+ GraphQLExperimentalFormattedIncrementalStreamResultAlpha2, -} from '@apollo/server'; -``` - -Incremental delivery types for the `graphql@17.0.0-alpha.9` version are now available using the `Alpha9` suffix: - -```ts -import type { - GraphQLExperimentalFormattedInitialIncrementalExecutionResultAlpha9, - GraphQLExperimentalFormattedSubsequentIncrementalExecutionResultAlpha9, - GraphQLExperimentalFormattedIncrementalResultAlpha9, - GraphQLExperimentalFormattedIncrementalDeferResultAlpha9, - GraphQLExperimentalFormattedIncrementalStreamResultAlpha9, - GraphQLExperimentalFormattedCompletedResultAlpha9, - GraphQLExperimentalPendingResultAlpha9, -} from '@apollo/server'; -``` From 28e9d9c7e8cd5e3c16a16cf287e32b76ad847664 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 1 Oct 2025 16:41:56 -0600 Subject: [PATCH 85/93] Fix typo Co-authored-by: David Glasser --- docs/source/workflow/requests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/workflow/requests.md b/docs/source/workflow/requests.md index 57378256401..e194f319e95 100644 --- a/docs/source/workflow/requests.md +++ b/docs/source/workflow/requests.md @@ -127,7 +127,7 @@ You cannot combine [batching](#batching) with incremental delivery in the same r -Apollo Server 5.1 changed the required pre-release `graphql` version from `17.0.0-alpha-2` to `17.0.0-alpha.9`. If you are using 5.0 or below, use `graphql` version `17.0.0-alpha.2` instead. +Apollo Server 5.1 changed the required pre-release `graphql` version from `17.0.0-alpha.2` to `17.0.0-alpha.9`. If you are using 5.0 or below, use `graphql` version `17.0.0-alpha.2` instead. From b32394a9293a881d2440815b018262254bee5dd2 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 1 Oct 2025 16:47:25 -0600 Subject: [PATCH 86/93] Grammar --- .changeset/fruity-ways-tell.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fruity-ways-tell.md b/.changeset/fruity-ways-tell.md index fe87675c400..93f211835ab 100644 --- a/.changeset/fruity-ways-tell.md +++ b/.changeset/fruity-ways-tell.md @@ -4,7 +4,7 @@ Apollo Server now supports the modern incremental delivery protocol (`@defer` and `@stream`) that ships with `graphql@17.0.0-alpha.9`. To use the modern protocol, clients must send the `Accept` header with a value of `multipart/mixed; incrementalDeliverySpec=3283f8a`. -To upgrade to 5.1 will depend on what version of `graphql` you have installed and whether you already support the incremental delivery protocol. +Upgrading to 5.1 will depend on what version of `graphql` you have installed and whether you already support the incremental delivery protocol. ## I use `graphql@16` without incremental delivery From 3c0a88e2c81460f1a34369e12cb534619b859e5d Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 1 Oct 2025 16:47:51 -0600 Subject: [PATCH 87/93] Remove use of "modern" in changeset --- .changeset/fruity-ways-tell.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/fruity-ways-tell.md b/.changeset/fruity-ways-tell.md index 93f211835ab..c1d082ddbb6 100644 --- a/.changeset/fruity-ways-tell.md +++ b/.changeset/fruity-ways-tell.md @@ -2,7 +2,7 @@ '@apollo/server': minor --- -Apollo Server now supports the modern incremental delivery protocol (`@defer` and `@stream`) that ships with `graphql@17.0.0-alpha.9`. To use the modern protocol, clients must send the `Accept` header with a value of `multipart/mixed; incrementalDeliverySpec=3283f8a`. +Apollo Server now supports the incremental delivery protocol (`@defer` and `@stream`) that ships with `graphql@17.0.0-alpha.9`. To use the current protocol, clients must send the `Accept` header with a value of `multipart/mixed; incrementalDeliverySpec=3283f8a`. Upgrading to 5.1 will depend on what version of `graphql` you have installed and whether you already support the incremental delivery protocol. @@ -39,7 +39,7 @@ import type { } from '@apollo/server'; ``` -Incremental delivery types for the more modern `graphql@17.0.0-alpha.9` version are now available using the `Alpha9` suffix: +Incremental delivery types for the `graphql@17.0.0-alpha.9` version are now available using the `Alpha9` suffix: ```ts import type { From bd5cd6330157dfdf1c8d7b58d55a2667f1f2b855 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 1 Oct 2025 16:49:03 -0600 Subject: [PATCH 88/93] Revert changes to peer deps --- package-lock.json | 8 +------- packages/server/package.json | 8 +------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0e450d9bd97..5b05aad937b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14940,13 +14940,7 @@ "node": ">=20" }, "peerDependencies": { - "@yaacovcr/transform": "^0.0.8", - "graphql": "^16.11.0 || 17.0.0-alpha.9" - }, - "peerDependenciesMeta": { - "@yaacovcr/transform": { - "optional": true - } + "graphql": "^16.11.0" } }, "packages/server/node_modules/@apollo/utils.fetcher": { diff --git a/packages/server/package.json b/packages/server/package.json index 4fc798441c7..b2897bf675d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -147,12 +147,6 @@ "whatwg-mimetype": "^4.0.0" }, "peerDependencies": { - "graphql": "^16.11.0 || 17.0.0-alpha.9", - "@yaacovcr/transform": "^0.0.8" - }, - "peerDependenciesMeta": { - "@yaacovcr/transform": { - "optional": true - } + "graphql": "^16.11.0" } } From 96af5520b8c4c8ff6d1e179b0449a076fb94f23b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 1 Oct 2025 17:09:28 -0600 Subject: [PATCH 89/93] Don't export unneeded types --- packages/server/src/incrementalDeliveryPolyfill.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/incrementalDeliveryPolyfill.ts b/packages/server/src/incrementalDeliveryPolyfill.ts index b3704ba901e..ec0bf04246d 100644 --- a/packages/server/src/incrementalDeliveryPolyfill.ts +++ b/packages/server/src/incrementalDeliveryPolyfill.ts @@ -146,13 +146,13 @@ type GraphQLExperimentalIncrementalResultAlpha9< | GraphQLExperimentalIncrementalDeferResultAlpha9 | GraphQLExperimentalIncrementalStreamResultAlpha9; -export interface GraphQLExperimentalPendingResultAlpha9 { +interface GraphQLExperimentalPendingResultAlpha9 { id: string; path: ReadonlyArray; label?: string; } -export interface GraphQLExperimentalCompletedResultAlpha9 { +interface GraphQLExperimentalCompletedResultAlpha9 { id: string; errors?: ReadonlyArray; } From 5d15554be208686b6b1f5643db21b694cec5c337 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 1 Oct 2025 17:49:30 -0600 Subject: [PATCH 90/93] Update error messages --- packages/integration-testsuite/src/httpServerTests.ts | 4 ++-- packages/server/src/incrementalDeliveryPolyfill.ts | 2 +- packages/server/src/runHttpQuery.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index 662057dbf0d..aa70f54f1d0 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -2454,7 +2454,7 @@ export function defineIntegrationTestSuiteHttpServerTests( "extensions": { "code": "BAD_REQUEST", }, - "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream), but the client does not accept multipart/mixed HTTP responses. To enable incremental delivery support, add the HTTP header 'Accept: multipart/mixed; incrementalDeliverySpec=3283f8a' if your client supports the modern incremental format or 'Accept: multipart/mixed; deferSpec=20220824' if your client supports the legacy incremental format", + "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream), but the client does not accept multipart/mixed HTTP responses. To enable incremental delivery support, add the HTTP header 'Accept: multipart/mixed; incrementalDeliverySpec=3283f8a' if your client supports the current incremental format or 'Accept: multipart/mixed; deferSpec=20220824' if your client supports the legacy incremental format", }, ], } @@ -2634,7 +2634,7 @@ content-type: application/json; charset=utf-8\r "extensions": { "code": "BAD_REQUEST", }, - "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream), but the client does not accept multipart/mixed HTTP responses. To enable incremental delivery support, add the HTTP header 'Accept: multipart/mixed; incrementalDeliverySpec=3283f8a' if your client supports the modern incremental format or 'Accept: multipart/mixed; deferSpec=20220824' if your client supports the legacy incremental format", + "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream), but the client does not accept multipart/mixed HTTP responses. To enable incremental delivery support, add the HTTP header 'Accept: multipart/mixed; incrementalDeliverySpec=3283f8a' if your client supports the current incremental format or 'Accept: multipart/mixed; deferSpec=20220824' if your client supports the legacy incremental format", }, ], } diff --git a/packages/server/src/incrementalDeliveryPolyfill.ts b/packages/server/src/incrementalDeliveryPolyfill.ts index ec0bf04246d..22a1e2e8467 100644 --- a/packages/server/src/incrementalDeliveryPolyfill.ts +++ b/packages/server/src/incrementalDeliveryPolyfill.ts @@ -256,7 +256,7 @@ export async function executeIncrementally({ '(@defer or @stream) with the legacy incremental format, but the server ' + 'does not support the legacy incremental delivery format. Add the HTTP ' + `header: 'Accept: ${MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9}' ` + - 'to use the modern incremental delivery format', + 'to use the current incremental delivery format', // Use 406 Not Accepted { extensions: { http: { status: 406 } } }, ); diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts index 769ae4e1225..bebdc248297 100644 --- a/packages/server/src/runHttpQuery.ts +++ b/packages/server/src/runHttpQuery.ts @@ -297,7 +297,7 @@ export async function runHttpQuery({ '(@defer or @stream), but the client does not accept multipart/mixed ' + 'HTTP responses. To enable incremental delivery support, add the HTTP ' + `header 'Accept: ${MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9}' ` + - 'if your client supports the modern incremental format or ' + + 'if your client supports the current incremental format or ' + `'Accept: ${MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2}' if your ` + 'client supports the legacy incremental format', // Use 406 Not Accepted From 79fb340002cb1e18d86701d30b86df95b57ed706 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 14 Oct 2025 11:49:16 -0600 Subject: [PATCH 91/93] Update incrementalDeliverySpec header value --- .changeset/fruity-ways-tell.md | 4 +-- docs/source/workflow/requests.md | 2 +- .../src/apolloServerTests.ts | 5 +-- .../src/httpServerTests.ts | 36 ++++++++++--------- packages/server/src/ApolloServer.ts | 5 +-- smoke-test/smoke-test.cjs | 4 +-- smoke-test/smoke-test.mjs | 4 +-- 7 files changed, 33 insertions(+), 27 deletions(-) diff --git a/.changeset/fruity-ways-tell.md b/.changeset/fruity-ways-tell.md index c1d082ddbb6..70a6d0eaf4a 100644 --- a/.changeset/fruity-ways-tell.md +++ b/.changeset/fruity-ways-tell.md @@ -2,7 +2,7 @@ '@apollo/server': minor --- -Apollo Server now supports the incremental delivery protocol (`@defer` and `@stream`) that ships with `graphql@17.0.0-alpha.9`. To use the current protocol, clients must send the `Accept` header with a value of `multipart/mixed; incrementalDeliverySpec=3283f8a`. +Apollo Server now supports the incremental delivery protocol (`@defer` and `@stream`) that ships with `graphql@17.0.0-alpha.9`. To use the current protocol, clients must send the `Accept` header with a value of `multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1`. Upgrading to 5.1 will depend on what version of `graphql` you have installed and whether you already support the incremental delivery protocol. @@ -12,7 +12,7 @@ Continue using `graphql` v16 with no additional changes. Incremental delivery wo ## I use `graphql@16` but would like to add support for incremental delivery -Install `graphql@17.0.0-alpha.9` and follow the ["Incremental delivery" guide](https://www.apollographql.com/docs/apollo-server/workflow/requests#incremental-delivery-experimental) to add the `@defer` and `@stream` directives to your schema. Clients should send the `Accept` header with a value of `multipart/mixed; incrementalDeliverySpec=3283f8a` to get multipart responses. +Install `graphql@17.0.0-alpha.9` and follow the ["Incremental delivery" guide](https://www.apollographql.com/docs/apollo-server/workflow/requests#incremental-delivery-experimental) to add the `@defer` and `@stream` directives to your schema. Clients should send the `Accept` header with a value of `multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1` to get multipart responses. ## I use `graphql@17.0.0-alpha.2` and use incremental delivery diff --git a/docs/source/workflow/requests.md b/docs/source/workflow/requests.md index e194f319e95..be8360ee360 100644 --- a/docs/source/workflow/requests.md +++ b/docs/source/workflow/requests.md @@ -119,7 +119,7 @@ const schema = new GraphQLSchema({ }); ``` -Clients sending operations with incremental delivery directives need to explicitly indicate that they are expecting to receive `multipart/mixed` responses in an `accept` header. Moreover, because incremental delivery has not yet been finalized in the GraphQL spec and may change before the final version, they need to specify that they expect the particular response format that Apollo Server produces today via a `incrementalDeliverySpec` parameter. Specifically, clients prepared to accept incremental delivery responses should send an `accept` header like `multipart/mixed; incrementalDeliverySpec=3283f8a`. Note that this header implies that *only* multipart responses should be accepted; typically, clients will send an accept header like `multipart/mixed; incrementalDeliverySpec=3283f8a, application/json` indicating that either multipart or single-part responses are acceptable. +Clients sending operations with incremental delivery directives need to explicitly indicate that they are expecting to receive `multipart/mixed` responses in an `accept` header. Moreover, because incremental delivery has not yet been finalized in the GraphQL spec and may change before the final version, they need to specify that they expect the particular response format that Apollo Server produces today via a `incrementalDeliverySpec` parameter. Specifically, clients prepared to accept incremental delivery responses should send an `accept` header like `multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1`. Note that this header implies that *only* multipart responses should be accepted; typically, clients will send an accept header like `multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1, application/json` indicating that either multipart or single-part responses are acceptable. > Apollo Server *only* supports the specific pre-release `graphql@17.0.0-alpha.9`. Apollo Server 5 checks the version of `graphql` you have installed and will not attempt to support incremental delivery unless it is precisely `17.0.0-alpha.9`. diff --git a/packages/integration-testsuite/src/apolloServerTests.ts b/packages/integration-testsuite/src/apolloServerTests.ts index d699fafa029..373e0284995 100644 --- a/packages/integration-testsuite/src/apolloServerTests.ts +++ b/packages/integration-testsuite/src/apolloServerTests.ts @@ -1233,7 +1233,8 @@ export function defineIntegrationTestSuiteApolloServerTests( method: 'POST', headers: { 'content-type': 'application/json', - accept: 'multipart/mixed; incrementalDeliverySpec=3283f8a', + accept: + 'multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1', }, body: JSON.stringify({ query: '{ justAField ...@defer { delayedFoo { bar} } }', @@ -1243,7 +1244,7 @@ export function defineIntegrationTestSuiteApolloServerTests( expect( response.headers.get('content-type'), ).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, + `"multipart/mixed; boundary="-"; incrementalDeliverySpec=graphql/incremental/v0.1"`, ); expect(await response.text()).toMatchInlineSnapshot(` " diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index aa70f54f1d0..4c9cfa9c0c1 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -2238,7 +2238,7 @@ export function defineIntegrationTestSuiteHttpServerTests( .post('/') .set( 'accept', - 'multipart/mixed; incrementalDeliverySpec=3283f8a, application/json', + 'multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1, application/json', ) // disables supertest's use of formidable for multipart .parse(superagent.parse.text) @@ -2247,7 +2247,7 @@ export function defineIntegrationTestSuiteHttpServerTests( }); expect(res.status).toEqual(200); expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, + `"multipart/mixed; boundary="-"; incrementalDeliverySpec=graphql/incremental/v0.1"`, ); expect(res.text).toMatchInlineSnapshot(` " @@ -2361,7 +2361,7 @@ export function defineIntegrationTestSuiteHttpServerTests( .post('/') .set( 'accept', - `multipart/mixed; incrementalDeliverySpec=3283f8a, application/json`, + `multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1, application/json`, ) .parse((res, fn) => { res.text = ''; @@ -2389,7 +2389,7 @@ export function defineIntegrationTestSuiteHttpServerTests( const res = await resPromise; expect(res.status).toEqual(200); expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, + `"multipart/mixed; boundary="-"; incrementalDeliverySpec=graphql/incremental/v0.1"`, ); expect(res.text).toMatchInlineSnapshot(` " @@ -2454,7 +2454,7 @@ export function defineIntegrationTestSuiteHttpServerTests( "extensions": { "code": "BAD_REQUEST", }, - "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream), but the client does not accept multipart/mixed HTTP responses. To enable incremental delivery support, add the HTTP header 'Accept: multipart/mixed; incrementalDeliverySpec=3283f8a' if your client supports the current incremental format or 'Accept: multipart/mixed; deferSpec=20220824' if your client supports the legacy incremental format", + "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream), but the client does not accept multipart/mixed HTTP responses. To enable incremental delivery support, add the HTTP header 'Accept: multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1' if your client supports the current incremental format or 'Accept: multipart/mixed; deferSpec=20220824' if your client supports the legacy incremental format", }, ], } @@ -2634,7 +2634,7 @@ content-type: application/json; charset=utf-8\r "extensions": { "code": "BAD_REQUEST", }, - "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream), but the client does not accept multipart/mixed HTTP responses. To enable incremental delivery support, add the HTTP header 'Accept: multipart/mixed; incrementalDeliverySpec=3283f8a' if your client supports the current incremental format or 'Accept: multipart/mixed; deferSpec=20220824' if your client supports the legacy incremental format", + "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream), but the client does not accept multipart/mixed HTTP responses. To enable incremental delivery support, add the HTTP header 'Accept: multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1' if your client supports the current incremental format or 'Accept: multipart/mixed; deferSpec=20220824' if your client supports the legacy incremental format", }, ], } @@ -2642,12 +2642,14 @@ content-type: application/json; charset=utf-8\r }); it.each([ - ['multipart/mixed; incrementalDeliverySpec=3283f8a'], [ - 'multipart/mixed; incrementalDeliverySpec=3283f8a, application/json', + 'multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1', ], [ - 'application/json, multipart/mixed; incrementalDeliverySpec=3283f8a', + 'multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1, application/json', + ], + [ + 'application/json, multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1', ], ])('basic @defer working with accept: %s', async (accept) => { const app = await createApp({ typeDefs, resolvers }); @@ -2661,7 +2663,7 @@ content-type: application/json; charset=utf-8\r }); expect(res.status).toEqual(200); expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, + `"multipart/mixed; boundary="-"; incrementalDeliverySpec=graphql/incremental/v0.1"`, ); expect(res.text).toEqual(`\r ---\r @@ -2677,12 +2679,14 @@ content-type: application/json; charset=utf-8\r }); it.each([ - ['multipart/mixed; incrementalDeliverySpec=3283f8a'], [ - 'multipart/mixed; incrementalDeliverySpec=3283f8a, application/json', + 'multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1', + ], + [ + 'multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1, application/json', ], [ - 'application/json, multipart/mixed; incrementalDeliverySpec=3283f8a', + 'application/json, multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1', ], ])('basic @stream working with accept: %s', async (accept) => { const app = await createApp({ @@ -2717,7 +2721,7 @@ content-type: application/json; charset=utf-8\r }); expect(res.status).toEqual(200); expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, + `"multipart/mixed; boundary="-"; incrementalDeliverySpec=graphql/incremental/v0.1"`, ); expect(res.text).toEqual(`\r ---\r @@ -2751,7 +2755,7 @@ content-type: application/json; charset=utf-8\r .post('/') .set( 'accept', - 'multipart/mixed; incrementalDeliverySpec=3283f8a, application/json', + 'multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1, application/json', ) .parse((res, fn) => { res.text = ''; @@ -2781,7 +2785,7 @@ content-type: application/json; charset=utf-8\r const res = await resPromise; expect(res.status).toEqual(200); expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a"`, + `"multipart/mixed; boundary="-"; incrementalDeliverySpec=graphql/incremental/v0.1"`, ); expect(res.text).toMatchInlineSnapshot(` " diff --git a/packages/server/src/ApolloServer.ts b/packages/server/src/ApolloServer.ts index 2e6380a783c..6a0afcf8bb4 100644 --- a/packages/server/src/ApolloServer.ts +++ b/packages/server/src/ApolloServer.ts @@ -1408,9 +1408,10 @@ export const MEDIA_TYPES = { // delivery is part of the official GraphQL spec. MULTIPART_MIXED_NO_DEFER_SPEC: 'multipart/mixed', MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2: 'multipart/mixed; deferSpec=20220824', - // spec version represents the commit hash of 17.0.0-alpha.9 + // This references the spec stored at + // https://github.com/apollographql/specs/pull/67 MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9: - 'multipart/mixed; incrementalDeliverySpec=3283f8a', + 'multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1', TEXT_HTML: 'text/html', }; diff --git a/smoke-test/smoke-test.cjs b/smoke-test/smoke-test.cjs index 32a7710f00d..b6af943206c 100644 --- a/smoke-test/smoke-test.cjs +++ b/smoke-test/smoke-test.cjs @@ -86,14 +86,14 @@ async function smokeTest() { method: 'POST', headers: { 'content-type': 'application/json', - accept: `multipart/mixed; incrementalDeliverySpec=3283f8a, application/json`, + accept: `multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1, application/json`, }, body: JSON.stringify({ query: '{h1: hello ...@defer{ h2: hello }}' }), }); assert.strictEqual( response.headers.get('content-type'), - `multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a`, + `multipart/mixed; boundary="-"; incrementalDeliverySpec=graphql/incremental/v0.1`, ); const body = await response.text(); diff --git a/smoke-test/smoke-test.mjs b/smoke-test/smoke-test.mjs index ada2b4ebad9..cb28ff0818c 100644 --- a/smoke-test/smoke-test.mjs +++ b/smoke-test/smoke-test.mjs @@ -82,14 +82,14 @@ if (process.env.INCREMENTAL_DELIVERY_TESTS_ENABLED) { method: 'POST', headers: { 'content-type': 'application/json', - accept: `multipart/mixed; incrementalDeliverySpec=3283f8a, application/json`, + accept: `multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1, application/json`, }, body: JSON.stringify({ query: '{h1: hello ...@defer{ h2: hello }}' }), }); assert.strictEqual( response.headers.get('content-type'), - `multipart/mixed; boundary="-"; incrementalDeliverySpec=3283f8a`, + `multipart/mixed; boundary="-"; incrementalDeliverySpec=graphql/incremental/v0.1`, ); const body = await response.text(); From 77b57de7471aebf209c3093fc869e5d5ef61012f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 14 Oct 2025 12:05:42 -0600 Subject: [PATCH 92/93] Fix typo --- docs/source/workflow/requests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/workflow/requests.md b/docs/source/workflow/requests.md index be8360ee360..22d9154fc29 100644 --- a/docs/source/workflow/requests.md +++ b/docs/source/workflow/requests.md @@ -119,7 +119,7 @@ const schema = new GraphQLSchema({ }); ``` -Clients sending operations with incremental delivery directives need to explicitly indicate that they are expecting to receive `multipart/mixed` responses in an `accept` header. Moreover, because incremental delivery has not yet been finalized in the GraphQL spec and may change before the final version, they need to specify that they expect the particular response format that Apollo Server produces today via a `incrementalDeliverySpec` parameter. Specifically, clients prepared to accept incremental delivery responses should send an `accept` header like `multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1`. Note that this header implies that *only* multipart responses should be accepted; typically, clients will send an accept header like `multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1, application/json` indicating that either multipart or single-part responses are acceptable. +Clients sending operations with incremental delivery directives need to explicitly indicate that they are expecting to receive `multipart/mixed` responses in an `accept` header. Moreover, because incremental delivery has not yet been finalized in the GraphQL spec and may change before the final version, they need to specify that they expect the particular response format that Apollo Server produces today via an `incrementalDeliverySpec` parameter. Specifically, clients prepared to accept incremental delivery responses should send an `accept` header like `multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1`. Note that this header implies that *only* multipart responses should be accepted; typically, clients will send an accept header like `multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1, application/json` indicating that either multipart or single-part responses are acceptable. > Apollo Server *only* supports the specific pre-release `graphql@17.0.0-alpha.9`. Apollo Server 5 checks the version of `graphql` you have installed and will not attempt to support incremental delivery unless it is precisely `17.0.0-alpha.9`. From 8b4bea518655ef3025729b78987e493bdc87296f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 20 Oct 2025 18:42:01 -0600 Subject: [PATCH 93/93] Update header value to final version --- .changeset/fruity-ways-tell.md | 4 +- docs/source/workflow/requests.md | 2 +- .../src/apolloServerTests.ts | 5 +-- .../src/httpServerTests.ts | 44 +++++++------------ packages/server/src/ApolloServer.ts | 5 +-- smoke-test/smoke-test.cjs | 4 +- smoke-test/smoke-test.mjs | 4 +- 7 files changed, 27 insertions(+), 41 deletions(-) diff --git a/.changeset/fruity-ways-tell.md b/.changeset/fruity-ways-tell.md index 70a6d0eaf4a..aa36805d139 100644 --- a/.changeset/fruity-ways-tell.md +++ b/.changeset/fruity-ways-tell.md @@ -2,7 +2,7 @@ '@apollo/server': minor --- -Apollo Server now supports the incremental delivery protocol (`@defer` and `@stream`) that ships with `graphql@17.0.0-alpha.9`. To use the current protocol, clients must send the `Accept` header with a value of `multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1`. +Apollo Server now supports the incremental delivery protocol (`@defer` and `@stream`) that ships with `graphql@17.0.0-alpha.9`. To use the current protocol, clients must send the `Accept` header with a value of `multipart/mixed; incrementalSpec=v0.2`. Upgrading to 5.1 will depend on what version of `graphql` you have installed and whether you already support the incremental delivery protocol. @@ -12,7 +12,7 @@ Continue using `graphql` v16 with no additional changes. Incremental delivery wo ## I use `graphql@16` but would like to add support for incremental delivery -Install `graphql@17.0.0-alpha.9` and follow the ["Incremental delivery" guide](https://www.apollographql.com/docs/apollo-server/workflow/requests#incremental-delivery-experimental) to add the `@defer` and `@stream` directives to your schema. Clients should send the `Accept` header with a value of `multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1` to get multipart responses. +Install `graphql@17.0.0-alpha.9` and follow the ["Incremental delivery" guide](https://www.apollographql.com/docs/apollo-server/workflow/requests#incremental-delivery-experimental) to add the `@defer` and `@stream` directives to your schema. Clients should send the `Accept` header with a value of `multipart/mixed; incrementalSpec=v0.2` to get multipart responses. ## I use `graphql@17.0.0-alpha.2` and use incremental delivery diff --git a/docs/source/workflow/requests.md b/docs/source/workflow/requests.md index 22d9154fc29..fa7e8acbe35 100644 --- a/docs/source/workflow/requests.md +++ b/docs/source/workflow/requests.md @@ -119,7 +119,7 @@ const schema = new GraphQLSchema({ }); ``` -Clients sending operations with incremental delivery directives need to explicitly indicate that they are expecting to receive `multipart/mixed` responses in an `accept` header. Moreover, because incremental delivery has not yet been finalized in the GraphQL spec and may change before the final version, they need to specify that they expect the particular response format that Apollo Server produces today via an `incrementalDeliverySpec` parameter. Specifically, clients prepared to accept incremental delivery responses should send an `accept` header like `multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1`. Note that this header implies that *only* multipart responses should be accepted; typically, clients will send an accept header like `multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1, application/json` indicating that either multipart or single-part responses are acceptable. +Clients sending operations with incremental delivery directives need to explicitly indicate that they are expecting to receive `multipart/mixed` responses in an `accept` header. Moreover, because incremental delivery has not yet been finalized in the GraphQL spec and may change before the final version, they need to specify that they expect the particular response format that Apollo Server produces today via a `incrementalSpec` parameter. Specifically, clients prepared to accept incremental delivery responses should send an `accept` header like `multipart/mixed; incrementalSpec=v0.2`. Note that this header implies that *only* multipart responses should be accepted; typically, clients will send an accept header like `multipart/mixed; incrementalSpec=v0.2, application/json` indicating that either multipart or single-part responses are acceptable. > Apollo Server *only* supports the specific pre-release `graphql@17.0.0-alpha.9`. Apollo Server 5 checks the version of `graphql` you have installed and will not attempt to support incremental delivery unless it is precisely `17.0.0-alpha.9`. diff --git a/packages/integration-testsuite/src/apolloServerTests.ts b/packages/integration-testsuite/src/apolloServerTests.ts index 373e0284995..eebb8c63946 100644 --- a/packages/integration-testsuite/src/apolloServerTests.ts +++ b/packages/integration-testsuite/src/apolloServerTests.ts @@ -1233,8 +1233,7 @@ export function defineIntegrationTestSuiteApolloServerTests( method: 'POST', headers: { 'content-type': 'application/json', - accept: - 'multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1', + accept: 'multipart/mixed; incrementalSpec=v0.2', }, body: JSON.stringify({ query: '{ justAField ...@defer { delayedFoo { bar} } }', @@ -1244,7 +1243,7 @@ export function defineIntegrationTestSuiteApolloServerTests( expect( response.headers.get('content-type'), ).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; incrementalDeliverySpec=graphql/incremental/v0.1"`, + `"multipart/mixed; boundary="-"; incrementalSpec=v0.2"`, ); expect(await response.text()).toMatchInlineSnapshot(` " diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index 4c9cfa9c0c1..cb482f8d02e 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -2238,7 +2238,7 @@ export function defineIntegrationTestSuiteHttpServerTests( .post('/') .set( 'accept', - 'multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1, application/json', + 'multipart/mixed; incrementalSpec=v0.2, application/json', ) // disables supertest's use of formidable for multipart .parse(superagent.parse.text) @@ -2247,7 +2247,7 @@ export function defineIntegrationTestSuiteHttpServerTests( }); expect(res.status).toEqual(200); expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; incrementalDeliverySpec=graphql/incremental/v0.1"`, + `"multipart/mixed; boundary="-"; incrementalSpec=v0.2"`, ); expect(res.text).toMatchInlineSnapshot(` " @@ -2361,7 +2361,7 @@ export function defineIntegrationTestSuiteHttpServerTests( .post('/') .set( 'accept', - `multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1, application/json`, + `multipart/mixed; incrementalSpec=v0.2, application/json`, ) .parse((res, fn) => { res.text = ''; @@ -2389,7 +2389,7 @@ export function defineIntegrationTestSuiteHttpServerTests( const res = await resPromise; expect(res.status).toEqual(200); expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; incrementalDeliverySpec=graphql/incremental/v0.1"`, + `"multipart/mixed; boundary="-"; incrementalSpec=v0.2"`, ); expect(res.text).toMatchInlineSnapshot(` " @@ -2454,7 +2454,7 @@ export function defineIntegrationTestSuiteHttpServerTests( "extensions": { "code": "BAD_REQUEST", }, - "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream), but the client does not accept multipart/mixed HTTP responses. To enable incremental delivery support, add the HTTP header 'Accept: multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1' if your client supports the current incremental format or 'Accept: multipart/mixed; deferSpec=20220824' if your client supports the legacy incremental format", + "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream), but the client does not accept multipart/mixed HTTP responses. To enable incremental delivery support, add the HTTP header 'Accept: multipart/mixed; incrementalSpec=v0.2' if your client supports the current incremental format or 'Accept: multipart/mixed; deferSpec=20220824' if your client supports the legacy incremental format", }, ], } @@ -2634,7 +2634,7 @@ content-type: application/json; charset=utf-8\r "extensions": { "code": "BAD_REQUEST", }, - "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream), but the client does not accept multipart/mixed HTTP responses. To enable incremental delivery support, add the HTTP header 'Accept: multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1' if your client supports the current incremental format or 'Accept: multipart/mixed; deferSpec=20220824' if your client supports the legacy incremental format", + "message": "Apollo server received an operation that uses incremental delivery (@defer or @stream), but the client does not accept multipart/mixed HTTP responses. To enable incremental delivery support, add the HTTP header 'Accept: multipart/mixed; incrementalSpec=v0.2' if your client supports the current incremental format or 'Accept: multipart/mixed; deferSpec=20220824' if your client supports the legacy incremental format", }, ], } @@ -2642,15 +2642,9 @@ content-type: application/json; charset=utf-8\r }); it.each([ - [ - 'multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1', - ], - [ - 'multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1, application/json', - ], - [ - 'application/json, multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1', - ], + ['multipart/mixed; incrementalSpec=v0.2'], + ['multipart/mixed; incrementalSpec=v0.2, application/json'], + ['application/json, multipart/mixed; incrementalSpec=v0.2'], ])('basic @defer working with accept: %s', async (accept) => { const app = await createApp({ typeDefs, resolvers }); const res = await request(app) @@ -2663,7 +2657,7 @@ content-type: application/json; charset=utf-8\r }); expect(res.status).toEqual(200); expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; incrementalDeliverySpec=graphql/incremental/v0.1"`, + `"multipart/mixed; boundary="-"; incrementalSpec=v0.2"`, ); expect(res.text).toEqual(`\r ---\r @@ -2679,15 +2673,9 @@ content-type: application/json; charset=utf-8\r }); it.each([ - [ - 'multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1', - ], - [ - 'multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1, application/json', - ], - [ - 'application/json, multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1', - ], + ['multipart/mixed; incrementalSpec=v0.2'], + ['multipart/mixed; incrementalSpec=v0.2, application/json'], + ['application/json, multipart/mixed; incrementalSpec=v0.2'], ])('basic @stream working with accept: %s', async (accept) => { const app = await createApp({ typeDefs: `#graphql @@ -2721,7 +2709,7 @@ content-type: application/json; charset=utf-8\r }); expect(res.status).toEqual(200); expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; incrementalDeliverySpec=graphql/incremental/v0.1"`, + `"multipart/mixed; boundary="-"; incrementalSpec=v0.2"`, ); expect(res.text).toEqual(`\r ---\r @@ -2755,7 +2743,7 @@ content-type: application/json; charset=utf-8\r .post('/') .set( 'accept', - 'multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1, application/json', + 'multipart/mixed; incrementalSpec=v0.2, application/json', ) .parse((res, fn) => { res.text = ''; @@ -2785,7 +2773,7 @@ content-type: application/json; charset=utf-8\r const res = await resPromise; expect(res.status).toEqual(200); expect(res.header['content-type']).toMatchInlineSnapshot( - `"multipart/mixed; boundary="-"; incrementalDeliverySpec=graphql/incremental/v0.1"`, + `"multipart/mixed; boundary="-"; incrementalSpec=v0.2"`, ); expect(res.text).toMatchInlineSnapshot(` " diff --git a/packages/server/src/ApolloServer.ts b/packages/server/src/ApolloServer.ts index 6a0afcf8bb4..86be5df4fdd 100644 --- a/packages/server/src/ApolloServer.ts +++ b/packages/server/src/ApolloServer.ts @@ -1409,9 +1409,8 @@ export const MEDIA_TYPES = { MULTIPART_MIXED_NO_DEFER_SPEC: 'multipart/mixed', MULTIPART_MIXED_EXPERIMENTAL_ALPHA_2: 'multipart/mixed; deferSpec=20220824', // This references the spec stored at - // https://github.com/apollographql/specs/pull/67 - MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9: - 'multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1', + // https://specs.apollo.dev/incremental/v0.2/ + MULTIPART_MIXED_EXPERIMENTAL_ALPHA_9: 'multipart/mixed; incrementalSpec=v0.2', TEXT_HTML: 'text/html', }; diff --git a/smoke-test/smoke-test.cjs b/smoke-test/smoke-test.cjs index b6af943206c..39e09da059a 100644 --- a/smoke-test/smoke-test.cjs +++ b/smoke-test/smoke-test.cjs @@ -86,14 +86,14 @@ async function smokeTest() { method: 'POST', headers: { 'content-type': 'application/json', - accept: `multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1, application/json`, + accept: `multipart/mixed; incrementalSpec=v0.2, application/json`, }, body: JSON.stringify({ query: '{h1: hello ...@defer{ h2: hello }}' }), }); assert.strictEqual( response.headers.get('content-type'), - `multipart/mixed; boundary="-"; incrementalDeliverySpec=graphql/incremental/v0.1`, + `multipart/mixed; boundary="-"; incrementalSpec=v0.2`, ); const body = await response.text(); diff --git a/smoke-test/smoke-test.mjs b/smoke-test/smoke-test.mjs index cb28ff0818c..f5a5a5fa401 100644 --- a/smoke-test/smoke-test.mjs +++ b/smoke-test/smoke-test.mjs @@ -82,14 +82,14 @@ if (process.env.INCREMENTAL_DELIVERY_TESTS_ENABLED) { method: 'POST', headers: { 'content-type': 'application/json', - accept: `multipart/mixed; incrementalDeliverySpec=graphql/incremental/v0.1, application/json`, + accept: `multipart/mixed; incrementalSpec=v0.2, application/json`, }, body: JSON.stringify({ query: '{h1: hello ...@defer{ h2: hello }}' }), }); assert.strictEqual( response.headers.get('content-type'), - `multipart/mixed; boundary="-"; incrementalDeliverySpec=graphql/incremental/v0.1`, + `multipart/mixed; boundary="-"; incrementalSpec=v0.2`, ); const body = await response.text();