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

  1. Make the swapping listener idempotent: only call setController() when the controller actually needs replacing.
  2. Guard with a request attribute flag so the swap happens at most once per request.
  3. 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

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


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)