You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
collect_subfields memoization keyed on id() of FieldDetails that are not kept alive returns another field's subfields #284
In 3.3.0, Executor.collect_subfields memoizes per execution in self._relevant_sub_fields, keyed on the return type and the id() of each FieldDetails in the list:
The cache keeps the collected subfields as values, but nothing keeps the FieldDetails of the key alive. Once a field has finished, its FieldDetails (a 3-tuple) can be freed. CPython then often reuses that address for the next tuple of the same size, and if that tuple is the FieldDetails of another field returning the same object type, the lookup hits the stale entry: the second field is answered with the first field's selection set.
We saw it intermittently (three times in a few hundred full runs) with queries like:
where every edges.node came back with only name, the selection of item. A client can thus receive fields it did not ask for and miss the ones it did.
Deterministic reproduction on CPython (freeing one FieldDetails and building the next, which CPython's allocator places at the same address; a gc.collect() in between allocates and hides it):
fromgraphqlimportGraphQLField, GraphQLObjectType, GraphQLSchema, GraphQLString, parsefromgraphql.execution.collect_fieldsimportFieldDetailsfromgraphql.execution.executorimportExecutoritem=GraphQLObjectType("Item", {"key": GraphQLField(GraphQLString), "name": GraphQLField(GraphQLString)})
schema=GraphQLSchema(GraphQLObjectType("Query", {"a": GraphQLField(item), "b": GraphQLField(item)}))
document=parse("{ a { key } b { name } }")
executor=Executor.build(schema, document)
a_node, b_node=document.definitions[0].selection_set.selectionsfirst= [FieldDetails(a_node, None)]
executor.collect_subfields(item, first)
address=id(first[0])
delfirstsecond= [FieldDetails(b_node, None)]
assertid(second[0]) ==address# usually true on CPythoncollected=executor.collect_subfields(item, second)
print(list(collected.grouped_field_set)) # ['key'] — expected ['name']
This looks related to #91. The comment above the key notes that list ids may be reused, but the same holds for the ids of the items once they are freed.
Possible fixes: keep the key's FieldDetails alive with the cached value (for example store (field_details_list, collected_fields) and compare by identity on hit), or key on the FieldNodes (id(fd.node)), which live as long as the document, plus whatever else of FieldDetails affects the collection (defer_usage, fragment_variable_values).
Our workaround meanwhile is an execution context whose collect_subfields keeps every field_details_list it is given referenced until the operation ends.
Thanks a lot for this excellent analysis and the deterministic reproduction, @abraira85. I can confirm it. This is a regression in 3.3: 3.2 used the ids of the FieldNodes, which live as long as the document, but 3.3 uses the ids of the FieldDetails, which can be freed during execution. So 3.2 is not affected.
And yes, this is related to #91. Back then, the cache used the id of the list, and that id could be reused. The fix for #91 switched to the ids of the FieldNodes, which was safe because they live as long as the document. That assumption silently broke when 3.3 replaced them with the short-lived FieldDetails.
The fix is in progress in #285. We will memoize on the identity of the field_details_list, like GraphQL.js does. This time, though, the list is kept alive in the cache entry, so its id cannot be reused while the operation is executing, which was the actual problem in #91. Your workaround is fine until then. The fix will be released with 3.3.1.
Activity
Work around a bug in graphql-core 3.3.0
Cito commentedon Oct 19, 2026
Thanks a lot for this excellent analysis and the deterministic reproduction, @abraira85. I can confirm it. This is a regression in 3.3: 3.2 used the ids of the
FieldNodes, which live as long as the document, but 3.3 uses the ids of theFieldDetails, which can be freed during execution. So 3.2 is not affected.And yes, this is related to #91. Back then, the cache used the id of the list, and that id could be reused. The fix for #91 switched to the ids of the
FieldNodes, which was safe because they live as long as the document. That assumption silently broke when 3.3 replaced them with the short-livedFieldDetails.The fix is in progress in #285. We will memoize on the identity of the
field_details_list, like GraphQL.js does. This time, though, the list is kept alive in the cache entry, so its id cannot be reused while the operation is executing, which was the actual problem in #91. Your workaround is fine until then. The fix will be released with 3.3.1.