Skip to content

[HttpKernel] AbstractSessionListener saves session for stateless request #50958

Closed
@gndk

Description

@gndk

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions