Livewire 4.x Pull Request Changelog: PR #10510 - Fix #[Authorize] Attribute to Trigger Component exception() Hook
Contributed by ghabriel25
Part of the livewire v4.4.7 release
Livewire PR #10510 fixes a subtle but important gap in the #[Authorize] attribute on the 4.x branch: authorization failures thrown before your action runs never reached the component's exception() hook. The fix routes attribute-based authorization through the same exception-handling wrapper as normal method calls, so your custom error handling now behaves consistently. The PR also adds two regression tests and a small reflection cleanup. If you're upgrading from v3.8.10 into the 4.x line, you get this behavior out of the box — no action required, though a few workarounds you may have written elsewhere can now be deleted.
The bug: authorization exceptions skipped the exception() hook
The #[Authorize] attribute performs a gate or policy check before your action method executes. Internally, that check runs through SupportAttributes::call(), which fires ahead of the method call and outside the wrapper pipeline that powers component exception hooks. When authorization failed, the resulting AuthorizationException was thrown before Livewire could hand it to your component.
In practice, code like this never reached the hook:
class PostPolicy
{
public function edit(User $user, Post $post): bool
{
return false;
}
}
use Illuminate\Auth\Access\AuthorizationException;
use Livewire\Attributes\Authorize;
use Livewire\Component;
class PostEditor extends Component
{
#[Authorize('edit', 'post')]
public function save(Post $post)
{
// Only reached when authorized.
}
public function exception($e, $stopPropagation)
{
if ($e instanceof AuthorizationException) {
$stopPropagation();
$this->dispatch('notify', message: 'You cannot edit this post.');
}
}
}
The exception() method was simply never invoked; the request fell through to the default forbidden handling instead. That inconsistency is what issue #10410 reported.
The fix: authorize inside the wrapper
Rather than restructure the attribute system, BaseAuthorize::call() now wraps the component and runs the authorization check inside a tap() callback — the same technique BaseValidate already uses:
use function Livewire\wrap;
public function call(array $parameters): void
{
wrap($this->component)->tap(function () use ($parameters) {
// Gate/policy check; may throw.
});
}
Because the check now executes inside wrap(), anything it throws flows through Livewire's Wrapper class — the exact place where the exception() hook is invoked. This approach replaces the simpler patch attempted in PR #10483 with one that matches how the rest of the framework handles attribute-time exceptions.
Tests and cleanup included
Two regression tests lock in the behavior:
test_authorize_attribute_trigger_component_exception_hookasserts the hook fires on a failed attribute check, and that calling$stopPropagation()lets the request complete OK instead of forbidden.test_can_authorize_using_attribute_and_trait_method_at_the_same_timeconfirms you can still mix#[Authorize]with$this->authorize()calls inside the method body, and that the hook stays silent when authorization passes.
The diff also swaps ReflectionObject for ReflectionMethod when matching named arguments (like 'post') to method parameters. Reflecting on the method directly has fewer layers than walking the whole object, and resolveArgument() drops its now-redundant $parameters argument since the lazy dependency-resolution closure already captures it. Both are internal details with no public-facing effect.
Upgrade impact
There is no API change: #[Authorize('ability', $argument)] works exactly as documented. The change is behavioral, in one direction:
- If you never noticed the bug, there is nothing to do — hooks now fire as you'd expect.
- If you worked around it — for example, calling
$this->authorize()manually inside actions so exceptions hit the hook, or handlingAuthorizationExceptionin a global handler — you can move that logic intoexception()and simplify. - If you relied on the hook not firing for attribute-based failures (unlikely, but possible if a component has an
exception()hook and you wanted the default 403 response), review that component: intercepting in the hook now takes precedence, just as it does for every other exception.
For teams coming from v3.8.10, the #[Authorize] attribute itself is part of what you're adopting in 4.x; this fix means its error handling is consistent from day one.
Takeaways
#[Authorize]failures now trigger the component'sexception()hook, consistent with all other exceptions.- Implemented by wrapping authorization in
wrap($component)->tap(...), the same pattern validation attributes use. - No public API changes; internal reflection streamlined (
ReflectionMethodoverReflectionObject). - Regression tests cover both the hook firing and attribute/trait authorization coexisting.