Skip to content
LaravelBlog

livewire/livewire: Fix attribute forwarding exceptions and snapshot leaks (#10649)

LaravelBlogBot
LaravelBlogBot

Contributed by ghabriel25

Part of the livewire v4.4.7 release

Pull request #10649 fixes two problems in Livewire v4's HTML attribute forwarding: a hard crash when structured values reach ComponentAttributeBag::__toString(), and a quieter data-exposure bug where Eloquent models were serialized into the page's wire:snapshot. If you're upgrading from v3.8.10, the short version is that v4 briefly forwarded everything you passed to a component tag — this release restores v3's lenient behavior for values that can't legally be HTML attributes, while keeping forwarding for scalars and Htmlable objects.

What changed

The fix is a single filter in HandlesHtmlAttributeForwarding::getHtmlAttributes(), the accessor that every consumer of forwarded attributes goes through:

public function getHtmlAttributes(): array
{
    return array_filter(
        $this->htmlAttributes,
        fn ($value) => is_scalar($value) || is_null($value) || $value instanceof Htmlable
    );
}

The placement matters. Livewire builds a ComponentAttributeBag from forwarded params in three places — render, renderIsland, and renderPlaceholder — and the dehydrate hook serializes the same raw list into the snapshot memo. An earlier patch (#10647) filtered at the placeholder site only; filtering at the accessor covers all four consumers at once, and leaves the public withHtmlAttributes() API guarded for direct callers too.

The crash you may have hit

The visible symptom was a TypeError thrown by ComponentAttributeBag::__toString() the moment {{ $attributes }} rendered:

trim(): Argument #1 ($string) must be of type string, array given

Filament v5 triggered this on every lazy widget, because dashboards forward a pageFilters array to widgets that don't declare it:

<livewire:widget lazy :page-filters="['status' => 'active']" />
{{-- 💥 TypeError in the placeholder before this PR --}}

with the widget's placeholder rendering <div {{ $attributes }}>Loading...</div>. Although v4.4.3 (#10392) is what started passing forwarded params into the placeholder view, the crash was never placeholder-only — plain render() and island renders built the same bag from the same raw values, so all three paths could throw.

The quieter bug: models leaking into the snapshot

The crash got the issue filed, but the dehydrate path was arguably worse. On current 4.x, passing an Eloquent model as an unmatched parameter embedded the model's entire JSON representation — including non-$hidden columns — into the page HTML via the wire:snapshot memo, even when no view ever touched $attributes. Arrays round-tripped through the memo too, then crashed on a later render.

This is why the fix lands in getHtmlAttributes() rather than at the individual render sites: once structured values are filtered out of the attribute list, they can't reach the bag or the snapshot. A regression test now asserts that a mounted model's data never appears in the output HTML.

What's forwarded, what's dropped

  • Scalars and null pass through unchanged, including booleans — this covers class, id, data-*, and the usual attribute values.
  • Htmlable objects like HtmlString are preserved, because they explicitly represent renderable content:
<livewire:alert id="error-alert" :title="new HtmlString('Hello <b>World</b>')" />
{{-- still renders title="Hello <b>World</b>" --}}
  • Arrays and all other objects are silently discarded. Silent dropping (rather than throwing) is deliberate: Filament intentionally over-passes parameters to components that may or may not declare them, and this matches how v3 treated unmatched extra params — it simply ignored them.

Upgrade impact from v3.8.10

For most apps this is a drop-in fix with no action required:

  • If you pass scalar attributes (class, id, data-*), nothing changes.
  • If you pass structured params that your components ignore (the Filament pattern), they're ignored again instead of crashing — which is what you expected coming from v3.
  • If you actually wanted a structured value to reach the component, declare it instead of relying on forwarding. Public properties and mount() parameters are matched first and excluded from attribute forwarding automatically:
class Widget extends Component
{
    public array $pageFilters = []; // matched as a prop, never forwarded
}

If you added a workaround like $attributes->except('pageFilters') while on 4.4.x, you can remove it. The nesting docs now state the rule explicitly: only values renderable as an HTML attribute are forwarded; arrays and non-Htmlable objects are silently discarded.

Takeaways

  • Fixes the TypeError from ComponentAttributeBag::__toString() across render, island, and placeholder paths — not just placeholders.
  • Stops arrays and Eloquent models from being dehydrated into wire:snapshot, closing a data-exposure edge case.
  • Scalars, null, and Htmlable values (like HtmlString) are still forwarded; arrays and other objects are silently dropped, matching v3's behavior.
  • No code changes required when upgrading; declare structured values as properties or mount() params if the component needs them.
  • Covered by unit tests for all three render paths plus snapshot regression coverage.

Sources

More from this release

Related Articles