Description
Symfony version(s) affected
6.3.1
Description
I've been seeing some session_write_close(): Failed to write session data with "Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler" handler
warnings in my Sentry for a specific route.
This is probably caused by Redis sessions being non-locking (fingers crossed for #4976).
So I set this route to stateless: true
, but it still happens.
This route receives an AJAX request during a "normal" pageview. It does not itself use the session.
But as it is an AJAX request, the request includes the session cookie, which is probably why AbstractSessionListener.php#L95 and AbstractSessionListener.php#L108 evaluate to true.
I think this case might have been missed when _stateless
was introduced in #35732.
How to reproduce
I wrote this test case for SessionListenerTest, which I believe should pass.
Currently, it fails with Symfony\Component\HttpFoundation\Session\Session::save() was not expected to be called.
.
public function testSessionNotSavedForStatelessRequest()
{
$session = $this->createMock(Session::class);
$session->expects($this->once())->method('isStarted')->willReturn(true);
$session->expects($this->once())->method('getUsageIndex')->willReturn(0);
$session->expects($this->never())->method('save');
$listener = new SessionListener(new Container(), false);
$kernel = $this->createMock(HttpKernelInterface::class);
$request = new Request();
$request->setSession($session);
$request->attributes->set('_stateless', true);
$listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, new Response()));
}
Possible Solution
Don't save the session if the request is stateless.
The whole check for stateless request could be moved to the top of the listener.
I don't understand why the session should be saved in a stateless request, but there is a test case for it in SessionListenerTest.php#L785.
Maybe the exception/warning for using the session in a stateless request should be thrown without actually saving the session?
$session = $event->getRequest()->getSession();
if ($event->getRequest()->attributes->get('_stateless', false)) {
if ($session->getUsageIndex() !== 0) {
if ($this->debug) {
throw new UnexpectedSessionUsageException('Session was used while the request was declared stateless.');
}
if ($this->container->has('logger')) {
$this->container->get('logger')->warning('Session was used while the request was declared stateless.');
}
}
return;
}
if ($session->isStarted()) {
// ...
$session->save();
// ...
}
Additional Context
ErrorException: Warning: session_write_close(): Failed to write session data with "Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler" handler
#19 /vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php(259): Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage::save
#18 /vendor/symfony/http-foundation/Session/Session.php(171): Symfony\Component\HttpFoundation\Session\Session::save
#17 /vendor/symfony/http-kernel/EventListener/AbstractSessionListener.php(134): Symfony\Component\HttpKernel\EventListener\AbstractSessionListener::onKernelResponse
#16 /vendor/symfony/event-dispatcher/EventDispatcher.php(260): Symfony\Component\EventDispatcher\EventDispatcher::Symfony\Component\EventDispatcher\{closure}
#15 /vendor/symfony/event-dispatcher/EventDispatcher.php(220): Symfony\Component\EventDispatcher\EventDispatcher::callListeners
#14 /vendor/symfony/event-dispatcher/EventDispatcher.php(56): Symfony\Component\EventDispatcher\EventDispatcher::dispatch
#13 /vendor/symfony/http-kernel/HttpKernel.php(199): Symfony\Component\HttpKernel\HttpKernel::filterResponse
#12 /vendor/symfony/http-kernel/HttpKernel.php(187): Symfony\Component\HttpKernel\HttpKernel::handleRaw
#11 /vendor/symfony/http-kernel/HttpKernel.php(74): Symfony\Component\HttpKernel\HttpKernel::handle
#10 /vendor/symfony/http-kernel/Kernel.php(197): Symfony\Component\HttpKernel\Kernel::handle
#9 /vendor/symfony/http-kernel/HttpCache/SubRequestHandler.php(86): Symfony\Component\HttpKernel\HttpCache\SubRequestHandler::handle
#8 /vendor/symfony/http-kernel/HttpCache/HttpCache.php(473): Symfony\Component\HttpKernel\HttpCache\HttpCache::forward
#7 /vendor/symfony/framework-bundle/HttpCache/HttpCache.php(68): Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache::forward
#6 /vendor/symfony/http-kernel/HttpCache/HttpCache.php(273): Symfony\Component\HttpKernel\HttpCache\HttpCache::pass
#5 /vendor/symfony/http-kernel/HttpCache/HttpCache.php(287): Symfony\Component\HttpKernel\HttpCache\HttpCache::invalidate
#4 /vendor/symfony/http-kernel/HttpCache/HttpCache.php(210): Symfony\Component\HttpKernel\HttpCache\HttpCache::handle
#3 /vendor/symfony/http-kernel/Kernel.php(188): Symfony\Component\HttpKernel\Kernel::handle
#2 /vendor/symfony/runtime/Runner/Symfony/HttpKernelRunner.php(35): Symfony\Component\Runtime\Runner\Symfony\HttpKernelRunner::run
#1 /vendor/autoload_runtime.php(29): require_once
#0 /public/index.php(7): null