What happened?
Description
After upgrading from Craft 5.9.17 to 5.9.18+, we found a GraphQL regression where populated Matrix content can disappear from an otherwise successful response.
In Craft 5.9.18 and 5.9.19, a polymorphic query can return an empty array for a Matrix field with HTTP 200 and no GraphQL errors when a few conditions line up:
- the query uses named fragments for concrete entry types
- multiple fragments alias different real Matrix field handles to the same response key
- one of the non-matching fragments includes a nested
... on EntryInterface selection
- another fragment shares the matching entry’s real Matrix handle
When that happens, the aliased Matrix field comes back as [], even though the entry is populated and the rest of the response is correct.
This worked in Craft 5.9.17, so it appears to be a regression introduced in 5.9.18 as part of the fix for #18588, specifically the change to $plan->when in src/gql/ElementQueryConditionBuilder.php in commit 63a7d32.
Steps to reproduce
-
Set up three entry types with populated Matrix content:
- two types sharing the same Matrix field handle
- one type using a different Matrix field handle
For example:
landingPage_Entry → contentBuilderPage
standardPage_Entry → contentBuilderPage
insightPost_Entry → contentBuilderInsight
- Run this query against a
landingPage_Entry entry with populated contentBuilderPage content:
fragment FRAG_SECTION_DEFAULT on pageSectionStandardPage_Entry {
uid
contentBuilderBlocks {
... on contentBuilderStandardText_Entry { uid typeHandle heading }
}
}
fragment FRAG_SECTION_INSIGHT on pageSectionInsight_Entry {
uid
contentBuilderBlocksInsight {
... on contentBuilderStandardText_Entry { uid typeHandle heading }
}
}
fragment FRAG_LANDING_PAGE on landingPage_Entry {
uid
typeHandle
contentBuilder: contentBuilderPage {
...FRAG_SECTION_DEFAULT
}
}
fragment FRAG_STANDARD_PAGE on standardPage_Entry {
uid
typeHandle
contentBuilder: contentBuilderPage {
...FRAG_SECTION_DEFAULT
}
}
fragment FRAG_INSIGHT on insightPost_Entry {
uid
typeHandle
... on EntryInterface { id slug uri }
contentBuilder: contentBuilderInsight {
...FRAG_SECTION_INSIGHT
}
}
query Test($uri: [String], $site: [String]) {
entry(
section: ["page", "report", "article", "pressRelease", "webinar"]
site: $site
uri: $uri
) {
...FRAG_LANDING_PAGE
...FRAG_STANDARD_PAGE
...FRAG_INSIGHT
}
}
- Observe that
data.entry.contentBuilder is returned as [].
Expected behaviour
For a landingPage_Entry, data.entry.contentBuilder should contain the Matrix blocks from contentBuilderPage, as it does in Craft 5.9.17.
Actual behaviour
data.entry.contentBuilder is returned as an empty array:
{
"data": {
"entry": {
"typeHandle": "landingPage",
"contentBuilder": []
}
}
}
The response still returns HTTP 200, errors is null, and other fields such as uid, title, etc. are populated correctly.
What we tested
We narrowed the bug down to a specific combination of conditions:
- Named fragments are required - switching to inline fragments avoids the issue
- Alias collision is required - giving each fragment a unique alias avoids the issue
- A non-matching fragment with
EntryInterface is required - adding EntryInterface only to the matching fragment does not trigger it
- Two fragments sharing the same real handle are required - a simple two-fragment different-handle case does not fail
We also confirmed:
- this is not data-specific - different entries and URIs show the same behaviour
- it reproduces with plain
EntryInterface scalar fields like id, slug, and uri
- so far we have only verified it against Matrix fields
Version matrix
| Craft version |
Result |
| 5.9.17 |
populated |
| 5.9.18 |
empty array |
| 5.9.19 |
empty array |
5.9.19 + revert of $plan->when closure |
populated |
Bisection
The relevant change is in src/gql/ElementQueryConditionBuilder.php:
// 5.9.17
$plan->when = fn(Element $element) =>
$element->getGqlTypeName() === $wrappingFragment->typeCondition->name->value;
// 5.9.18+
$plan->when = function(Element $element) use ($wrappingFragment) {
$typeName = $wrappingFragment->typeCondition->name->value;
if (preg_match('/^(\w+)Interface$/', $typeName, $match)) {
return str_ends_with($element->getGqlTypeName(), "_{$match[1]}");
}
return $element->getGqlTypeName() === $typeName;
};
Reverting only this closure to the 5.9.17 form on a stock 5.9.19 install restores the expected Matrix content.
We have not fully traced the downstream eager-load path, but our current suspicion is that the interface-matching branch changes how eager-load plans are resolved when multiple concrete-type fragments share the same response alias.
We also confirmed that our own interface-fragment use case still works after reverting this closure, though we did not re-run the original #18588 reproduction exactly.
Environment
- Craft CMS: 5.9.18, 5.9.19
- PHP: 8.3.x
- MySQL: 8.0
- OS: macOS + Docker
Workarounds
These all avoid the issue:
-
Do not alias multiple fragment fields to the same response key
-
Use separate per-type queries
-
Use unique aliases per fragment, for example:
landingBlocks: contentBuilderPage
insightBlocks: contentBuilderInsight
Craft CMS version
5.9.18
PHP version
8.3
Operating system and version
MacOS + Docker & Ubuntu
Database type and version
MySQL 8.0
Image driver and version
N/a
Installed plugins and versions
What happened?
Description
After upgrading from Craft 5.9.17 to 5.9.18+, we found a GraphQL regression where populated Matrix content can disappear from an otherwise successful response.
In Craft 5.9.18 and 5.9.19, a polymorphic query can return an empty array for a Matrix field with HTTP 200 and no GraphQL errors when a few conditions line up:
... on EntryInterfaceselectionWhen that happens, the aliased Matrix field comes back as
[], even though the entry is populated and the rest of the response is correct.This worked in Craft 5.9.17, so it appears to be a regression introduced in 5.9.18 as part of the fix for #18588, specifically the change to
$plan->wheninsrc/gql/ElementQueryConditionBuilder.phpin commit63a7d32.Steps to reproduce
Set up three entry types with populated Matrix content:
For example:
landingPage_Entry→contentBuilderPagestandardPage_Entry→contentBuilderPageinsightPost_Entry→contentBuilderInsightlandingPage_Entryentry with populatedcontentBuilderPagecontent:data.entry.contentBuilderis returned as[].Expected behaviour
For a
landingPage_Entry,data.entry.contentBuildershould contain the Matrix blocks fromcontentBuilderPage, as it does in Craft 5.9.17.Actual behaviour
data.entry.contentBuilderis returned as an empty array:{ "data": { "entry": { "typeHandle": "landingPage", "contentBuilder": [] } } }The response still returns HTTP 200,
errorsisnull, and other fields such asuid,title, etc. are populated correctly.What we tested
We narrowed the bug down to a specific combination of conditions:
EntryInterfaceis required - addingEntryInterfaceonly to the matching fragment does not trigger itWe also confirmed:
EntryInterfacescalar fields likeid,slug, anduriVersion matrix
$plan->whenclosureBisection
The relevant change is in
src/gql/ElementQueryConditionBuilder.php:Reverting only this closure to the 5.9.17 form on a stock 5.9.19 install restores the expected Matrix content.
We have not fully traced the downstream eager-load path, but our current suspicion is that the interface-matching branch changes how eager-load plans are resolved when multiple concrete-type fragments share the same response alias.
We also confirmed that our own interface-fragment use case still works after reverting this closure, though we did not re-run the original #18588 reproduction exactly.
Environment
Workarounds
These all avoid the issue:
Do not alias multiple fragment fields to the same response key
Use separate per-type queries
Use unique aliases per fragment, for example:
landingBlocks: contentBuilderPageinsightBlocks: contentBuilderInsightCraft CMS version
5.9.18
PHP version
8.3
Operating system and version
MacOS + Docker & Ubuntu
Database type and version
MySQL 8.0
Image driver and version
N/a
Installed plugins and versions