Overview

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
instances that were not modified by the auto-updater.

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.

This started to happen when I updated the site from Canvas 1.0.2 to Canvas 1.2.

Steps to reproduce

1. Create two JS components via Canvas UI/CLI:
- Component A: e.g., a simple card with a title (string) prop
- Component B: e.g., a hero with a backgroundImage (entity_reference targeting a single media bundle) and a body (string_long) prop
2. Create a Canvas page using instances of both Component A and Component B. Save the page.
3. Update Component A — add a new optional prop (e.g., subtitle). This is a safe change: canUpdate() will return TRUE for existing instances.
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.
5. Open the page in the Canvas editor (triggers GET /canvas/api/layout/{page_id}).

Proposed resolution

Result:

500 Internal Server Error with InvalidComponentInputsPropSourceException.

Expected:

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).

Error

InvalidComponentInputsPropSourceException: The shape of prop {prop_name} of component
{component_id} has the following shape: '{"sourceType":"static:field_item:entity_reference",
"expression":"...old..."}', but must match the default, which is:
'{"sourceType":"static:field_item:entity_reference","expression":"...new..."}'.

Thrown at GeneratedFieldExplicitInputUxComponentSourceBase::optimizeExplicitInput() (~line 1335).

Call chain

ApiLayoutController::buildRegion()
→ ComponentSourceManager::updateComponentInstances()
// Auto-updates Component A (canUpdate()=TRUE) → wasModified=TRUE
// Leaves Component B alone (canUpdate()=FALSE)
→ AutoSaveManager::saveEntity() // triggered because wasModified=TRUE
→ normalizeEntity()
→ ComponentTreeItem::optimizeInputs() // called on ALL items, including B
→ Component::loadVersion($pinned_version)
→ optimizeExplicitInput($inputs)
→ getDefaultStaticPropSource($prop, FALSE)
→ hasSameShapeAs($default)
// THROWS — Component B's stored shape ≠ new version's default shape

Root cause

AutoSaveManager::normalizeEntity() (~line 228):

if ($item instanceof ComponentTreeItem) {
$item->optimizeInputs();
}

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
with the old version's shapes, optimizeExplicitInput() compares them against the new version's defaults and throws.

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.

Key condition

This only crashes when both of the following are true on the same page:

1. At least one component instance can be auto-updated (canUpdate()=TRUE) — this triggers wasModified=TRUE → auto-save creation
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

If all instances on a page can be auto-updated, or none can, the crash does not occur.

Concrete shape changes that trigger this

Prop type Change Effect
entity_reference Target bundles changed (e.g., single-bundle → multi-bundle) Expression string differs → shape mismatch
link link_type added/removed from field storage settings sourceTypeSettings differs → shape mismatch
string_longtext_long Field type changed entirely sourceType and expression both differ

In all cases, canUpdate() correctly returns FALSE for the affected instances. The crash only happens because normalizeEntity() forces validation on them anyway.

Suggested fix

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.

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
that item, which is acceptable since the item wasn't modified.

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
at all.

Comments

amangrover90 created an issue. See original summary.

wim leers’s picture

Root cause is the stale prop source?

A single item with a stale prop source shape

Isn't that the actual root cause then? 🤔

(What the auto-save system does may be suboptimal: it's slower than it could be, but it shouldn't crash for reasons outside the auto-save system.)

And: how can that even occur?

Timeline: this has been impossible since long before 1.0-beta

Canvas timeline for related key changes:

  • #3538487: Don't allow passing uncollapsed inputs if using default expression started enforcing collapsed inputs in August 2025 (change record), long before the alpha in September and the 1.0 in December.
  • The UI never allowed a Canvas user to deviate from what the default field type+expression+… anyway.
  • To be fair, the update path (see CollapseComponentInputsUpdateTest) only updates the config entities, not the content entities. That was intentional. Because we were not even in alpha!

(Update paths/data integrity is only promised since beta.)

Cross-check

The epic wall of text (much appreciated 👏, gives me enough info to connect dots! 😊🙏) seems to imply places that this requires steps impossible since 1.0 (and actually much earlier):

  1. 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.

    This seems to be talking about the Component config entity, but I suspect you're actually referring to the component instance?

    If it were referring to the Component config entity, then the component version referenced by the component instance would mean that the shapes do stay in sync.

    So that makes me think you're talking about an uncollapsed prop source for this input, and modifying that, directly in the canvas_page__components table's component_inputs column?

  2. The InvalidComponentInputsPropSourceException exception you mention can also only be thrown by GeneratedFieldExplicitInputUxComponentSourceBase::optimizeExplicitInput(), and only in the case if >=1 prop's input does NOT have a collapsed value — i.e. it can only happen if it contains an entire { "sourceType": … } array.
  3. Thrown at GeneratedFieldExplicitInputUxComponentSourceBase::optimizeExplicitInput() (~line 1335). confirms that too.

Correct?

Are my deductions correct?

  1. If not: I don't see how this can be reproduced using 1.0-beta or later.
  2. If yes: I don't think there's something to fix here — old Canvas-created data should either be deleted or manually fixed. 😅🙈 It should be possible to write a script to auto-fix some of these, and a simple SELECT * FROM canvas_page__components WHERE components_inputs LIKE '%"sourceType":"static%' would surface all affected rows.
wim leers’s picture

Status: Active » Postponed (maintainer needs more info)

Not actionable per #2.