This project adheres to Semantic Versioning v2.0.0.
response_cache can now emit a Cache-Tag header for CDN-side purging (Issue #9481)If you cache router responses at a CDN or reverse proxy in front of the router, the CDN previously had no way to know what to purge when your underlying data changed — Cache-Control only governs freshness (TTL), not invalidation. A new opt-in response_cache.cdn_invalidation block closes that gap: the router emits a response header carrying the same invalidation labels it already uses for active invalidation against its own Redis cache, so you can purge your CDN's edge cache using the CDN's own tag-based purge API.
response_cache:
enabled: true
cdn_invalidation:
enabled: true
subgraph:
all:
enabled: true
redis:
urls: ["redis://..."]
The header's value is a delimited list of labels drawn from the same sources already used for Redis invalidation (@cacheTag directives and the apolloCacheTags/apolloEntityCacheTags response extensions) — no schema changes required if you've already set those up. Each label is one of three tiers, coarsest to finest:
subgraph-{name} — every subgraph touched by the response.type-{subgraph}-{type} — every distinct (subgraph, GraphQL type) pair touched.@cacheTag/the extensions, unchanged.All three tiers are always sent together (subject to the size limit below), so you can escalate from purging a single fine-grained tag up to an entire subgraph's cached data if you're not confident a narrower purge fully propagated across every CDN edge location.
This is independent of the router's Redis-backed invalidation indexes: you don't need Redis response caching enabled, and you don't need the cache_tag invalidation index on, for the header to work correctly — including on a cache hit, since the labels needed to rebuild the header are persisted alongside the cached entry.
Configuration:
response_cache:
cdn_invalidation:
enabled: true
header_name: "Cache-Tag" # e.g. "Surrogate-Key" for Fastly
header_delimiter: "," # e.g. " " for Fastly
max_bytes: 16384 # matches Cloudflare's default Cache-Tag limit
experimental_on_overflow: truncate # "truncate" | "drop"
When a response's full label set would exceed max_bytes, the router packs the header coarsest-first and drops whatever doesn't fit, finest-grained first — so an oversized response still gets a usable header rather than none at all. That default (truncate) favors availability over precision: the response still gets cached at the CDN, just with a coarser invalidation surface than it ideally would have.
For cases where that tradeoff isn't acceptable — where caching data you can't fully invalidate by its intended fine-grained tag is worse than not caching it at all — set experimental_on_overflow: drop. This omits the header entirely whenever truncation would otherwise occur, so you can pair it with a CDN-side rule that forces cache bypass on a missing header, guaranteeing you never end up with cached data whose purge surface is narrower than what your invalidation logic expects. It's marked experimental because it's a deliberate, opt-in safety valve rather than the router's default posture, and its shape may still change.
With response_cache.debug: true, the cache debugger now also shows the labels behind each cache entry and, per response, the Cache-Tag header's outcome, value, untruncated size, and whether it was actually emitted. Three new metrics (cdn_tag_header.outcome, cdn_tag_header.untruncated_size, cdn_tag_header.error) track header emission, truncation, and errors.
Off by default; existing deployments see no behavior change.
By @aaronArinder in https://github.com/apollographql/router/pull/9811
When a client sent an operation with an unknown field on an input-object variable, the router's VALIDATION_INVALID_TYPE_VARIABLE error message embedded the full composed input type definition, including federation directives (@join__type, @tag, etc.) and internal subgraph names, regardless of the introspection or redact_query_validation_errors settings. This error now reports only the type name, matching the existing behavior for enum and scalar coercion errors.
Separately, variable coercion validated input-object fields and enum values against the internal supergraph schema rather than the client-facing API schema, so a variable could reference an @inaccessible field or enum value even though the same reference would be rejected in the operation document itself. Variable coercion now validates against the API schema, consistent with how operation documents are already validated.
By @carodewig
Default values in schemas and operations are now coerced and validated more correctly, fixing several bugs:
@deprecated on required arguments and input fields.@defer labelsThe query planner now rejects operations that reuse a @defer label, so labels are unique within an operation.
By @duckki
Demand control now bounds the list sizes it uses when estimating operation cost. Slicing-argument values (for example first) provided as negative integers are clamped to zero, and configured list_size defaults use a saturating conversion so that very large values cannot wrap. This keeps an operation's estimated cost from being computed lower than the work it represents.
By @abernix
http_max_request_bytes on GraphQL queries sent via HTTP GETThe router's limits.http_max_request_bytes setting previously only limited the size of the HTTP request body, so it had no effect on GraphQL-over-HTTP GET requests, which carry the query in the URL's query string instead of the body. In environments with a strict http_max_request_bytes configured, this allowed the limit to be bypassed by sending a GET request instead of a POST.
GET requests are now checked against the same http_max_request_bytes limit, measured against the byte length of the URI's query string, and rejected with a 414 (URI Too Long) response if it's exceeded.
Note that this measures the percent-encoded query string, which is larger than the same query's byte count in a compact POST JSON body (URL encoding can expand some characters to 3 bytes each). A query that fits under the limit as a POST body may need a slightly higher limit to also fit as a GET query string.
By @carodewig
iss claim when a JWKS entry configures an issuers allowlistWhen a JWKS entry is configured with an issuers allowlist, the router now rejects a validly-signed JWT that omits its iss claim (or sets it to null), instead of accepting it. Previously a token could sidestep the allowlist by not carrying an issuer at all, even though a token presenting a non-matching issuer was already rejected.
This matches how the router already handles the aud claim under an audiences allowlist: once an operator configures an allowlist, a token that cannot satisfy it is rejected. Behavior is unchanged when no issuers are configured — tokens with or without an iss claim are still accepted.
By @carodewig
When two JWKS entries shared the same key material — one kid-specific and constrained by issuer/audience, the other algorithm-only and unconstrained — a token that should have been rejected by the constrained entry could be accepted through the unconstrained one. When searching for a key, a kid match and an algorithm match each scored equally, so a kid-specific entry (matched on kid only) and an alg-only entry (matched on algorithm only) tied and were both returned as candidates. Validation then verified the signature against the constrained entry, failed its issuer/audience check, and fell through to the unconstrained entry, which accepted the token.
When a token's key ID (kid) matches one or more JWKS entries, only those matching entries are now considered, and each is validated in turn. A token can no longer be accepted by falling through to an entry whose kid it never matched. Entries that share key material under the same kid but carry different constraints — for example, the same key duplicated across a multi-tenant identity provider — are all still tried, so legitimate multi-entry setups continue to work.
By @carodewig
sha256Hash values before hex-decoding in APQThe Automatic Persisted Queries (APQ) layer decoded the client-supplied sha256Hash extension value with hex::decode before checking its length. A valid SHA-256 hash is always exactly 64 hex characters; a request with an arbitrarily large sha256Hash string (megabytes of valid hex) would still be hex-decoded in full, allocating memory and burning CPU proportional to the attacker-controlled input size before any comparison against the actual query hash occurred.
The router now rejects any sha256Hash whose length is not exactly 64 characters immediately, before attempting to decode it. This matches existing behavior for other malformed hashes: the request falls through to the normal PERSISTED_QUERY_NOT_FOUND response.
By @carodewig
Body-based (freeform) persisted-query safelist matching previously ignored the clientName scope declared in the PQ manifest, so an operation registered for one client was accepted for any client. Freeform matching now respects the manifest's client-name scope — trying the request's client name first and falling back to a client-agnostic entry — consistent with how ID-based lookup already behaves. Note that clientName is a self-reported, unauthenticated header and is not an authorization boundary.
By @carodewig
Adds header masking configuration to automatically mask sensitive header values in router logs, telemetry events, and coprocessor communications. This prevents accidental exposure of credentials, API keys, session tokens, and other sensitive information in observability data.
Key Features:
masking: block is configured, masking is enabled with a built-in sensitive-header list (authorization, cookie, set-cookie, x-api-key, etc.)headers.all and override for specific subgraphsrouter.request, router.response, supergraph.request, supergraph.response, connector.request, connector.response telemetry events, coprocessor logging, OpenTelemetry spans, and Apollo trace-report header forwarding (telemetry.apollo.send_headers) — which now redacts the same sensitive headers as the rest of header masking instead of only a hardcoded authorization/cookie/set-cookie setConfiguration:
Masking is configured within the headers plugin, nested under request and/or response sections. By default both global and per-subgraph sensitive_headers lists are additive: any entries you provide are added to the built-in fail-secure list (authorization, cookie, set-cookie, x-api-key, …). Set replace_defaults: true on a global or per-subgraph block to opt out of the built-ins and treat that block's list as authoritative. A subgraph that enables masking always inherits the built-in defaults even when global masking is disabled.
headers:
# Global defaults applied to all subgraphs
all:
request:
masking:
enabled: true # default
# Additional headers to mask on top of the built-in fail-secure list.
sensitive_headers:
- x-custom-secret
response:
masking:
enabled: true
sensitive_headers:
- x-internal-trace-id
# Per-subgraph extensions (added to global + built-ins).
subgraphs:
products:
request:
masking:
enabled: true
sensitive_headers:
- x-products-api-key
# Example: replace the built-in list entirely (advanced).
# all:
# request:
# masking:
# replace_defaults: true
# sensitive_headers:
# - x-only-this
Per-selector override (telemetry):
Telemetry header selectors — custom span/event/instrument attributes that read a request or response header — accept an optional redact field to override the masking rules for that single attribute:
redact: mask — always mask this header's value, regardless of the masking config.redact: allow — always emit the raw value, ignoring the masking rules.telemetry:
instrumentation:
spans:
router:
attributes:
my.auth.header:
request_header: authorization
redact: mask
Note: Telemetry emitted at the shared
http_clienttransport layer uses the global masking rules, because that layer has no subgraph identity. Per-subgraph overrides still apply at the subgraph and connector telemetry layers, and the global rules include the fail-secure defaults.Note: Masking applies to header values. In coprocessor debug logs, only the headers are masked — a request
bodyorcontextthat a coprocessor copies a sensitive header into is logged verbatim, so avoid placing secrets there if debug logging is enabled.
When enabled, sensitive header values are replaced with ***MASKED*** in debug logs and telemetry output while preserving header names for debugging purposes.
By @zachfettersmoore in https://github.com/apollographql/router/pull/9155
The telemetry RouterSelector surface gains two optional selectors for working with the GraphQL errors list on the router response:
response_errors_count — evaluates a JSONPath against the errors payload and exposes the match count as an integer OpenTelemetry value. Use a path like $[*] to count every error, or a filter expression to count only errors that match specific extension codes, messages, or other fields.response_errors_field — runs a JSONPath per error object and collects the matched values into an OpenTelemetry string array attribute, so you can attach structured error detail (for example $.message or $.extensions.code) to custom metrics or log pipelines.These selectors follow the same response-body wiring as the existing response_errors selector, so they are available once the serialized router response body is available for inspection.
By @smyrick and @carodewig in https://github.com/apollographql/router/pull/9448
Users can now set a cardinality_limit in their router config to override the OpenTelemetry SDK's default limit of 2000 distinct attribute combinations per metric. Once the limit is reached, additional attribute combinations are dropped and replaced with a single overflow series tagged otel_metric_overflow="true", losing their per-attribute breakdown.
Note that raising the cardinality limit increases memory usage proportionally, since each allowed attribute combination consumes memory. Monitor apollo.router.telemetry.metrics.cardinality_overflow to detect when a metric is hitting its limit.
The limit can be set globally under telemetry.exporters.metrics.common.cardinality_limit and per-metric under individual views[].cardinality_limit. The per-metric setting takes precedence over the global one.
telemetry:
exporters:
metrics:
common:
cardinality_limit: 5000
views:
- name: http.server.request.duration
cardinality_limit: 20000
Behavior change for existing views[] entries on non-histogram instruments. Previously, any views[] entry without an explicit aggregation silently converted counters and gauges to histograms. A counter named my.counter with a per-view entry would emit my_counter_bucket/my_counter_sum/my_counter_count instead of my_counter_total. Per-view configuration (cardinality_limit, rename, description, unit, allowed_attribute_keys) now preserves the instrument's native aggregation.
If you were relying on this conversion (e.g., you have dashboards or alerts built on _bucket/_sum/_count series for a counter), add an explicit aggregation: histogram to the affected view to keep the previous behavior:
views:
- name: my.counter
aggregation:
histogram:
buckets: [0.1, 0.5, 1.0]
By @rregitsky in https://github.com/apollographql/router/pull/9220
You can now set a sampler on individual tracing exporters so that each exporter receives a different fraction of traces. Previously, telemetry.exporters.tracing.common.sampler applied globally and all exporters received the same set of spans.
The per-exporter sampler field is available on:
telemetry.exporters.tracing.otlp.samplertelemetry.exporters.tracing.zipkin.samplertelemetry.exporters.tracing.datadog.samplertelemetry.apollo.samplerThe value uses the same trace-ID-based algorithm as telemetry.exporters.tracing.common.sampler, so it represents an absolute fraction of all requests — not a fraction of already-sampled spans. For example, to send 10% of traces to Apollo Studio but only 2% to an external OTLP endpoint:
telemetry:
exporters:
tracing:
common:
sampler: 0.1
otlp:
enabled: true
endpoint:
sampler: 0.02
The per-exporter sampler must not exceed telemetry.exporters.tracing.common.sampler; Router returns an error at startup if it does.
The sampler field is ignored on the Datadog exporter when preview_datadog_agent_sampling is enabled, because in that mode the Datadog agent controls sampling decisions and all spans must be forwarded unfiltered. The OTLP sampler is still respected in that mode (since OTLP typically targets a different backend), but a warning is emitted at startup — if the OTLP endpoint is also the Datadog agent, it may receive incomplete traces.
By @carodewig in https://github.com/apollographql/router/pull/9582
max_recursive_selections configurable (PR #9445)The router protects against deeply recursive or explosively large operations by counting the total number of selections encountered when recursively expanding fragment spreads. Previously this limit was hardcoded at 10,000,000. It can now be tuned via limits.router.max_recursive_selections:
limits:
router:
max_recursive_selections: 10000000 # default
Reducing this value further restricts the complexity of operations the router will accept. The existing escape hatch (APOLLO_ROUTER_DISABLE_SECURITY_RECURSIVE_SELECTIONS_CHECK) still applies when the limit is exceeded.
Previously, setting limits.router.warn_only would not affect the max recursive selections check, this has now been changed to only emit a warning log if warn_only is set to true.
By @rohan-b99 in https://github.com/apollographql/router/pull/9445
indexes configuration to response_cache invalidation (Issue #9521)Adds a new indexes block under each subgraph's response_cache.subgraph.<name>.invalidation configuration, letting operators choose which invalidation indexes Apollo Router maintains in Redis for that subgraph. All three indexes are enabled by default, so existing deployments are unchanged.
response_cache:
enabled: true
invalidation:
listen: "127.0.0.1:3000"
path: "/invalidation"
subgraph:
all:
enabled: true
invalidation:
enabled: true
shared_key: ""
indexes: # all three default to true; omit fields you want kept on
subgraph: false # disable `By subgraph` invalidation for this subgraph
type: false # disable `By type` invalidation for this subgraph
# cache_tag inherits its default (true) and continues to be honored
subgraphs:
networkapi_subgraph:
invalidation:
enabled: true
indexes:
type: false # mix per subgraph; other fields inherit their defaults
When a subgraph's indexes block disables a mode, the corresponding ZSET writes are skipped on cache inserts and the /invalidation endpoint returns HTTP 400 with a structured error for requests of that kind. Operators with workloads that only ever invalidate by a subset of modes can use this to tailor response_cache's indexing to their access pattern.
Index changes are additive only. Enabling a previously-disabled index does not retroactively populate it for entries that were written under the prior configuration. If a deployment changes indexes.subgraph from false to true, the subgraph-{name} ZSET will only see entries written after the change; pre-change entries are invisible to By subgraph invalidation requests until they age out via TTL. To bring a newly-enabled index online over the full cache set, flush Redis (or the affected namespace) before turning the index on.
Cache-Control parsing and serialization in the response cache (PR #9562)The response cache's Cache-Control handling has been refactored and several bugs fixed:
stale-if-error=N parse error fixed: Subgraph responses containing stale-if-error=600 previously caused a SUBREQUEST_HTTP_ERROR. The directive is now stored as Option<u64> and parsed correctly.stale-if-error or stale-while-revalidate as a boolean are now transparently deserialized instead of failing.Cache-Control headers: A header containing only unrecognized extension directives (e.g. cdn-cache-control=300) is now treated as no-store rather than being cached indefinitely with no TTL.s-maxage preserved separately from max-age throughout parsing, merging, and serialization.= (e.g. cdn-cache-control=rev=abc) are now correctly passed through to the _ => {} wildcard instead of returning a parse error, per RFC 9111 §5.2.no-cache field-specific form: no-cache="Authorization" (RFC 9111 §5.2.2.4) now correctly permits caching rather than being treated as a blanket revalidation directive.created timestamp is in the future are now treated as expired.public/private mutual exclusion: The response serializer now correctly suppresses public when private is also set.By @carodewig in https://github.com/apollographql/router/pull/9562
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
graphql.error.extensions.code on span events for all counted GraphQL errors (PR #9207)The apollo.router.operations.error metric carries graphql.error.extensions.code for every counted GraphQL error, but the matching span event only fired for errors raised by the demand_control and connectors plugins. Subgraph-returned, supergraph, execution, and router parse/validation errors reached OTLP traces without the code attribute, so trace-based consumers could not attribute errors to specific codes the way metric-based consumers already could.
The router now also emits the span event from count_operation_errors as a catch-all, gated on the same flag as the metric (telemetry.apollo.errors.preview_extended_error_metrics: enabled). The connectors and demand_control plugins continue to emit on their own spans so the event keeps the source-site attributes (connector coordinate, demand control context, etc.); to avoid double-emission, graphql::Error carries a non-serialized span_event_emitted flag that the catch-all checks and respects. The metric still increments either way, and the flag is never serialized into the user-facing error response.
By @david-castaneda in https://github.com/apollographql/router/pull/9207
The response cache uses Redis ZSETs as invalidation indexes — each cache entry is a member scored by its expiry timestamp. A background maintenance worker periodically calls ZREMRANGEBYSCORE to purge expired members. Under heavy write load, the worker's channel could accumulate thousands of identical keys, causing it to issue redundant Redis commands and fall behind.
This fix changes the worker to batch-drain up to 1,000 pending keys per cycle and deduplicate them into a HashSet before issuing any Redis commands, ensuring at most one ZREMRANGEBYSCORE call per unique key per cycle regardless of how many duplicates were queued.
By @aaronArinder in https://github.com/apollographql/router/pull/9642
A router started with a license whose expiry date falls more than roughly two years in the future crashed on startup with invalid deadline; err=Invalid. It now starts and serves traffic normally with such licenses.
By @rohan-b99 in https://github.com/apollographql/router/pull/9561
When a requested field is missing from the merged subgraph response, emit a
RESPONSE_VALIDATION_FAILED error in response.errors — turned on by
enable_result_coercion_errors. Previously, missing fields were only reported in
extensions.valueCompletion (and only for non-nullable fields), not in response.errors.
Additionally, redundant coercion and valueCompletion errors along null-bubble paths are now
suppressed. Previously, a single bad value inside nested non-null types could produce multiple
duplicate entries — one per non-null wrapper in the bubble chain. Now each coercion failure produces
exactly one originating error in response.errors and one valueCompletion entry at the source,
with no nesting-level duplicates.
By @duckki in https://github.com/apollographql/router/pull/9549
client.name and client.version attributes on router metrics can use selectors (PR #9502)A recent change added client.name and client.version as standard attributes on RouterAttributes to support aliasing. This inadvertently caused the JSON schema to reject selector-based overrides e.g.
client.name:
request_header: x-my-header
for those fields. We now support both the boolean/alias form, as well as the custom selector syntax.
By @rohan-b99 in https://github.com/apollographql/router/pull/9502
A self-referential connector input type (e.g. input Node { child: Node }) previously caused two problems:
walk would re-enter the same group indefinitely, consuming memory until composition was killed (previously reported as Type "X" has already been pre-inserted).@connect expression validation, resolve_shape would recurse through the type's Object fields without a cycle guard, causing a stack overflow.Recursive inputs now expand correctly and validate without unbounded recursion. When the validator re-enters a schema-defined named shape that is already on the resolution stack, it short-circuits to Unknown rather than walking the cycle.
By @briannafugate in https://github.com/apollographql/router/pull/9524
When a connector's isSuccess evaluates to false and the user has configured errors.extensions, the resulting top-level error now correctly deep-merges the user-supplied extensions into the default extensions object. Previously, a mapping like errors.extensions: "http: { myField: ... }" would wipe out the default http: { status } field; now both appear side-by-side, matching the public docs contract that defaults are retained alongside user fields.
This PR also adds Connector::output_shape() as foundation API for downstream validators (entity-key checker, type walker) to reason about both the success and error branches of an errors-as-data connector via Shape::one([selection.shape(), errors_shape()], []). No existing validator behavior changes in this PR.
By @briannafugate in https://github.com/apollographql/router/pull/9575
This fixes a query planner bug where the deferred block of an @defer query could be missing field values that should have been forwarded from the primary block, resulting in null fields or absent data in the deferred chunk at runtime.
When the query planner builds the fetch dependency graph, it runs a reduction step that prunes redundant "must run before" edges. That step could drop edges whose source fetch was the only producer of fields the deferred block needed (typically __typename or entity keys). The planner now detects those dropped edges and restores them as deferred dependencies so the deferred block receives the values it needs.
By @duckki in https://github.com/apollographql/router/pull/9443
Request::to_sha256 (Issue/PR #9497)The subgraph dedup hash concatenated its sections (headers, claim, operation_name, query, variables, extensions) with no domain separator. An empty section followed by a populated one fed the hasher the same bytes as the populated section followed by an empty one, so a request with variables: {"k": "1"}, extensions: {} produced the same SHA-256 as a request with variables: {}, extensions: {"k": "1"}. Because this hash drives the subgraph dedup cache and subscription dedup keying, the cache could serve one request's response back to a semantically distinct request.
Tag each section with a two-byte sentinel (\0H, \0C, \0O, \0Q, \0V, \0E) before its bytes, so cross-section collisions are no longer possible. Added two regression tests covering the variables ↔ extensions swap and the operation_name ↔ query concatenation collision.
By @aaronArinder in https://github.com/apollographql/router/pull/9497
->entries in connect v0.4 (PR #9619)Composition with connect v0.4 reported spurious CONNECTORS_UNRESOLVED_FIELD errors for fields selected
beneath an ->entries sub-selection — e.g. attributes: attributes->entries { key value } against
attributes: [AttributesEntry] left AttributesEntry.key and AttributesEntry.value "unresolved", even
though the selection plainly resolves them. The identical schema composed cleanly under connect v0.3.
Cause: v0.4's shape-based selection validator only collected seen fields for object-shaped selections;
list-valued shapes — produced by methods with statically known list outputs, like ->entries — fell
through a catch-all and contributed no seen fields. The validator now walks Array shapes by validating
each item shape against the field's (already list-unwrapped) inner named type.
By @fernando-apollo in PR #9619
Addresses a race condition where context keys added by concurrent parallel subgraph stages could unintentionally be deleted.
By @rohan-b99 in https://github.com/apollographql/router/pull/9519
The SubgraphRequest::to_sha256 helper, used as the key for subscription dedup
and the dedup-cache fast path, iterated http::HeaderMap directly. HeaderMap
does not guarantee a stable iteration order across requests, so two logically
identical requests could produce different SHA-256 hashes and miss the dedup
cache. The previous implementation acknowledged this with a // this assumes headers are in the same order comment but did not enforce it. Header pairs are
now sorted before being fed to the hasher, making the hash deterministic for a
given set of (name, value) entries regardless of insertion order.
This also fixes a macOS-only flake in
integration::subscriptions::ws_passthrough::test_subscription_ws_passthrough_dedup,
where header bucket ordering differed often enough to defeat dedup in practice.
By @aaronArinder in https://github.com/apollographql/router/pull/9497
Composition with connect v0.4 reported a spurious GROUP_SELECTION_IS_NOT_OBJECT error for a renamed
arrow-method projection over a nested-list scalar field — e.g. data: data->map(@->map(@->toString))
against data: [[String]] produced "selects a group data {}, but ReportData.data is of type String
which is not an object." The selection is a scalar projection, not a group selection, and the field is
[[String]]. The identical schema composed cleanly under connect v0.3.
Cause: the shape-based group-selection check treated every Array-shaped selection as a group selection,
then required the field's type to be an object. A list is now treated as a group selection only when its
element shape is itself a group (a list of objects), so a list of scalars validates cleanly. This is a
sibling fix to PR #9619, in the group-selection
detector rather than the seen-fields walker.
traffic_shaping.deduplicate_variables field (PR #9586)The router config field traffic_shaping.deduplicate_variables is now deprecated. Since variable deduplication is unconditionally enabled, the field is silently ignored and will be removed. A warning will now be issued at startup when this field is set to alert operators to remove the field from their config.
By @conwuegb in https://github.com/apollographql/router/pull/9586