This project adheres to Semantic Versioning v2.0.0.
When a Redis cluster had an even number of replicas, the router's use of lazy_connections = true could trigger a bug in fred's round-robin replica selection logic. Fred increments its round-robin counter when searching for a routable replica, and increments it again when it can't find one before requeuing the command. With an even replica count this causes fred to consistently target replicas that have no established connection, leading to GET failures falling through to backends and Redis CPU spikes.
Switched to lazy_connections = false (eager connections) so all replica connections are established upfront. The RouteableReplicaFilter that was the original motivation for lazy connections — preventing unroutable replicas from entering the routing table — continues to handle that responsibility, making the blast-radius isolation that lazy connections provided redundant.
By @aaronArinder in https://github.com/apollographql/router/pull/9589
connect/v0.4, with a behavior change for primitive literals (PR #9261)In schemas that @link connect/v0.4, the JSONSelection mapping grammar collapses the previously distinct SubSelection ({ id name }, whitespace-separated field selection) and LitObject ({ id: 1, name: "x" }, comma-separated object literal) productions into a single rule. The guiding principle, now pinned in the mapping-language reference:
Copy-and-paste any JSON value and it is already a valid
JSONSelection.
What gets easier:
{ id, title, address { street, city } }
Previously a comma inside a nested subselection (address { street, city }) was a parse error that surfaced only as a bare end-of-input diagnostic.id expands to id: id).$(...) wrapper.⚠️ Behavior change — primitive tokens in value position are now literals. Because { … } is now read as JSON, a bare primitive after name: is a literal value in v0.4, where v0.3 read it as a property access on the input data $:
| Selection string | v0.3 meaning | v0.4 meaning |
|---|---|---|
currencyCode: "USD" | $."USD" (look up property USD) | literal string "USD" |
flag: true | $.true (look up property true) | literal boolean true |
x: null | $.null | literal null |
An unchanged selection string can therefore produce different output once you bump its @link to v0.4. In practice this is overwhelmingly safe or beneficial — across a corpus of 25,703 selections from 7,375 supergraphs, 97.4% parse and evaluate identically, and most of the rest are cases where the v0.3 property lookup silently resolved to null and v0.4 now emits the literal the developer intended. The pattern that needs attention is selecting a REST field whose name GraphQL won't accept, e.g. gqlSafeAlias: "@odata.nextLink" — in v0.4 that becomes the constant string instead of the data, so restore the lookup by fortifying with $.:
gqlSafeAlias: $."@odata.nextLink"
This is gated to connect/v0.4 — the v0.2 and v0.3 parse paths are unchanged, and verbose v0.3 forms still parse under v0.4, so schemas that have not upgraded their @link are unaffected.
By @benjamn in https://github.com/apollographql/router/pull/9261
->split arrow method to the connectors mapping language (PR #9199)Connector @connect(selection:) mappings gain a ->split arrow method, analogous to JavaScript's String.prototype.split. It splits a string into an array of substrings on a separator:
$('a,b,c')->split(',') → ["a", "b", "c"]
$('hello')->split('') → ["h", "e", "l", "l", "o"] # empty separator splits into characters (UTF-8 aware)
$('a,b,c')->split(',', 2) → ["a", "b"] # optional non-negative limit caps the element count
text->split($.sep) → separator taken dynamically from the data
It is the inverse of ->joinNotNull ($->split(',')->joinNotNull(',') round-trips to the original), is string-only, and errors on non-string input.
By @benjamn in https://github.com/apollographql/router/pull/9199
->trim, ->trimStart, and ->trimEnd arrow methods to the connectors mapping language (PR #9211)Three zero-argument string-trimming arrow methods are now available in @connect(selection:) mappings:
->trim — strip leading and trailing whitespace->trimStart — strip leading whitespace only->trimEnd — strip trailing whitespace only$(' hello ')->trim → "hello"
$(' hello ')->trimStart → "hello "
$(' hello ')->trimEnd → " hello"
Semantics match Rust's str::trim / str::trim_start / str::trim_end: locale-independent and Unicode White_Space–aware (NBSP, EM SPACE, etc.). All three are String -> String and error on non-string input.
By @benjamn in https://github.com/apollographql/router/pull/9211
@connect directives without an http block (PR #9124)A @connect directive can now omit its http property, producing a mapping-only (requestless) connector that resolves a field by applying its selection to data already available — such as field arguments and the enclosing object — without issuing an outbound HTTP request. Because no transport runs, the selection cannot reference transport-derived data (the response body, $status, or $response); doing so is rejected at composition. Requestless connectors are also supported in nested mutations.
By @andrewmcgivery in https://github.com/apollographql/router/pull/9124
ignore_auth_context option to subscription deduplication config (PR #9078)When the router's JWT authentication plugin validates a token, it decodes the claims and stores them internally on the request — before any subgraph request is built. The router then factors those stored claims into its check for whether two subscriptions are identical, separately from any HTTP headers it may forward downstream.
This means that on any router with JWT authentication enabled, every authenticated user effectively gets their own subgraph WebSocket connection — even if the subscription data is identical for all users, and even if the Authorization header is never forwarded to the subgraph at all. Adding authorization to ignored_headers doesn't help here, because it only affects HTTP headers; the decoded claims live in a different layer that ignored_headers never touches.
Two new capabilities are added to the deduplication config block:
ignore_auth_context: bool (default: false) — when true, the router skips stored JWT claims when checking subscription identity, allowing all authenticated users to share a single subgraph WebSocket connection when the subscription data is truly non-personalized (e.g., product price updates, stock price feeds).all: / subgraphs: — deduplication settings can now be set globally with a default and overridden per subgraph by name, using the standard SubgraphConfiguration<T> pattern already used elsewhere in the router config.subscription:
deduplication:
all:
enabled: true
subgraphs:
stocks:
ignore_auth_context: true
By @abernix in https://github.com/apollographql/router/pull/9078
include_cache_control_header_on_router_response to suppress Cache-Control on client responses (PR #9002)The response cache plugin now supports a include_cache_control_header_on_router_response boolean config option (defaults to true). When set to false, the router omits the Cache-Control header from supergraph responses sent to clients, while all internal caching behavior — Redis storage, TTL enforcement, cache key computation, and the cache debugger — remains unchanged.
This is useful when the router sits behind a CDN or reverse proxy that manages its own caching headers, or when you want to prevent clients from caching responses locally while keeping server-side caching active.
response_cache:
enabled: true
include_cache_control_header_on_router_response: false # default: true
subgraph:
all:
enabled: true
redis:
urls: ["redis://..."]
By @ebylund in https://github.com/apollographql/router/pull/9002
The router can now cap the number of bytes it reads from subgraph and connector HTTP response bodies, protecting against out-of-memory conditions when a downstream service returns an unexpectedly large payload.
The limit is enforced as the response body streams in — the router stops reading and returns a GraphQL error as soon as the limit is exceeded, without buffering the full body first.
Configure a global default and optional per-subgraph or per-source overrides:
limits:
subgraph:
all:
http_max_response_size: 10MB # 10 MB for all subgraphs
subgraphs:
products:
http_max_response_size: 20MB # 20 MB override for 'products'
connector:
all:
http_max_response_size: 5MB # 5 MB for all connector sources
sources:
products.rest:
http_max_response_size:
There is no default limit; responses are unrestricted unless you configure this option.
When a response is aborted due to the limit, the router:
SUBREQUEST_HTTP_ERRORapollo.router.limits.subgraph_response_size.exceeded or apollo.router.limits.connector_response_size.exceeded counterapollo.subgraph.response.aborted: "response_size_limit" or apollo.connector.response.aborted: "response_size_limit" on the relevant spanConfiguration migration: Existing limits fields (previously at the top level of limits) are now nested under limits.router. A configuration migration is included that updates your config file automatically.
By @carodewig in https://github.com/apollographql/router/pull/9160
apollo.router.connection.acquire.duration metric for TCP/TLS connection timing (PR #9309)Adds a new histogram metric, apollo.router.connection.acquire.duration, that records how long it takes to establish a new TCP or Unix socket connection to a downstream service (subgraph, connector, or coprocessor). The metric fires only when the connection pool opens a new connection — pool hits are not recorded.
This metric is useful for diagnosing connection establishment latency. For example, if a subgraph shows elevated overall response latency, a high connection.acquire.duration indicates the delay is in TCP/TLS setup; a near-zero value (or no data) points to post-connection causes like slow server responses.
Attributes:
network.transport: tcp for HTTP connections, unix for Unix socket connectionssubgraph.name: name of the subgraph (for subgraph connections)connector.source.name: name of the connector source (for connector connections)coprocessor: true (for coprocessor connections)By @carodewig in https://github.com/apollographql/router/pull/9309
max_lifetime configuration for subscriptions (PR #9216)Adds a new max_lifetime field to the subscription configuration block, allowing operators to set a maximum duration for how long a subscription can remain open. After the configured duration the subscription is closed and the client receives a terminal error with extension code SUBSCRIPTION_MAX_LIFETIME_EXCEEDED.
subscription:
enabled: true
max_lifetime: 10m # close subscriptions after 10 minutes
mode:
callback:
public_url: "https://my-router.example.com/subscription/callback"
By default (max_lifetime unset) there is no lifetime limit, preserving the existing behaviour.
By @BobaFetters in https://github.com/apollographql/router/pull/9216
operation_body_timeout for file upload requests (PR #9243)Adds a new operation_body_timeout limit to the file uploads plugin, allowing operators to set a tight deadline on reading the operations field (GraphQL query + variables) from multipart request bodies, independently of the overall router timeout.
File uploads is the only router flow where the request body is read as a stream in the plugin layer: the multipart body must be parsed to extract the operations field before query planning can begin. This means a slow or stalled client can hold a connection open until the global router timeout fires. The new operation_body_timeout lets you set a tighter deadline specifically for that body-reading phase.
If operation_body_timeout is not set, no additional body-read timeout is applied — the overall router timeout remains the only bound.
preview_file_uploads:
enabled: true
protocols:
multipart:
enabled: true
limits:
operation_body_timeout: 5s # optional; no default
When the timeout fires, the router returns a 504 Gateway Timeout response with extension code GATEWAY_TIMEOUT.
By @carodewig in https://github.com/apollographql/router/pull/9243
CONNECTORS_CANNOT_RESOLVE_KEY for @connect bodies that use an arrow method before a subselection (PR #9375)Composition rejected an otherwise-valid @connect mapping with CONNECTORS_CANNOT_RESOLVE_KEY when an arrow method such as ->filter(...) was followed by a { … } subselection on a field used to resolve an entity @key (or @requires). For example:
type Cart @key(fields: "id") {
result: String
@connect(
http: { POST: "/p", body: "{ items: $this.items->filter(@.product) { id name } }" }
…
)
}
The selection parsed correctly — the key-resolution validator was the failure point, because its trie walker treated arrow methods as opaque leaves and never recursed into the post-method subselection. The fix routes key-resolution analysis through the shape system (a new SelectionAnalysis), which consults each method's shape relation to its input: shape-preserving methods like ->filter / ->slice correctly attribute consumption to the input array, while shape-transforming methods like ->size / ->jsonStringify terminate at the method boundary. The fix applies across connect/v0.2–v0.4.
By @benjamn in https://github.com/apollographql/router/pull/9375
@connect field values when a root query alias is combined with field-level aliases (Issue #9347)Queries that aliased both the root query field and one or more of its subfields on a @connect-backed type returned null for every aliased subfield, which could cascade into null propagation for non-nullable types. Either alias in isolation worked correctly — only the combination of both triggered the bug.
Given this query:
{
items: search_items(query: "test") {
results {
id
link: viewUri
}
}
}
Before
Aliased fields returned null, and null propagation bubbled up through non-nullable types until the entire result was nullified:
{
"data": { "items": null },
"extensions": {
"valueCompletion": [
{ "message": "Null value found for non-nullable type String", "path": ["items", "results", 0] },
{ "message": "Null value found for non-nullable type Item", "path": ["items", "results", 0] },
After
Root and field aliases now work together as expected:
{
"data": {
"items": {
"results": [
{ "id": "1", "link": "https://example.com/docs/001" }
]
}
}
}
By @jhrldev in https://github.com/apollographql/router/pull/9358
apollo.router.operations.coprocessor.duration even when the coprocessor call times out (PR #9296)apollo.router.operations.coprocessor.duration is now recorded even when a coprocessor call is cut short by a router timeout. Previously, the metric was only emitted when the call completed normally, leaving timeout latencies invisible in the histogram.
By @conwuegb and @carodewig in https://github.com/apollographql/router/pull/9296
Previously, if a schema reload failed — for example, because a persisted query manifest fetch from Uplink encountered a transient network error — the router would log "error while reloading, continuing with previous configuration" and then stop retrying. All subsequent background polls from Uplink would return Unchanged (because last_id had already advanced to the new schema ID), leaving the router permanently serving the old schema until manually restarted.
The router now enters a Reloading state on reload failure and schedules automatic retries. The retry delay (default 10 seconds) and maximum retry count (default 5) are configurable via the new reload configuration key:
reload:
max_retries: 5 # 0 to disable, null for unlimited
retry_delay: 10s
The retry budget is reset whenever a new reload trigger arrives — a new schema or license from Uplink, a configuration or rhai script file change, or an explicit reload signal — so any new change always gets a fresh set of attempts even if previous retries were exhausted.
By @carodewig in https://github.com/apollographql/router/pull/9391
When authentication.subgraph.subgraphs configured aws_sig_v4 for a specific subgraph, the signing parameters were visible to every other subgraph in the same operation — causing unconfigured subgraphs to have their Authorization header overwritten with AWS credentials.
Signing parameters are now scoped to the individual subgraph HTTP request rather than the operation, so AWS credentials only travel with requests to the subgraph they were configured for.
By @carodewig in https://github.com/apollographql/router/pull/9385
@defer chunk correctly in the is_primary_response telemetry selector (PR #9238)The is_primary_response: true supergraph telemetry selector returned false for every chunk of a multipart @defer response — including the primary (first) chunk — when evaluated at on_response or on_response_event scope. This made it impossible to distinguish primary from deferred chunks in metrics, events, and conditional telemetry.
The selector now returns true for the primary chunk and false for subsequent deferred chunks, so per-chunk filtering works as documented:
telemetry:
instrumentation:
instruments:
supergraph:
my.defer.primary.duration:
value: event_duration
type: histogram
attributes:
is_primary:
is_primary_response: true
Now produces split metric series (is_primary="true" for the primary chunk, is_primary="false" for deferred chunks) instead of a single series with is_primary="false" for everything.
By @ebylund in https://github.com/apollographql/router/pull/9238
on_graphql_error per response part in coprocessors and telemetry for @defer responses (PR #9365)Previously, the on_graphql_error condition used in coprocessor and telemetry configurations did not work correctly for deferred (@defer) multipart responses:
CONTAINS_GRAPHQL_ERROR context flag, which caused it to fire for every subsequent chunk once any earlier chunk had errors — and to never fire if the first chunk was clean.on_graphql_error telemetry selector at the supergraph stage returned the accumulated error state rather than the current chunk's error state.The router and supergraph coprocessors, along with telemetry selectors, now evaluate on_graphql_error conditions per response part, so the condition fires exactly once per part that contains GraphQL errors — no more, no less.
Additionally, on_graphql_error: false (fire when there are no errors) now works correctly in all selector contexts: router, supergraph, and subgraph.
By @carodewig in https://github.com/apollographql/router/pull/9365
When many filesystem events arrived in quick succession, the Rhai script watcher could spin an OS thread or panic — the previous retry loop kept trying to send on a full channel, and would panic if the receiver closed before the retry succeeded.
The watcher now drops duplicate notifications when the channel is already full, matching the behavior introduced for the configuration file watcher in PR #8336. Reloads always re-read the current file from disk, so a single pending notification in the channel is sufficient to guarantee the latest contents will be picked up.
By @carodewig in https://github.com/apollographql/router/pull/9391
cache-control: no-cache is set with response_cache enabled (PR #9197)When response_cache (or preview_entity_cache) was enabled and an incoming request carried cache-control: no-cache, entity fields resolved via _entities queries returned null. Root fields were unaffected. Regression introduced in v2.13.0.
The cache plugin's no-cache path is now treated as all-cache-miss: the cache builds a properly sized result list so _entities is assembled correctly, skips the Redis round-trip, and suppresses misleading hit/miss telemetry for requests that intentionally bypass the cache.
By @OriginLeon in https://github.com/apollographql/router/pull/9197
batching.mode optional and reject unknown subgraph batching fields (PR #9315)The batching configuration struct now uses a struct-level #[serde(default)] with an explicit impl Default, rather than per-field #[serde(default)] annotations. This aligns with the project's YAML design guidance, which requires that the serde deserialization path and the Default implementation use the same mechanism.
As a result of this change:
mode field now has an explicit default of batch_http_link (the only supported mode), so it is no longer required in YAML configuration. Existing configurations that specify mode: batch_http_link are unaffected.subgraph batching configuration now rejects unknown fields, consistent with the rest of the router configuration.By @BobaFetters in https://github.com/apollographql/router/pull/9315
connectors.subgraphs configuration field (PR #9415)The connectors.subgraphs configuration field is now deprecated in favor of connectors.sources. When connectors.subgraphs is set, the router will emit a deprecation warning at startup directing operators to rename the key. The field will be removed in a future 3.x release.
By @BobaFetters in https://github.com/apollographql/router/pull/9415
Query::apply_root_selection_set performance by 5-15% (PR #9458)Query::apply_root_selection_set now combines three separate map lookups into one, reducing work on every query plan application by 5-15%.
By @rohan-b99 in https://github.com/apollographql/router/pull/9458
Adds a new section to the request limits docs showing how to use a custom telemetry event with a gt condition on the query selector to log operations that exceed a candidate max_aliases, max_depth, max_height, or max_root_fields value — without configuring limits or enabling warn_only mode. The example also captures the client name and version from the apollographql-client-name and apollographql-client-version headers so you can see which clients are sending the offending operations. The warn_only section now cross-references this approach.
By @smyrick in https://github.com/apollographql/router/pull/9294
The authorization docs now explain what happens when you apply @authenticated, @requiresScopes, or @policy directly to a root operation type (Query, Mutation, or Subscription) in a subgraph. Because root operation types are shared merged types in a federated graph, the directive composes into the supergraph root type and applies to every field on that type, including fields contributed by other subgraphs. To scope authorization reliably, apply the directive to each field rather than to the root type.
By @andywgarcia in https://github.com/apollographql/router/pull/9213
Redis TLS configuration is now documented inline on every feature page that exposes a Redis configuration section — APQ distributed caching, query plan distributed caching, and response cache customization — instead of being mentioned only on the central TLS overview page. Operators configuring Redis for any specific feature can now find the TLS guidance directly on the page they're already reading.
By @bignimbus in https://github.com/apollographql/router/pull/9172
http2_max_headers_list_bytes and correct the limits config example (PR #9388)The Request Limits page now documents the limits.router.http2_max_headers_list_bytes option, including its default (16KiB) and what the router returns on overflow (431 Request Header Fields Too Large). The combined limits YAML example on the same page has also been updated to match the nested limits.router.* / limits.subgraph.* / limits.connector.* shape that v2.15.0 introduced — so copy-pasting the example into a router config now produces a configuration the router will accept.
By @apollo-mateuswgoettems in https://github.com/apollographql/router/pull/9388
apollo.router.cache.redis.reconnection and apollo.router.cache.redis.unresponsive metrics (PR #9306)Two Redis-health counters that the router already emits are now documented on the standard instruments reference page and the response cache observability page:
apollo.router.cache.redis.reconnection — increments when a Redis server signals the client to reconnect.apollo.router.cache.redis.unresponsive — increments when a Redis server stops responding.Both carry kind (which Redis-backed cache — APQ, query plan, entity cache, response cache) and server (the specific Redis endpoint) attributes, making it possible to track Redis health per cache and per endpoint.
By @apollo-mateuswgoettems in https://github.com/apollographql/router/pull/9306
required_to_start: true on startupThe router's Redis-backed caches (query planner, entity cache, APQ, response cache) could silently stall after a network event involving Redis replicas or the full cluster — accumulating queued commands, command timeouts, latency, and memory pressure until the router was redeployed. The router now detects when the underlying Redis client has given up reconnecting, drains the connection pool, and rebuilds it on the next request. In deployments where the broadcast cluster topology contains nodes that aren't routeable from the router's network position (for example, internal IPs reserved for replica promotion), a new replica filter screens those nodes out before they enter the routing table.
The required_to_start: true flag — available on each cache under supergraph.query_planning.cache.redis, apq.router.cache.redis, preview_entity_cache.subgraph.all.redis, and experimental_response_cache.subgraph.all.redis — now actually fails the router's startup fast when Redis is unreachable, instead of hanging indefinitely or silently returning success under broadcast overflow.
The router also now supports required_to_start: false, allowing the router to start when Redis is unavailable at boot and to begin caching once Redis becomes reachable.
For more technical internal details, see PR #9023 and PR #9418. For more details on configuring the router's Redis-backed caches, see Response Cache Customization and the related caching docs.
By @aaronArinder in https://github.com/apollographql/router/pull/9023 and https://github.com/apollographql/router/pull/9418
The router no longer emits the spurious cannot use meter provider after shutdown error during shutdown. The metrics aggregation layer now returns a noop instrument in that path instead of panicking.
By @rohan-b99 in https://github.com/apollographql/router/pull/9248
When pool_idle_timeout was introduced in v2.13.0, the router unconditionally enabled a background timer that proactively closed idle connections exceeding the timeout. In some network environments, the TCP close sent by this background task raced with a new connection attempt and caused significant latency spikes on the next request.
The router now uses lazy eviction: connections are only closed at checkout time, when a request finds a pooled connection that has exceeded pool_idle_timeout. No TCP closes are sent between requests. This matches router behavior before v2.13.0.
By @carodewig in https://github.com/apollographql/router/pull/9308
When the JWKS server is unreachable, the router now logs a specific, actionable message including the URL and the failure category (timed out, connection failed, or generic failure) — replacing the previous vague "could not get url" message.
By @carodewig in https://github.com/apollographql/router/pull/9258
The router's DNS resolver (via hickory-resolver) inherits two upstream advisories in hickory-proto / hickory-net 0.26.0. Both are fixed in 0.26.1, which is now pinned in Cargo.lock.
Source-built consumers were already insulated by caret-semver dependency declarations; this change picks up the fix in Apollo's pre-built binaries and Docker images.
By @carodewig in https://github.com/apollographql/router/pull/9321
Batched queries that use @defer are not supported by the router. Previously these requests produced a malformed multipart response; they now return a single JSON response with errors that explicitly indicates the lack of support.
By @rohan-b99 in https://github.com/apollographql/router/pull/9311
http_max_request_bytes only to the operations field, not file streams (PR #9226, PR #9327)Previously, limits.http_max_request_bytes (default 2 MB) was applied to the entire multipart body of file upload requests, causing large file uploads to be rejected even when preview_file_uploads.protocols.multipart.limits.max_file_size was configured to allow them.
The limit now applies only to the GraphQL operations field (the query and variables). File data is bounded separately by max_file_size, enforced by the multer parser.
By @carodewig in https://github.com/apollographql/router/pull/9226 and https://github.com/apollographql/router/pull/9327
Adds apollo.router.config.experimental_* OTLP gauge metrics for all customer-facing experimental config flags, using the existing populate_config_instrument! pattern in configuration/metrics.rs. This enables Apollo to track adoption of experimental features so we can inform decisions about which to promote or remove in future releases.
Features now instrumented:
experimental_chaosexperimental_type_conditioned_fetchingexperimental_hoist_orphan_errorsexperimental_log_on_broken_pipeexperimental_plans_limitexperimental_paths_limitexperimental_reuse_query_plansexperimental_cooperative_cancellationexperimental_prewarm_query_plan_cacheexperimental_local_field_metricsexperimental_response_trace_idexperimental_otlp_endpointexperimental_otlp_tracing_protocolexperimental_otlp_metrics_protocolexperimental_http2experimental_http2_keep_alive_intervalexperimental_http2_keep_alive_timeoutexperimental_mock_subgraphsexperimental.expose_query_plan (recorded as apollo.router.config.experimental_expose_query_plan)The mandatory experimental_diagnostics plugin is intentionally excluded because it is loaded on every router and would always report adoption as 100%.
By @aaronArinder in https://github.com/apollographql/router/pull/9330
The router now avoids some unnecessary memory allocations when making subgraph requests, particularly on the APQ (Automatic Persisted Queries) path.
By @carodewig in https://github.com/apollographql/router/pull/9266
Every query plan cache lookup — including cache hits — previously acquired the wait_map mutex before checking whether the value was in memory. On a warm cache this was pure overhead: the mutex was locked twice, a broadcast::Sender was allocated, and a cleanup task was spawned, all to be immediately discarded.
A fast path now checks the in-memory cache before acquiring the mutex. On a hit the value is returned immediately; the wait_map path is only entered on a miss, which is the only case where deduplication is needed.
By @theJC in https://github.com/apollographql/router/pull/9279