Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5792cef
Update `DeferredJsonMerger` to take `pending` and `completed` into ac…
BoD Dec 13, 2024
25916b6
Track pending fragment ids rather than completed ones.
BoD Dec 16, 2024
824cc85
Update more tests
BoD Dec 16, 2024
8840a50
Add Apollo Server end-to-end tests
BoD Dec 16, 2024
9f40011
Add a few more edge case tests
BoD Dec 17, 2024
2259e5d
Fix missed test
BoD Dec 17, 2024
dccde73
Support appending lists in DeferredJsonMerger (for @stream)
BoD Jul 18, 2025
bb505c1
Rename DeferredJsonMerger -> IncrementalResultsMerger and DeferredFra…
BoD Jul 18, 2025
072059a
Fix running tests hardcoded to true
BoD Jul 21, 2025
989e122
Support both legacy and modern incremental delivery protocols, with a…
BoD Sep 24, 2025
a9bf9b2
Simplify CI job
BoD Sep 25, 2025
8bf128d
Do not expose accept headers in DefaultHttpRequestComposer's public API
BoD Sep 25, 2025
1dcd3b4
Make more symbols internal
BoD Sep 25, 2025
48b18e4
Mark IncrementalDeliveryProtocol @ApolloExperimental
BoD Sep 25, 2025
b323879
Rename Defer20220824 protocol to GraphQL17Alpha2 fpr consistency
BoD Sep 25, 2025
7fb253a
Keep symbols public but deprecated
BoD Sep 26, 2025
35cef06
Let HttpNetworkTransport handle the Accept header
BoD Sep 26, 2025
08c7a05
Rename Defer20220824 protocol to GraphQL17Alpha2 for consistency
BoD Sep 26, 2025
a14e3b3
Revert typealiases and use DeferredFragmentIdentifier again
BoD Sep 26, 2025
eec7001
Revert handling incremental results in WebSocket
BoD Sep 26, 2025
af5a3a4
Default to `application/graphql-response+json, application/json` in D…
BoD Sep 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
name: defer-integration-tests

on:
schedule:
- cron: '0 3 * * *'
Expand All @@ -22,9 +24,23 @@ jobs:
- run: |
./router --supergraph tests/defer/router/simple-supergraph.graphqls &

- uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda #v3.4.2

- env:
DEFER_WITH_ROUTER_TESTS: true
run: |
./gradlew --no-daemon --console plain -p tests :defer:allTests
defer-with-apollo-server-tests:
runs-on: ubuntu-latest
if: github.repository == 'apollographql/apollo-kotlin'
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7

- working-directory: tests/defer/apollo-server/
run: |
npm install --legacy-peer-deps
npx patch-package
APOLLO_PORT=4000 npm start &

- env:
DEFER_WITH_APOLLO_SERVER_TESTS: true
run: |
./gradlew --no-daemon --console plain -p tests :defer:allTests
5 changes: 5 additions & 0 deletions libraries/apollo-api/api/apollo-api.api
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ public final class com/apollographql/apollo/api/DefaultUploadKt {
}

