symfony: [Session][Test] Cannot set session ID after the session has started.

Hi there,

the exception is thrown by this line: https://github.com/symfony/symfony/blob/2.5/src/Symfony/Component/HttpFoundation/Session/Storage/MockArraySessionStorage.php#L134

My setup works completely fine within a valid PHP environment (without TestSessionListener) - data getting stored in the session before the TestSessionListener::onKernelRequest has been executed (using MockFileSessionStorage).

What’s the intention of this exception? I can’t get my head around it. The TestSessionListener is explicitly doing this (setId) and if there is another object accessing the session before the TestSessionListener sets the id, an id is automatically generated. The listener is not correctly mimicking the session creation of PHP and this exception makes it (for some parts) unusable.

If there is no actual use on this exception, it may be removed, no?

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Reactions: 1
  • Comments: 49 (33 by maintainers)

Commits related to this issue

Most upvoted comments

I had randomly the same problem in Symfony 3.2.* during functional tests. Solution presented by @rgarcia-martin didn’t work for me, so I made own with following code:

<?php

namespace CoreBundle\Event;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\EventListener\TestSessionListener as BaseTestSessionListener;

class TestSessionListener extends BaseTestSessionListener
{
    /**
     * @var ContainerInterface
     */
    protected $container;

    /**
     * TestRequestListener constructor.
     * @param ContainerInterface $container
     */
    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    /**
     * @return null|SessionInterface
     */
    protected function getSession() : ?SessionInterface
    {
        if (!$this->container->has('session')) {
            return null;
        }

        return $this->container->get('session');
    }

    /**
     * @param GetResponseEvent $event
     */
    public function onKernelRequest(GetResponseEvent $event)
    {
        if (!$event->isMasterRequest()) {
            return;
        }

        // bootstrap the session
        $session = $this->getSession();
        if (!$session) {
            return;
        }

        $cookies = $event->getRequest()->cookies;

        if ($cookies->has($session->getName())) {
            if (!$session->isStarted()) {
                $session->setId($cookies->get($session->getName()));
            }
        }
    }
}

and in config_test.yml :

services:
    test.session.listener:
        class: AppBundle\Event\TestSessionListener
        arguments:
            - '@service_container'
        tags:
            - { name: kernel.event_subscriber }

Seems it works.

Hi everybody!

This work for me: Im doing functionality tests in my Symfony 2.6 app. That test go against routes protected and i need to do the requests with an UsernamePasswordToken setted at the session.

-The cookie always have the same id that the session, but that Exception is thrown when the cookie is setted and TestSessionListener check that the cookie have an item with the session name.

My solution:

  • Create an class than extends Symfony\Bundle\FrameworkBundle\EventListener\TestSessionListener
  • Set “onKernelRequest” with the code:
public function onKernelRequest(GetResponseEvent $event)
    {
        if (!$event->isMasterRequest()) {
            return;
        }

        // bootstrap the session
        $session = $this->getSession();
        if (!$session) {
            return;
        }

        $cookies = $event->getRequest()->cookies;

        if ($cookies->has($session->getName())) {
            if($session->getId() != $cookies->get($session->getName())){
                $session->setId($cookies->get($session->getName()));
            }
        }
    }
  • The cookie[sessionName] value and session[id] are the same…
  • Set in config_test.yml :
parameters:
    test.session.listener.class: "Path\To\Your\Class"

Why it have to set the id again and expose us to get an exception if the session is started?

I hope help you! Bye!

Hoping this will help someone. I’m using the Liip Functional Test Bundle and was getting this issue. The way I solved it was to save the session before a request was made.

Example:

$client = $this->makeClient();
$client->getContainer()->get('session')->set('session_key_to_use', 'value_for_key');
$client->getContainer()->get('session')->save(); // This seems to make it work
$crawler = $client->request('GET', '/path/to');

config_test.yml

framework:
  test: ~
  session:
    name: MOCKSESSID
    storage_id: session.storage.mock_file

Symfony Version: 3.4.2 Liip Functional Test Version: 1.9.0

Hope this helps

One of my coworkers solved this problem. The config_test.yml had these lines:

framework:
    test: ~
    profiler:
        collect: false

Deleting test: ~ from config_test.yml got rid of the error.

If you interested, I have worked around this by clearing the session between requests. In one test:

$client->disableReboot();
$client->request...
...
$client->getCookieJar()->clear();
$client->request...

This means I didn’t have to apply fixes inside the Symfony code.

