AutoSaveManager::normalizeEntity() crashes layout API when page contains component instances with stale prop shapes
>>> [!note] Migrated issue
<!-- Drupal.org comment -->
<!-- Migrated from issue #3579086. -->
Reported by: [amangrover90](https://www.drupal.org/user/3602433)
>>>
<h3 id="overview">Overview</h3>
<p>When a component's active version is updated (via the Canvas UI or CLI) and the new version introduces changed prop shapes, the layout API crashes with InvalidComponentInputsPropSourceException during auto-save creation — even for component<br>
instances that were not modified by the auto-updater.</p>
<p> The root cause is that AutoSaveManager::normalizeEntity() calls optimizeInputs() on all component tree items in the entity, not just the ones that were auto-updated. A single item with a stale prop source shape poisons the entire auto-save.</p>
<p>This started to happen when I updated the site from Canvas 1.0.2 to Canvas 1.2.</p>
<h3 id="steps-to-reproduce">Steps to reproduce</h3>
<p>1. Create two JS components via Canvas UI/CLI:<br>
- Component A: e.g., a simple card with a title (string) prop<br>
- Component B: e.g., a hero with a backgroundImage (entity_reference targeting a single media bundle) and a body (string_long) prop<br>
2. Create a Canvas page using instances of both Component A and Component B. Save the page.<br>
3. Update Component A — add a new optional prop (e.g., subtitle). This is a safe change: canUpdate() will return TRUE for existing instances.<br>
4. Update Component B — change the backgroundImage prop's target bundles (e.g., single-bundle → multi-bundle), or change body from string_long to text_long. This is an unsafe shape change: canUpdate() will return FALSE.<br>
5. Open the page in the Canvas editor (triggers GET /canvas/api/layout/{page_id}).</p>
<h3 id="proposed-resolution">Proposed resolution</h3>
<h3>Result:</h3>
<p> 500 Internal Server Error with InvalidComponentInputsPropSourceException.</p>
<h3>Expected:</h3>
<p> The page loads. Component A instances are auto-updated to the new active version. Component B instances stay at their pinned version (since canUpdate() correctly returns FALSE due to the shape change).</p>
<h3> Error </h3>
<p> InvalidComponentInputsPropSourceException: The shape of prop {prop_name} of component<br>
{component_id} has the following shape: '{"sourceType":"static:field_item:entity_reference",<br>
"expression":"...old..."}', but must match the default, which is:<br>
'{"sourceType":"static:field_item:entity_reference","expression":"...new..."}'.</p>
<p> Thrown at GeneratedFieldExplicitInputUxComponentSourceBase::optimizeExplicitInput() (~line 1335).</p>
<h3> Call chain</h3>
<p> ApiLayoutController::buildRegion()<br>
→ ComponentSourceManager::updateComponentInstances()<br>
// Auto-updates Component A (canUpdate()=TRUE) → wasModified=TRUE<br>
// Leaves Component B alone (canUpdate()=FALSE)<br>
→ AutoSaveManager::saveEntity() // triggered because wasModified=TRUE<br>
→ normalizeEntity()<br>
→ ComponentTreeItem::optimizeInputs() // called on ALL items, including B<br>
→ Component::loadVersion($pinned_version)<br>
→ optimizeExplicitInput($inputs)<br>
→ getDefaultStaticPropSource($prop, FALSE)<br>
→ hasSameShapeAs($default)<br>
// THROWS — Component B's stored shape ≠ new version's default shape</p>
<h3> Root cause</h3>
<p> AutoSaveManager::normalizeEntity() (~line 228):</p>
<p> if ($item instanceof ComponentTreeItem) {<br>
$item->optimizeInputs();<br>
}</p>
<p> This iterates every ComponentTreeItem in the entity. The auto-updater correctly skips Component B (shape changed → canUpdate()=FALSE), but normalizeEntity() still calls optimizeInputs() on it. Since Component B's stored inputs were written<br>
with the old version's shapes, optimizeExplicitInput() compares them against the new version's defaults and throws.</p>
<p> The auto-updater does the right thing. The problem is that normalizeEntity() doesn't respect the updater's decision and validates items the updater intentionally left alone.</p>
<h3> Key condition</h3>
<p> This only crashes when both of the following are true on the same page:</p>
<p> 1. At least one component instance can be auto-updated (canUpdate()=TRUE) — this triggers wasModified=TRUE → auto-save creation<br>
2. At least one other component instance cannot be auto-updated (canUpdate()=FALSE) — its stored shapes are stale but the auto-updater intentionally left it alone</p>
<p> If all instances on a page can be auto-updated, or none can, the crash does not occur.</p>
<h3>Concrete shape changes that trigger this</h3>
<table>
<tr>
<th>Prop type</th>
<th>Change</th>
<th>Effect</th>
</tr>
<tr>
<td><code>entity_reference</code></td>
<td>Target bundles changed (e.g., single-bundle → multi-bundle)</td>
<td>Expression string differs → shape mismatch</td>
</tr>
<tr>
<td><code>link</code></td>
<td><code>link_type</code> added/removed from field storage settings</td>
<td><code>sourceTypeSettings</code> differs → shape mismatch</td>
</tr>
<tr>
<td><code>string_long</code> → <code>text_long</code></td>
<td>Field type changed entirely</td>
<td><code>sourceType</code> and <code>expression</code> both differ</td>
</tr>
</table>
<p> In all cases, canUpdate() correctly returns FALSE for the affected instances. The crash only happens because normalizeEntity() forces validation on them anyway.</p>
<h3> Suggested fix</h3>
<p> Option A — Scope optimizeInputs() to modified items only: normalizeEntity() should only call optimizeInputs() on items that were actually modified by updateComponentInstances(), or accept a list of modified item UUIDs to limit the scope.</p>
<p> Option B — Catch and log instead of crashing: Wrap the optimizeInputs() call in normalizeEntity() with a try/catch for InvalidComponentInputsPropSourceException, log a warning, and continue. The auto-save will contain the un-optimized data for<br>
that item, which is acceptable since the item wasn't modified.</p>
<p> Option C — Skip optimizeInputs() in auto-save normalization entirely: Since auto-save is a transient store and optimizeInputs() already runs on preSave() when the entity is actually persisted, it may not need to run during auto-save creation<br>
at all.</p>
> Related issue: [Issue #3538487](https://www.drupal.org/node/3538487)
issue