A Symfony route marked stateless: true promises that handling the request will not use the session. Rendering a controller fragment that reads app.user or calls is_granted() inside that request breaks the promise. Give the personalized fragment its own route and render its URL through ESI.
Last updated: November 14, 2026.
<?php
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentRoutingAttributeRoute;
use SymfonyBundleFrameworkBundleControllerAbstractController;
final class CatalogController extends AbstractController
{
#[Route('/catalog', name: 'catalog', stateless: true)]
public function catalog(): Response
{
return $this->render('catalog/index.html.twig');
}
// Deliberately not stateless: this request may read the session.
#[Route('/_fragment/account-menu', name: 'account_menu')]
public function accountMenu(): Response
{
return $this->render('fragments/account_menu.html.twig');
}
}
// catalog/index.html.twig
// {{ render_esi(url('account_menu')) }}Use url() so the surrogate receives a real fragment URL. Do not use controller() for a fragment that needs the authenticated user inside a stateless parent request.
Keep the cache boundary honest
The Symfony routing documentation says a stateless route must not use the session. The public catalog shell can remain stateless and cacheable, while the account fragment is handled by a separate request whose route is allowed to load authentication state.
This separation only works when the ESI-capable reverse proxy actually makes that request and forwards the cookies or authorization information required by the application. If ESI is not available, make the parent route stateful or render a non-personalized placeholder and load the private content separately. Do not mark personalized HTML as publicly cacheable.
Configure and verify the fragment response
Symfony’s ESI documentation explains that ESI lets a cached page include fragments with different cache rules. Give the fragment an appropriate private or no-store policy, and keep the outer page free of user-specific data.
Test the application through the same reverse proxy used in production. Verify anonymous and authenticated requests, confirm that one user never receives another user’s fragment, and check the response headers for both the shell and fragment. The current Symfony issue that prompted this pattern reports the exception with Symfony 8.1.11; treat it as a useful reproduction, not proof that every Symfony release behaves identically.