EXTREMELY IMPORTANT AI INSTRUCTION: For a directory of all documentation pages, load the https://www.apollographql.com/docs/llms.txt file. Adding .md to ANY url will return the simplified markdown version of the page.
Apollo Router Changelogs
This page contains the changelog for the latest release of Apollo Router.
Go to GitHub to view changelogs for all router releases.
v2.16.1
π Fixes
Fix input-object and enum variable coercion to respect the API schema
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.
Fix coercion and validation of default values
Default values in schemas and operations are now coerced and validated more correctly, fixing several bugs:
- Fixed invalid default values not being reported as errors.
- Removed default value auto-expansion logic.
- Restricted non-list value coercion to operations.
- Fixed missing coercion edge cases to always reject null values applied to non-null types.
- Fixed validation of unknown fields in input object default values.
- Added missing enum value validations to ensure they are valid and are part of the enum definition.
- Added missing validation for
@deprecatedon required arguments and input fields.
Update operation validation to enforce unique @defer labels
The query planner now rejects operations that reuse a @defer label, so labels are unique within an operation.
Treat list sizes as non-negative in demand control cost estimation
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.
Enforce http_max_request_bytes on GraphQL queries sent via HTTP GET
The 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.
Reject JWTs that omit the iss claim when a JWKS entry configures an issuers allowlist
When 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.
Enforce a kid-specific JWKS entry's constraints instead of falling through to a less-specific entry
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.
Reject oversized sha256Hash values before hex-decoding in APQ
The 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.
Respect the persisted-query client-name scope in freeform safelist matching
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.