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 &mdash; 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 &mdash; 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 &mdash; change the backgroundImage prop's target bundles (e.g., single-bundle &rarr; 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> &rarr; ComponentSourceManager::updateComponentInstances()<br> // Auto-updates Component A (canUpdate()=TRUE) &rarr; wasModified=TRUE<br> // Leaves Component B alone (canUpdate()=FALSE)<br> &rarr; AutoSaveManager::saveEntity() // triggered because wasModified=TRUE<br> &rarr; normalizeEntity()<br> &rarr; ComponentTreeItem::optimizeInputs() // called on ALL items, including B<br> &rarr; Component::loadVersion($pinned_version)<br> &rarr; optimizeExplicitInput($inputs)<br> &rarr; getDefaultStaticPropSource($prop, FALSE)<br> &rarr; hasSameShapeAs($default)<br> // THROWS &mdash; Component B's stored shape &ne; new version's default shape</p> <h3> Root cause</h3> <p> AutoSaveManager::normalizeEntity() (~line 228):</p> <p> if ($item instanceof ComponentTreeItem) {<br> $item-&gt;optimizeInputs();<br> }</p> <p> This iterates every ComponentTreeItem in the entity. The auto-updater correctly skips Component B (shape changed &rarr; 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) &mdash; this triggers wasModified=TRUE &rarr; auto-save creation<br> 2. At least one other component instance cannot be auto-updated (canUpdate()=FALSE) &mdash; 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 &rarr; multi-bundle)</td> <td>Expression string differs &rarr; 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 &rarr; shape mismatch</td> </tr> <tr> <td><code>string_long</code> &rarr; <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 &mdash; 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 &mdash; 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 &mdash; 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