Repository navigation
planner: reserve a variable name for dependent step node IDs - #17
Merged
Merged
Conversation
Coverage Report for CI Build 33768000178Coverage increased (+0.07%) to 91.352%Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
wutsch0
approved these changes
Sep 3, 2026
A query whose fields span two services is split into a plan where the
second step re-enters the remote service through node(id: ...), once per
object the first step returned. The planner declared that argument's
variable as $id, and only when the client had not already declared one
by that name; otherwise it reused the client's declaration. The executor
then assigned the node ID under the same name on every dependent step.
$id is an ordinary name for a client to choose, so the two collided.
Given:
query ($id: ID!, $category: String!) {
allUsers {
favoriteCatPhoto(category: $category, owner: $id) { URL }
}
}
where favoriteCatPhoto is owned by a second service, the dependent step
ran once per user with $id set to that user's ID. The owner argument,
which the client meant to pin to a single ID, was silently rewritten to
u1, then u2, then u3. Nothing surfaced an error: the query is valid and
both node(id:) and owner take an ID, so every request succeeded and
returned the wrong photos.
Reusing the client's declaration also inherited its type. A client
declaring $id as String! and using it for a String! argument, which is
legal on its own terms, produced node(id: $id) against node(id: ID!) and
the remote service rejected the entire step.
Give the gateway a name of its own, _gateway_node_id. Since it can never
be one of the client's variables, declare it unconditionally rather than
conditionally reusing whatever is there, and reject operations that
declare it instead of overwriting them silently.
cideM
force-pushed
the
fix/node-id-variable-collision
branch
from
September 3, 2026 14:37
ce3a182 to
f4eed97
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is a bug fix for something I discovered while working on other features. Definitely something I'd want to upstream.
EDIT: I created a PR upstream as well nautilus#238
Summary
Fix bug: when a query spanning two services declared a variable named
$idand also used it as a field argument in the second service, every dependentnode(id:)step overwrote the client's value with the ID of the object being resolved. The client's$idnow keeps its own value.The ID variable of dependent step queries is now named
_gateway_node_idinstead ofid.Any client query that declares a variable
_gateway_node_idis rejected.Longer Explanation
I believe that there is a bug hiding in the way dependent steps are planned and executed.
Let's take the query below:
favoriteCatPhotois owned by a second service. The idea behind the request is thatowner: $idacts like a filter, so we only get cat photos where the owner is set to one specific ID (keep that in mind).At runtime, the above query results in one query to the service that owns
allUsersand from that we generate multiple, dependent steps, one for each user. Those dependent steps are sent to the second service in the form ofnode(id: ...)queries.Those dependent step queries obviously need a user ID. What the code currently does is it checks if the query, as sent by the client, already has an
idvariable. If so, this variable is re-used.And on the execution side, the node ID is written into the variables under that same name for every dependent step:
In this case, this means that the existing
idvariable will be populated with each user's ID.In the end, we generate the following query:
Both
node(id: $id)andowner: $idnow read from the same variable, and the executor sets it tou1, thenu2, thenu3, one dependent step per user.The problem here is that the
$idvariable is now ambiguous as to what it does: originally it was meant to be a single user ID, for the filtering, but now it also carries the meaning of "each user's ID". As a result, the client gets back the wrong result.There's a second error mode: if the client declares
$id: String!, the reused declaration keeps the client'sString!type, so the remote rejectsnode(id: $id)againstnode(id: ID!).My proposed fix is to use a variable name that's unlikely to conflict, like
_gateway_node_id(TestPlanQuery_clientIDVariableSurvivesNodeStep.). Also add a check that the client query does not already use that variable (TestPlanQuery_rejectsReservedNodeIDVariable).