{"record":{"id":"d40e9af434f7c2a3","repo":"symfony/http-kernel","slug":"controller-swap-loop-detected-while-dispatching-attributes","errorCode":null,"errorMessage":"Controller swap loop detected while dispatching attributes for event \"%s\"; a listener keeps changing the controller.","messagePattern":"Controller swap loop detected while dispatching attributes for event \"(.+?)\"; a listener keeps changing the controller\\.","errorType":"exception","errorClass":"LogicException","httpStatus":null,"severity":"error","filePath":"EventListener/ControllerAttributesListener.php","lineNumber":69,"sourceCode":"\n        dispatch_attributes:\n        foreach ($attributes as $attribute) {\n            if (!$attributeEventNames = $this->getAttributeEventNames($attribute, $eventName)) {\n                continue;\n            }\n\n            foreach ($attributeEventNames as $attributeEventName) {\n                $dispatcher->dispatch(new ControllerAttributeEvent($attribute, $event, $this->expressionLanguage), $attributeEventName);\n\n                if ($event->isPropagationStopped()) {\n                    return;\n                }\n            }\n\n            $c = $event->getController();\n            if ($c instanceof \\Closure ? $c != $controller : $c !== $controller) {\n                if (--$swapBudget < 0) {\n                    throw new \\LogicException(\\sprintf('Controller swap loop detected while dispatching attributes for event \"%s\"; a listener keeps changing the controller.', $eventName));\n                }\n                $controller = $c;\n                $attributes = $event->getAttributes('*');\n                goto dispatch_attributes;\n            }\n        }\n    }\n\n    public function afterController(KernelEvent $event, string $eventName, EventDispatcherInterface $dispatcher): void\n    {\n        $attributes = $event->controllerMetadata?->getAttributes('*') ?? [];\n\n        for ($i = \\count($attributes) - 1; $i >= 0; --$i) {\n            $attribute = $attributes[$i];\n            $attributeEventNames = $this->getAttributeEventNames($attribute, $eventName);\n\n            for ($j = \\count($attributeEventNames) - 1; $j >= 0; --$j) {\n                $dispatcher->dispatch(new ControllerAttributeEvent($attribute, $event, $this->expressionLanguage), $attributeEventNames[$j]);","sourceCodeStart":51,"sourceCodeEnd":87,"githubUrl":"https://github.com/symfony/http-kernel/blob/aa3a39d7286a62cdfea98f0e69c651a3da6e36cf/EventListener/ControllerAttributesListener.php#L51-L87","documentation":"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.","triggerScenarios":"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.","commonSituations":"Custom decorators/subcontrollers swapping controllers each dispatch; two listeners each overwriting the other's controller; debugging code that unconditionally calls setController.","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."],"exampleFix":"// before\npublic function onKernelController(ControllerEvent $e) { $e->setController(fn() => new Response()); }\n// after\npublic function onKernelController(ControllerEvent $e) {\n  if ($e->getRequest()->attributes->has('_swapped')) return;\n  $e->getRequest()->attributes->set('_swapped', true);\n  $e->setController(fn() => new Response());\n}","handlingStrategy":"validation","validationCode":"// audit listeners registered on kernel.controller / kernel.controller_arguments\nforeach ($dispatcher->getListeners('kernel.controller') as $l) { /* ensure none calls setController unconditionally */ }","typeGuard":null,"tryCatchPattern":"try { $kernel->handle($request); } catch (\\LogicException $e) { if (str_contains($e->getMessage(), 'swap loop')) { /* find offending listener via profiler */ } throw $e; }","preventionTips":["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"],"tags":["infinite-loop","kernel-controller","listeners","symfony"],"backgroundTag":"internal-invariant-violation","analyzedSha":"aa3a39d7286a62cdfea98f0e69c651a3da6e36cf","analyzedAt":"2026-09-13T18:03:36.509Z","contentChangedAt":"2026-09-13T18:03:36.509Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}