Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[HttpKernel] AbstractSessionListener saves session for stateless request #50958

Copy link
Copy link
Closed
@gndk

Description

@gndk
Issue body actions

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

      Morty Proxy This is a proxified and sanitized view of the page, visit original site.