symfony/http-kernel · error · LogicException
Controller swap loop detected while dispatching attributes…
Error message
Controller swap loop detected while dispatching attributes for event "%s"; a listener keeps changing the controller.
What it means
ControllerAttributesListener::beforeController() allows listeners dispatched during attribute processing to swap the request controller, re-dispatching with the new controller under a finite swap budget. When swaps keep happening beyond the budget, it concludes a listener loop and throws a LogicException to prevent infinite recursion.
Solutions
- Make the swapping listener idempotent: only call setController() when the controller actually needs replacing.
- Guard with a request attribute flag so the swap happens at most once per request.
- Remove the controller-swapping listener if it is redundant.
Example fix
// before
public function onKernelController(ControllerEvent $e) { $e->setController(fn() => new Response()); }
// after
public function onKernelController(ControllerEvent $e) {
if ($e->getRequest()->attributes->has('_swapped')) return;
$e->getRequest()->attributes->set('_swapped', true);
$e->setController(fn() => new Response());
} Defensive patterns
Strategy: validation
Validate before calling
// audit listeners registered on kernel.controller / kernel.controller_arguments
foreach ($dispatcher->getListeners('kernel.controller') as $l) { /* ensure none calls setController unconditionally */ } Try / catch
try { $kernel->handle($request); } catch (\LogicException $e) { if (str_contains($e->getMessage(), 'swap loop')) { /* find offending listener via profiler */ } throw $e; } Prevention
- Only call setController when a change is actually needed
- Use a request-attribute sentinel to make swaps one-shot
- Review custom kernel.controller listeners after framework upgrades
When it happens
Trigger: A kernel.controller (or arguments) listener that repeatedly changes $event->setController() so the controller is different after each attribute dispatch round, e.g. a listener that sets a new closure or substitutes the controller on every invocation.
Common situations: Custom decorators/subcontrollers swapping controllers each dispatch; two listeners each overwriting the other's controller; debugging code that unconditionally calls setController.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- Request payload contains invalid "form" data.
- Unsupported format: " ".
- Request payload contains invalid
- Request payload contains invalid
- The uid for the " " parameter is invalid.
AI-assisted analysis of symfony/http-kernel@aa3a39d728 (2026-09-13).
Data as JSON: /api/errors/d40e9af434f7c2a3.
Report an issue: GitHub.
Appendix: source
Thrown at EventListener/ControllerAttributesListener.php:69
dispatch_attributes:
foreach ($attributes as $attribute) {
if (!$attributeEventNames = $this->getAttributeEventNames($attribute, $eventName)) {
continue;
}
foreach ($attributeEventNames as $attributeEventName) {
$dispatcher->dispatch(new ControllerAttributeEvent($attribute, $event, $this->expressionLanguage), $attributeEventName);
if ($event->isPropagationStopped()) {
return;
}
}
$c = $event->getController();
if ($c instanceof \Closure ? $c != $controller : $c !== $controller) {
if (--$swapBudget < 0) {
throw new \LogicException(\sprintf('Controller swap loop detected while dispatching attributes for event "%s"; a listener keeps changing the controller.', $eventName));
}
$controller = $c;
$attributes = $event->getAttributes('*');
goto dispatch_attributes;
}
}
}
public function afterController(KernelEvent $event, string $eventName, EventDispatcherInterface $dispatcher): void
{
$attributes = $event->controllerMetadata?->getAttributes('*') ?? [];
for ($i = \count($attributes) - 1; $i >= 0; --$i) {
$attribute = $attributes[$i];
$attributeEventNames = $this->getAttributeEventNames($attribute, $eventName);
for ($j = \count($attributeEventNames) - 1; $j >= 0; --$j) {
$dispatcher->dispatch(new ControllerAttributeEvent($attribute, $event, $this->expressionLanguage), $attributeEventNames[$j]);View on GitHub (pinned to aa3a39d728)