I’ve also encountered this issue in functional test extending WebTestCase. It was working fine before i disabled kernel rebooting (by calling disableReboot on test.client.class because of DB isolation - i want to share the same connection so it can run in one transaction).

There is a TestSessionListener class which sets session ID if present in cookies on every master request. Problem is when the session is shared between requests (which is also caused by disabling the kernel reboot). Then it tries to set the session id on already started session in MockArraySessionStorage.php:142

    public function setId($id)
    {
        if ($this->started) {
            throw new \LogicException('Cannot set session ID after the session has started.');
        }

        $this->id = $id;
    }

In this case this call is redundant since the same session ID is already present in the session.

I temporarily fixed it by extending MockArraySessionStorage (or MockFileSessionStorage) like this.

    /**
     * {@inheritdoc}
     */
    public function setId($id)
    {
        if ($this->id !== $id) {
            parent::setId($id);
        }
    }

It is simpler than extending the TestSessionListener class. But if I understand it correctly then the listener is the place where the condition should be present.

Should it be fixed? Should i provide a PR? Have someone already provided a failing test case?

I stumbled upon this problem and may have a fix with https://github.com/symfony/symfony/pull/28433. Don’t hesitate to try and give your opinion on the PR or here. Thanks!

I ran into the same problem and found that the issue is that I have an event subscriber with a higher priority than Symfony’s TestSessionListener that interacts with the session.

So what happens is first my listener (priority 2048) calls has on the session, but the session hasn’t started yet, so it is started with a random ID generated by MockArraySessionStorage.

After that the TestSessionListener (priority 192) is called and wants to set the ID of the session it retrieved from a session cookie, but this fails because a session was already started by my call to has in my own listener, and you get the exception Cannot set session ID after the session has started.

I’ve “solved” this for now with a CompilerPass that removes the default tags from TestSessionListener and replaces it with tags with a higher priority for kernel.request than my own listener, so the TestSessionListener is called before my own listener, and that solves the problem for me.

$container->getDefinition('test.session.listener')->clearTags();
$container->getDefinition('test.session.listener')->addTag(
    'kernel.event_listener',
    [
        'event' => 'kernel.request',
        'method' => 'onKernelRequest',
        'priority' => 2049 // priority of my listener + 1
    ]
);
$container->getDefinition('test.session.listener')->addTag(
    'kernel.event_listener',
    [
        'event' => 'kernel.response',
        'method' => 'onKernelResponse',
        'priority' => -1000 // same as original, doesn't really matter
    ]
);

Without resorting to gigantic numbers for the priority of TestSessionListener I’m not really sure how to structurally solve this problem though. And even with a very high priority there is always someone that needs an even higher priority for one reason or another and breaks it again.

Maybe there is a way to retrieve all events subscribed to kernel.request, finds the highest priority, and register TestSessionListener with that highest priority + 1 in a CompilerPass somewhere? Rather nasty but would work.

Or maybe we could pass the RequestStack to the MockFileSessionStorage and let it use the cookie in the master request (if it exists) instead of generating it’s own ID. That would couple the two, but they are already coupled at the moment through the TestSessionListener. That would require the RequestStack have a master request upon first call though, and I’m not sure that something we can and/or should guarantee.

Got the same issue with Liip Functional Test Bundle too, and Symfony 4.0.

Workaround for my case, inspired from https://github.com/symfony/symfony/issues/13450#issuecomment-147447252:

use Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage as BaseSessionStorage;

final class MockFileSessionStorage extends BaseSessionStorage
{
    public function setId($id)
    {
        if ($this->id !== $id) {
            parent::setId($id);
        }
    }
}

Then on the Kernel class:

use App\Tests\TestCase\MockFileSessionStorage;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        if ('test' === $this->environment) {
            $container->getDefinition('session.storage.mock_file')->setClass(MockFileSessionStorage::class);
        }
    }

Work for me. But I’m sure the exception is not here for nothing. 😃

What is the state of this issue?

In my case I was using the @session service in the Twig Extension constructor, like:

public function __construct($session) {
  $this->timezone = $this->session->get('timezone', 'UTC');
}

The solution was just to store @session and use it later:

public function __construct($session) {
  $this->session = $session;
}

Thanks, anyway.

I fixed this during a 2.5 -> 2.6 upgrade by finding all references to session in my source and removing them one-by-one til I found the culprit (as usual, the very last one.) It seems that passing the session to a service is OK, but accessing its variables or modifying it in the service’s constructor is a no-no unless the session is started. You can check this by checking if($session->isStarted()).