github apollographql/router v2.16.3

3 hours ago

🐛 Fixes

Fix custom attributes on the apollo.router.operations.subscriptions.terminated.client subscription metric (PR #9605)

The telemetry for configuration apollo.router.operations.subscriptions.terminated.client accepts the same router selector syntax as other router instruments, but selector-based attributes were not actually applied at runtime.

You can now configure the default attributes (reason, subgraph.name, and client.name) and add custom attributes using any RouterSelector. For example, to include the operation name on every termination event:

telemetry:
  instrumentation:
    instruments:
      router:
        apollo.router.operations.subscriptions.terminated.client:
          attributes:
            reason: true
            subgraph.name: true
            client.name: true
            graphql.operation.name:
              operation_name: string

By @rohan-b99 in #9605

Include 431/414 responses in router metrics (PR #9606)

HTTP 431 (Request Header Fields Too Large) and 414 (URI Too Long) responses are generated by hyper's HTTP parser before a request reaches the the router's telemetry plugin. Those rejections were therefore invisible to apollo.router.operations, the http.server.request.duration histogram, and APM traces - making it hard to spot clients sending oversized headers or URIs.

The router now emits the same metrics as a normal router operation (counter and duration histogram, both tagged with http.response.status_code) plus a router trace span. The span records apollo_private.duration_ns for Apollo Studio, and declares http.response.status_code so APM exporters surface the status.

By @rohan-b99 in #9606

Fix excess CPU and latency from extended metrics reference mode on fragment-heavy operations

extract_enums_from_selection_set (used when telemetry.apollo.metrics_reference_mode is extended, the default) deduplicated fragment spreads with a HashSet keyed on &Object. Because &T hashes and compares by value, every fragment spread deep-hashed the entire response subtree, so extraction cost grew with response size and fragment count. Operations with many nested fragments over large responses saw significant added CPU and tail latency.

The set now keys on the response object's address via a small ByAddress wrapper, which is O(1) and sufficient for the deduplication and cycle protection this function needs. Reported reference data is unchanged.

By @ebylund in #9840

Fix query planner silently dropping a field's own @requires when merged into a same-subgraph ancestor

When a field with its own @requires was reached from an ancestor fetch node in the same subgraph, the planner's merge optimization could fold that field directly into the ancestor without checking whether it still had other pending dependencies -- such as a fetch created specifically to satisfy its @requires. The dependency was silently discarded: the ancestor's fetch would request the field without first fetching the data it required, producing incomplete or incorrect results with no error or warning.

The query planner's node-merging logic now rejects merging a node into an ancestor unless every other dependency of that node is already satisfied by (i.e., is an ancestor of) the merge target, preserving the correct fetch ordering.

By @briannafugate408 in #9967

Fix subgraph errors being silently dropped on Flatten fetches crossing a type-conditioned field

A subgraph-level error with no _entities-indexed path (for example, the error produced by a traffic_shaping timeout) could be silently dropped instead of surfacing to the client, when the erroring fetch's Flatten path crossed an abstract-typed (interface or union) field reached through a type condition on a single-valued field rather than on an array wildcard (e.g. ...edges.@.node|[SomeConcreteType].collection).

The router expands such an error across every entity in the batch by matching the error's declared path (which retains its type condition) against each entity's real, already-materialized path (which never carries one). That match only special-cased array Index/Flatten pairs and otherwise required exact structural equality, so a Key path element with a type condition never matched the same Key without one — the error matched none of the batch's entities and was dropped entirely, with no trace in the response's errors or anywhere else, while the corresponding data was simply missing.

Path::equal_if_flattened now also treats two Key path elements as equal whenever their names match, regardless of any type condition attached to either side, so these errors correctly surface once per affected entity instead of vanishing silently.

By @BobaFetters in #9925

JSONPath slice selectors no longer return nothing when the array is shorter than the slice bound

Telemetry selectors that take a JSONPath (response_errors, response_errors_count, response_data, and the headers plugin's from_body path) evaluate that path against a serde_json_bytes::Value. Slice expressions such as $[:10], $[0:10], or $[-10:] silently matched zero elements whenever the array was shorter than the bound, instead of clamping the bound to the array length as standard JSONPath slice semantics require. A selector like $[:10] over response_errors therefore only produced an attribute once a response happened to carry at least 10 errors, and reported nothing — not even a count of zero's worth of real errors — for every smaller response.

Out-of-range slice bounds are now clamped to the array length, so $[:10] over a two-element array yields both elements.

By @carodewig in #9999

Fix query planner error on @requires when the key's conditions are fetched below the entity (PR #9926)

Planning a query could fail with the internal error Union types don't have field "<field>", only "__typename" when an entity's @requires had to be resolved through a nested @key whose own fields came from another subgraph. Concretely, this happens when the entity's key is nested (for example @key(fields: "subEntity { id2 }")) and id2 is only resolvable elsewhere, so the key-resolution conditions get fetched at a path deeper in the response (unionField.subEntity) than the entity that needs them (unionField).

In that situation the condition fetch sits below the key fetch, so there is no downward path from the former to the latter. Three places mishandled that:

  • compute_nodes_for_key_resolution() subtracted the two paths in the wrong direction, producing a path describing the reverse relation. That invalid path was then resolved against the entity fetch's _Entity union root, which raised the error above.
  • handle_conditions_tree() skipped its entire "merge into the grand parent" block when there was no path into the parent, silently dropping the condition fetch nodes it had just created instead of reporting them as created.
  • create_post_requires_node() assumed a path into the parent existed whenever there was a single parent, and aborted with Missing path_in_parent for @require otherwise.

The first defect masked the other two, which are just as damaging on their own: with only the second unfixed, planning succeeds but the resulting plan silently omits the fetch that resolves the field carrying the @requires, so that field comes back unresolved and no error is reported; with only the third unfixed, planning aborts outright. All three are fixed, and such queries now plan correctly, fetching the required field before the field that requires it.

By @dariuszkuc in #9926

🛠 Maintenance

Add caching to Query::apply_selection_set (PR #9592)

Adds fragment caching to Query::apply_selection_set to significantly reduce time spent formatting responses from operations with deeply nested fragments where the fragment is not spread on the root.

By @rohan-b99 in #9592

Don't miss a new router release

NewReleases is sending notifications on new releases.