public final class com/apollographql/apollo/api/DeferredFragmentIdentifier {
public static final field Companion Lcom/apollographql/apollo/api/DeferredFragmentIdentifier$Companion;
public fun <init> (Ljava/util/List;Ljava/lang/String;)V
public final fun component1 ()Ljava/util/List;
public final fun component2 ()Ljava/lang/String;
Expand All @@ -542,6 +543,10 @@ public final class com/apollographql/apollo/api/DeferredFragmentIdentifier {
public fun toString ()Ljava/lang/String;
}

public final class com/apollographql/apollo/api/DeferredFragmentIdentifier$Companion {
public final fun getPending ()Lcom/apollographql/apollo/api/DeferredFragmentIdentifier;
}

public final class com/apollographql/apollo/api/EnumType : com/apollographql/apollo/api/CompiledNamedType {
public fun <init> (Ljava/lang/String;Ljava/util/List;)V
public final fun getValues ()Ljava/util/List;
Expand Down
5 changes: 5 additions & 0 deletions libraries/apollo-api/api/apollo-api.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,11 @@ final class com.apollographql.apollo.api/DeferredFragmentIdentifier { // com.apo
final fun equals(kotlin/Any?): kotlin/Boolean // com.apollographql.apollo.api/DeferredFragmentIdentifier.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // com.apollographql.apollo.api/DeferredFragmentIdentifier.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // com.apollographql.apollo.api/DeferredFragmentIdentifier.toString|toString(){}[0]

final object Companion { // com.apollographql.apollo.api/DeferredFragmentIdentifier.Companion|null[0]
final val Pending // com.apollographql.apollo.api/DeferredFragmentIdentifier.Companion.Pending|{}Pending[0]
final fun <get-Pending>(): com.apollographql.apollo.api/DeferredFragmentIdentifier // com.apollographql.apollo.api/DeferredFragmentIdentifier.Companion.Pending.<get-Pending>|<get-Pending>(){}[0]
}
}

final class com.apollographql.apollo.api/EnumType : com.apollographql.apollo.api/CompiledNamedType { // com.apollographql.apollo.api/EnumType|null[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ fun <T : Any> and(vararg other: BooleanExpression<T>): BooleanExpression<T> = Bo
fun <T : Any> not(other: BooleanExpression<T>): BooleanExpression<T> = BooleanExpression.Not(other)
fun variable(name: String): BooleanExpression<BVariable> = BooleanExpression.Element(BVariable(name))
fun label(label: String? = null): BooleanExpression<BLabel> = BooleanExpression.Element(BLabel(label))
fun possibleTypes(vararg typenames: String): BooleanExpression<BPossibleTypes> = BooleanExpression.Element(BPossibleTypes(typenames.toSet()))
fun possibleTypes(vararg typenames: String): BooleanExpression<BPossibleTypes> =
BooleanExpression.Element(BPossibleTypes(typenames.toSet()))

internal fun <T : Any> BooleanExpression<T>.evaluate(block: (T) -> Boolean): Boolean {
return when (this) {
Expand All @@ -74,18 +75,29 @@ fun BooleanExpression<BTerm>.evaluate(
return evaluate {
when (it) {
is BVariable -> !(variables?.contains(it.name) ?: false)
is BLabel -> hasDeferredFragment(deferredFragmentIdentifiers, croppedPath!!, it.label)
is BLabel -> shouldParseFragment(deferredFragmentIdentifiers, croppedPath!!, it.label)
is BPossibleTypes -> it.possibleTypes.contains(typename)
}
}
}

private fun hasDeferredFragment(deferredFragmentIdentifiers: Set<DeferredFragmentIdentifier>?, path: List<Any>, label: String?): Boolean {
private fun shouldParseFragment(deferredFragmentIdentifiers: Set<DeferredFragmentIdentifier>?, path: List<Any>, label: String?): Boolean {
if (deferredFragmentIdentifiers == null) {
// By default, parse all deferred fragments - this is the case when parsing from the normalized cache.
return true
}
return deferredFragmentIdentifiers.contains(DeferredFragmentIdentifier(path, label))
val identifier = DeferredFragmentIdentifier(path, label)
return if (deferredFragmentIdentifiers.isPending()) {
// Modern protocol: parse fragments that are _not_ pending
!deferredFragmentIdentifiers.contains(identifier)
} else {
// Legacy GraphQL17Alpha2 protocol: parse fragments that have been merged
deferredFragmentIdentifiers.contains(identifier)
}
}

private fun Set<DeferredFragmentIdentifier>.isPending(): Boolean {
return any { it === DeferredFragmentIdentifier.Pending }
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class CustomScalarAdapters private constructor(
@JvmField
val falseVariables: Set<String>?,
/**
* Defer identifiers used to determine whether the parser must parse @defer fragments
* Identifiers used to determine whether the parser must parse deferred fragments
*/
@JvmField
val deferredFragmentIdentifiers: Set<DeferredFragmentIdentifier>?,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
package com.apollographql.apollo.api

import com.apollographql.apollo.annotations.ApolloInternal

data class DeferredFragmentIdentifier(
/**
* Path of the fragment in the overall JSON response. The elements can either be Strings (names) or Integers (array indices).
*/
val path: List<Any>,
val label: String?,
)
) {
companion object {
/**
* Special identifier to signal that the identifiers are pending, as in the modern version of the protocol.
*/
@ApolloInternal
val Pending = DeferredFragmentIdentifier(emptyList(), "__pending")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.apollographql.apollo.api.CustomScalarAdapters
import com.apollographql.apollo.api.Operation
import com.apollographql.apollo.api.Subscription
import com.apollographql.apollo.api.Upload
import com.apollographql.apollo.api.http.DefaultHttpRequestComposer.Companion.composePostParams
import com.apollographql.apollo.api.http.internal.urlEncode
import com.apollographql.apollo.api.json.JsonWriter
import com.apollographql.apollo.api.json.buildJsonByteString
Expand Down Expand Up @@ -39,14 +40,20 @@ class DefaultHttpRequestComposer(
val customScalarAdapters = apolloRequest.executionContext[CustomScalarAdapters] ?: CustomScalarAdapters.Empty

val requestHeaders = mutableListOf<HttpHeader>().apply {
if (apolloRequest.operation is Subscription<*>) {
add(HttpHeader(HEADER_ACCEPT_NAME, HEADER_ACCEPT_VALUE_MULTIPART))
} else {
add(HttpHeader(HEADER_ACCEPT_NAME, HEADER_ACCEPT_VALUE_DEFER))
}
if (apolloRequest.httpHeaders != null) {
addAll(apolloRequest.httpHeaders)
}
if (get("accept") == null) {
add(
HttpHeader("accept",
if (apolloRequest.operation is Subscription<*>) {
"multipart/mixed;subscriptionSpec=1.0, application/graphql-response+json, application/json"
} else {
"application/graphql-response+json, application/json"
}
)
)
}
}

val sendApqExtensions = apolloRequest.sendApqExtensions ?: false
Expand Down Expand Up @@ -92,14 +99,18 @@ class DefaultHttpRequestComposer(
// and thus is safe to execute.
// See https://www.apollographql.com/docs/apollo-server/security/cors/#preventing-cross-site-request-forgery-csrf
// for details.
internal val HEADER_APOLLO_REQUIRE_PREFLIGHT = "Apollo-Require-Preflight"
private const val HEADER_APOLLO_REQUIRE_PREFLIGHT = "Apollo-Require-Preflight"

@Deprecated("This was made public by mistake and will be removed in a future version, please use your own constants instead")
@ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v5_0_0)
val HEADER_ACCEPT_NAME = "Accept"

// TODO The deferSpec=20220824 part is a temporary measure so early backend implementations of the @defer directive
// can recognize early client implementations and potentially reply in a compatible way.
// This should be removed in later versions.
@Deprecated("This was made public by mistake and will be removed in a future version, please use your own constants instead")
@ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v5_0_0)
val HEADER_ACCEPT_VALUE_DEFER = "multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json"

@Deprecated("This was made public by mistake and will be removed in a future version, please use your own constants instead")
@ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v5_0_0)
val HEADER_ACCEPT_VALUE_MULTIPART = "multipart/mixed;subscriptionSpec=1.0, application/graphql-response+json, application/json"

private fun <D : Operation.Data> buildGetUrl(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ internal object ResponseParser {
val falseVariables = operation.falseVariables(customScalarAdapters)
data = operation.parseData(jsonReader, customScalarAdapters, falseVariables, deferredFragmentIds, errors)
}

"errors" -> errors = jsonReader.readErrors()
"extensions" -> extensions = jsonReader.readAny() as? Map<String, Any?>
else -> {
Expand Down Expand Up @@ -100,7 +101,8 @@ private fun JsonReader.readError(): Error {


@Suppress("DEPRECATION")
return Error.Builder(message = message).locations(locations).path(path).extensions(extensions).nonStandardFields(nonStandardFields).build()
return Error.Builder(message = message).locations(locations).path(path).extensions(extensions).nonStandardFields(nonStandardFields)
.build()
}

private fun JsonReader.readPath(): List<Any>? {
Expand Down Expand Up @@ -164,4 +166,4 @@ fun JsonReader.readErrors(): List<Error> {
}
endArray()
return list
}
}
26 changes: 13 additions & 13 deletions libraries/apollo-runtime/api/android/apollo-runtime.api
Original file line number Diff line number Diff line change
Expand Up @@ -215,17 +215,6 @@ public final class com/apollographql/apollo/interceptor/RetryOnErrorInterceptorK
public static final fun RetryOnErrorInterceptor (Lcom/apollographql/apollo/network/NetworkMonitor;)Lcom/apollographql/apollo/interceptor/ApolloInterceptor;
}

public final class com/apollographql/apollo/internal/DeferredJsonMerger {
public fun <init> ()V
public final fun getHasNext ()Z
public final fun getMerged ()Ljava/util/Map;
public final fun getMergedFragmentIds ()Ljava/util/Set;
public final fun isEmptyPayload ()Z
public final fun merge (Ljava/util/Map;)Ljava/util/Map;
public final fun merge (Lokio/BufferedSource;)Ljava/util/Map;
public final fun reset ()V
}

public final class com/apollographql/apollo/internal/MultipartReader : java/io/Closeable {
public fun <init> (Lokio/BufferedSource;Ljava/lang/String;)V
public fun close ()V
Expand All @@ -240,6 +229,14 @@ public final class com/apollographql/apollo/internal/MultipartReader$Part : java
public final fun getHeaders ()Ljava/util/List;
}

public final class com/apollographql/apollo/network/IncrementalDeliveryProtocol : java/lang/Enum {
public static final field GraphQL17Alpha2 Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol;
public static final field GraphQL17Alpha9 Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol;
public static fun values ()[Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol;
}

public abstract interface class com/apollographql/apollo/network/NetworkMonitor : java/io/Closeable {
public abstract fun isOnline ()Lkotlinx/coroutines/flow/StateFlow;
}
Expand Down Expand Up @@ -350,7 +347,7 @@ public abstract interface class com/apollographql/apollo/network/http/HttpInterc
}

public final class com/apollographql/apollo/network/http/HttpNetworkTransport : com/apollographql/apollo/network/NetworkTransport {
public synthetic fun <init> (Lcom/apollographql/apollo/api/http/HttpRequestComposer;Lcom/apollographql/apollo/network/http/HttpEngine;Ljava/util/List;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lcom/apollographql/apollo/api/http/HttpRequestComposer;Lcom/apollographql/apollo/network/http/HttpEngine;Ljava/util/List;ZLcom/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun dispose ()V
public fun execute (Lcom/apollographql/apollo/api/ApolloRequest;)Lkotlinx/coroutines/flow/Flow;
public final fun getInterceptors ()Ljava/util/List;
Expand All @@ -366,6 +363,7 @@ public final class com/apollographql/apollo/network/http/HttpNetworkTransport$Bu
public final fun httpEngine (Lcom/apollographql/apollo/network/http/HttpEngine;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder;
public final fun httpHeaders (Ljava/util/List;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder;
public final fun httpRequestComposer (Lcom/apollographql/apollo/api/http/HttpRequestComposer;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder;
public final fun incrementalDeliveryProtocol (Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder;
public final fun interceptors (Ljava/util/List;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder;
public final fun serverUrl (Ljava/lang/String;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder;
}
Expand Down Expand Up @@ -546,6 +544,7 @@ public final class com/apollographql/apollo/network/websocket/WebSocketNetworkTr
public final fun build ()Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport;
public final fun connectionAcknowledgeTimeout-BwNAW2A (Lkotlin/time/Duration;)Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport$Builder;
public final fun idleTimeout-BwNAW2A (Lkotlin/time/Duration;)Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport$Builder;
public final fun incrementalDeliveryProtocol (Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol;)Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport$Builder;
public final fun parserFactory (Lcom/apollographql/apollo/network/websocket/SubscriptionParserFactory;)Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport$Builder;
public final fun pingInterval-BwNAW2A (Lkotlin/time/Duration;)Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport$Builder;
public final fun serverUrl (Ljava/lang/String;)Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport$Builder;
Expand Down Expand Up @@ -674,7 +673,7 @@ public final class com/apollographql/apollo/network/ws/WebSocketEngineKt {
}

public final class com/apollographql/apollo/network/ws/WebSocketNetworkTransport : com/apollographql/apollo/network/NetworkTransport {
public synthetic fun <init> (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lcom/apollographql/apollo/network/ws/WebSocketEngine;JLcom/apollographql/apollo/network/ws/WsProtocol$Factory;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lcom/apollographql/apollo/network/ws/WebSocketEngine;JLcom/apollographql/apollo/network/ws/WsProtocol$Factory;Lkotlin/jvm/functions/Function3;Lcom/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun closeConnection (Ljava/lang/Throwable;)V
public fun dispose ()V
public fun execute (Lcom/apollographql/apollo/api/ApolloRequest;)Lkotlinx/coroutines/flow/Flow;
Expand All @@ -688,6 +687,7 @@ public final class com/apollographql/apollo/network/ws/WebSocketNetworkTransport
public final fun build ()Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport;
public final fun headers (Ljava/util/List;)Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport$Builder;
public final fun idleTimeoutMillis (J)Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport$Builder;
public final fun incrementalDeliveryProtocol (Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol;)Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport$Builder;
public final fun protocol (Lcom/apollographql/apollo/network/ws/WsProtocol$Factory;)Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport$Builder;
public final fun reopenWhen (Lkotlin/jvm/functions/Function3;)Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport$Builder;
public final fun serverUrl (Ljava/lang/String;)Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport$Builder;
Expand Down
Loading
Loading