RuntimeException: Failed to start the session because headers have already been sent by Response.php

Created on 19 August 2022, almost 2 years ago
Updated 26 July 2023, 11 months ago

I have subscription:

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    return [
      OrderEvents::ORDER_PAID => 'onPaid',
    ];
  }

On notification page /payment/notify/yookassa_ru I have an error:

RuntimeException: Failed to start the session because headers have already been sent by "/DATA/home-sites/lom/vendor/symfony/http-foundation/Response.php" at line 401 Exception Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage->start() /DATA/home-sites/lom/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php 155 3 Array 
#0 /DATA/home-sites/lom/web/core/lib/Drupal/Core/Session/SessionManager.php(162): Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage->start() 
#1 /DATA/home-sites/lom/web/core/lib/Drupal/Core/Session/SessionManager.php(110): Drupal\Core\Session\SessionManager->startNow() 
#2 /DATA/home-sites/lom/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php(373): Drupal\Core\Session\SessionManager->start() 
#3 /DATA/home-sites/lom/vendor/symfony/http-foundation/Session/Session.php(251): Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage->getBag('attributes') 
#4 /DATA/home-sites/lom/vendor/symfony/http-foundation/Session/Session.php(273): Symfony\Component\HttpFoundation\Session\Session->getBag('attributes') 
#5 /DATA/home-sites/lom/vendor/symfony/http-foundation/Session/Session.php(73): Symfony\Component\HttpFoundation\Session\Session->getAttributeBag() 
#6 /DATA/home-sites/lom/web/core/lib/Drupal/Core/TempStore/PrivateTempStore.php(230): Symfony\Component\HttpFoundation\Session\Session->get('core.tempstore....') 
#7 /DATA/home-sites/lom/web/core/lib/Drupal/Core/TempStore/PrivateTempStore.php(215): Drupal\Core\TempStore\PrivateTempStore->getOwner() 
#8 /DATA/home-sites/lom/web/core/lib/Drupal/Core/TempStore/PrivateTempStore.php(105): Drupal\Core\TempStore\PrivateTempStore->createkey('region_tid') 
#9 /DATA/home-sites/rg_common/modules/rg_location/src/RgRegionManager.php(116): Drupal\Core\TempStore\<strong>PrivateTempStore->get('region_tid') </strong>
#10 /DATA/home-sites/rg_common/modules/rg_location/src/RgRegionManager.php(88): Drupal\rg_location\RgRegionManager->getByTempstore() 
#11 /DATA/home-sites/rg_common/modules/rg_location/src/RgLocationFactory.php(82): Drupal\rg_location\RgRegionManager->getCurrentCityTid() 
#12 /DATA/home-sites/rg_common/modules/rg_location/rg_location.tokens.inc(111): Drupal\rg_location\RgLocationFactory->createCurrent() 
#13 [internal function]: rg_location_tokens('site', Array, Array, Array, Object(Drupal\Core\Render\BubbleableMetadata)) 
#14 /DATA/home-sites/lom/web/core/lib/Drupal/Core/Extension/ModuleHandler.php(426): call_user_func_array(Object(Closure), Array) 
#15 /DATA/home-sites/lom/web/core/lib/Drupal/Core/Extension/ModuleHandler.php(405): Drupal\Core\Extension\ModuleHandler->Drupal\Core\Extension\{closure}(Object(Closure), 'rg_location') 
#16 /DATA/home-sites/lom/web/core/lib/Drupal/Core/Extension/ModuleHandler.php(433): Drupal\Core\Extension\ModuleHandler->invokeAllWith('tokens', Object(Closure)) 
#17 /DATA/home-sites/lom/web/core/lib/Drupal/Core/Utility/Token.php(357): Drupal\Core\Extension\ModuleHandler->invokeAll('tokens', Array) 
#18 /DATA/home-sites/lom/web/core/lib/Drupal/Core/Utility/Token.php(239): Drupal\Core\Utility\Token->generate('site', Array, Array, Array, Object(Drupal\Core\Render\BubbleableMetadata)) 
#19 /DATA/home-sites/lom/web/core/lib/Drupal/Core/Utility/Token.php(189): Drupal\Core\Utility\Token->doReplace(true, '\xD0\x9F\xD0\xBE\xD0\xB6\xD0\xB0\xD0\xBB\xD1\x83\xD0\xB9\xD1...', Array, Array, Object(Drupal\Core\Render\BubbleableMetadata)) 
#20 /DATA/home-sites/rg_common/modules/rg/src/MailHandler.php(119): Drupal\Core\Utility\Token->replace(Object(Drupal\Core\StringTranslation\TranslatableMarkup)) 
#21 /DATA/home-sites/rg_common/modules/rg_balance/src/Mail/DepositDoneMail.php(26): Drupal\rg\MailHandler->sendMail('info@sday-lom.r...', '\xD0\x9A\xD1\x83\xD0\xBF\xD0\xB8\xD0\xBB\xD0\xB8 \xD1\x82...', Array, Array) 
#22 /DATA/home-sites/rg_common/modules/rg_balance/modules/commerce/src/EventSubscriber/OrderCompleteSubscriber.php(135): Drupal\rg_balance\Mail\DepositDoneMail->send(Object(Drupal\user\Entity\User), Object(Drupal\commerce_price\Price)) 
#23 [internal function]: Drupal\rg_balance_commerce\EventSubscriber\<strong>OrderCompleteSubscriber->onPaid</strong>(Object(Drupal\rg_balance\Event\DepositEvent), 'commerce_order....', Object(Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher)) 
#24 /DATA/home-sites/lom/web/core/lib/Drupal/Component/EventDispatcher/ContainerAwareEventDispatcher.php(142): call_user_func(Array, Object(Drupal\commerce_order\Event\OrderEvent), 'commerce_order....', Object(Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher)) 
#25 /DATA/home-sites/lom/web/modules/contrib/commerce/modules/order/src/OrderStorage.php(100): Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object(Drupal\commerce_order\Event\OrderEvent), 'commerce_order....') 
#26 /DATA/home-sites/lom/web/modules/contrib/commerce/modules/order/src/OrderStorage.php(61): Drupal\commerce_order\OrderStorage->doOrderPreSave(Object(Drupal\commerce_order\Entity\Order)) 
#27 /DATA/home-sites/lom/web/core/lib/Drupal/Core/Entity/EntityStorageBase.php(563): Drupal\commerce_order\OrderStorage->invokeHook('presave', Object(Drupal\commerce_order\Entity\Order)) 
#28 /DATA/home-sites/lom/web/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php(756): Drupal\Core\Entity\EntityStorageBase->doPreSave(Object(Drupal\commerce_order\Entity\Order)) 
#29 /DATA/home-sites/lom/web/core/lib/Drupal/Core/Entity/EntityStorageBase.php(517): Drupal\Core\Entity\ContentEntityStorageBase->doPreSave(Object(Drupal\commerce_order\Entity\Order)) 
#30 /DATA/home-sites/lom/web/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php(802): Drupal\Core\Entity\EntityStorageBase->save(Object(Drupal\commerce_order\Entity\Order)) 
#31 /DATA/home-sites/lom/web/core/lib/Drupal/Core/Entity/EntityBase.php(339): Drupal\Core\Entity\Sql\SqlContentEntityStorage->save(Object(Drupal\commerce_order\Entity\Order)) 
#32 /DATA/home-sites/lom/web/modules/contrib/commerce/modules/payment/src/PaymentOrderUpdater.php(95): Drupal\Core\Entity\EntityBase->save() 
#33 /DATA/home-sites/lom/web/modules/contrib/commerce/modules/payment/src/PaymentOrderUpdater.php(59): Drupal\commerce_payment\PaymentOrderUpdater->updateOrder(Object(Drupal\commerce_order\Entity\Order), true) 
#34 /DATA/home-sites/lom/web/modules/contrib/commerce/modules/payment/src/PaymentOrderUpdater.php(106): Drupal\commerce_payment\PaymentOrderUpdater->updateOrders() 
#35 /DATA/home-sites/lom/web/core/lib/Drupal/Core/EventSubscriber/KernelDestructionSubscriber.php(51): Drupal\commerce_payment\PaymentOrderUpdater->destruct() 
#36 [internal function]: Drupal\Core\EventSubscriber\KernelDestructionSubscriber->onKernelTerminate(Object(Symfony\Component\HttpKernel\Event\TerminateEvent), 'kernel.terminat...', Object(Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher)) 
#37 /DATA/home-sites/lom/web/core/lib/Drupal/Component/EventDispatcher/ContainerAwareEventDispatcher.php(142): call_user_func(Array, Object(Symfony\Component\HttpKernel\Event\TerminateEvent), 'kernel.terminat...', Object(Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher)) 
#38 /DATA/home-sites/lom/vendor/symfony/http-kernel/HttpKernel.php(100): Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object(Symfony\Component\HttpKernel\Event\TerminateEvent), 'kernel.terminat...') 
#39 /DATA/home-sites/lom/vendor/stack/builder/src/Stack/StackedHttpKernel.php(32): Symfony\Component\HttpKernel\HttpKernel->terminate(Object(Symfony\Component\HttpFoundation\Request), Object(Symfony\Component\HttpFoundation\Response)) 
#40 /DATA/home-sites/lom/web/core/lib/Drupal/Core/DrupalKernel.php(687): Stack\StackedHttpKernel->terminate(Object(Symfony\Component\HttpFoundation\Request), Object(Symfony\Component\HttpFoundation\Response)) 
#41 /DATA/home-sites/lom/web/index.php(22): Drupal\Core\DrupalKernel->terminate(Object(Symfony\Component\HttpFoundation\Request), Object(Symfony\Component\HttpFoundation\Response)) 
#42 {main} Exception in /DATA/home-sites/lom/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php:155

In onPaid() function I use Private TempStore which starts a new session. But session is already closed by \Drupal\commerce_payment\Controller\PaymentNotificationController::notifyPage

I guess OrderComplete actions should bee done before returning the Response.

🐛 Bug report
Status

Active

Version

2.0

Component

Order

Created by

🇺🇦Ukraine super_romeo Kiev

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • 🇩🇪Germany cspitzlay 🇩🇪🇪🇺

    I see a similar error after successful online payment.

    The PaymentOrderUpdater->updateOrders() call is executed when the kernel terminates which is after the response has already been sent.

    In my case it actually udpates the order. But the entity save method tries to trigger the replica kill switch which in turn tries to write to the session. That is impossible if the response has already been sent.

    NB: That particular issue is only triggered if there are replica DBs defined.

  • 🇩🇪Germany cspitzlay 🇩🇪🇪🇺

    Here are some more details:

    The common thing in both cases is that Drupal\commerce_payment\PaymentOrderUpdater->updateOrders()
    which runs in the kernel termination event triggers code that accesses the session.

    I guess it is expected from a method called updateOrders() that it, well, updates orders.
    But the general implementation in Drupal\Core\Entity\Sql\SqlContentEntityStorage::save() triggers the
    replica kill switch: \Drupal::service('database.replica_kill_switch')->trigger();
    which in turn will write to the session(*) (which will crash if called after sending of the response):

      public function trigger() {
        $connection_info = Database::getConnectionInfo();
        // Only set ignore_replica_server if there are replica servers being used,
        // which is assumed if there are more than one.
        if (count($connection_info) > 1) {
          // Five minutes is long enough to allow the replica to break and resume
          // interrupted replication without causing problems on the Drupal site
          // from the old data.
          $duration = $this->settings->get('maximum_replication_lag', 300);
          // Set session variable with amount of time to delay before using replica.
          $this->session->set('ignore_replica_server', $this->time->getRequestTime() + $duration);
        }
      }
    

    Thus it looks fundamentally wrong to me to call updateOrders() from the Kernel termination event.

    At least in my case this error even goes unnoticed by the user, as the response has already been sent to the browser
    and the crash will not appear in the messenger. There is a watchdog message though,
    and the DB transaction that contains the changes that the updateOrders() was about will be rolled back.

    (*): Unless there are no replicas at all.

  • 🇩🇪Germany cspitzlay 🇩🇪🇪🇺

    Here is my stacktrace from the watchdog:

    Drupal\Core\Entity\EntityStorageException: Failed to start the session because headers have already been sent by "/var/www/html/vendor/symfony/http-foundation/Response.php" at line 384. in Drupal\Core\Entity\Sql\SqlContentEntityStorage->save() (line 815 of /var/www/html/web/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php) #0 /var/www/html/web/core/lib/Drupal/Core/Entity/EntityBase.php(339): Drupal\Core\Entity\Sql\SqlContentEntityStorage->save(Object(Drupal\commerce_order\Entity\Order))
    #1 /var/www/html/web/modules/contrib/commerce/modules/payment/src/PaymentOrderUpdater.php(101): Drupal\Core\Entity\EntityBase->save()
    #2 /var/www/html/web/modules/contrib/commerce/modules/payment/src/PaymentOrderUpdater.php(59): Drupal\commerce_payment\PaymentOrderUpdater->updateOrder(Object(Drupal\commerce_order\Entity\Order), true)
    #3 /var/www/html/web/modules/contrib/commerce/modules/payment/src/PaymentOrderUpdater.php(112): Drupal\commerce_payment\PaymentOrderUpdater->updateOrders()
    #4 /var/www/html/web/core/lib/Drupal/Core/EventSubscriber/KernelDestructionSubscriber.php(51): Drupal\commerce_payment\PaymentOrderUpdater->destruct()
    #5 [internal function]: Drupal\Core\EventSubscriber\KernelDestructionSubscriber->onKernelTerminate(Object(Symfony\Component\HttpKernel\Event\TerminateEvent), 'kernel.terminat...', Object(Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher))
    #6 /var/www/html/web/core/lib/Drupal/Component/EventDispatcher/ContainerAwareEventDispatcher.php(142): call_user_func(Array, Object(Symfony\Component\HttpKernel\Event\TerminateEvent), 'kernel.terminat...', Object(Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher))
    #7 /var/www/html/vendor/symfony/http-kernel/HttpKernel.php(103): Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object(Symfony\Component\HttpKernel\Event\TerminateEvent), 'kernel.terminat...')
    #8 /var/www/html/vendor/stack/builder/src/Stack/StackedHttpKernel.php(32): Symfony\Component\HttpKernel\HttpKernel->terminate(Object(Symfony\Component\HttpFoundation\Request), Object(Drupal\Core\Routing\TrustedRedirectResponse))
    #9 /var/www/html/web/core/lib/Drupal/Core/DrupalKernel.php(712): Stack\StackedHttpKernel->terminate(Object(Symfony\Component\HttpFoundation\Request), Object(Drupal\Core\Routing\TrustedRedirectResponse))
    #10 /var/www/html/web/index.php(22): Drupal\Core\DrupalKernel->terminate(Object(Symfony\Component\HttpFoundation\Request), Object(Drupal\Core\Routing\TrustedRedirectResponse))
    
  • 🇩🇪Germany cspitzlay 🇩🇪🇪🇺

    That's true for the exception I'm seeing. Thanks for finding that issue.

    NB: Fixing 🐛 ReplicaKillSwitch unnecessarily starts a session on each request Needs work won't help with the original reporter's problem though.

  • 🇦🇹Austria gr4phic3r

    subscribed, got the same issue

  • 🇸🇬Singapore vhin0210

    Still having this issue. How do we fix this?

  • 🇮🇱Israel jsacksick

    I'm not sure what to do here as the payment order updater service was written this way to mitigate race conditions when saving the order / data loss.

    Maybe we can revisit this now that 🐛 The changes made to the order on the onNotify method are not applied on the onReturn method Fixed in in... but TBH I'm afraid to introduce a breaking change if we change this now...

  • 🇳🇱Netherlands INAScon

    Besides the reported error message, we also see that the session ID no longer exists after payment has been completed. Having no session ID makes it impossible to create an account after payment (related to the completed order), which is an issue on the 30+ webshops we manage.

    This is an urgent issue for us. Is there anything I can do to help fix this issue?

  • 🇺🇸United States rszrama

    I suspect the answer here will be some combination of fixing the core issue and finding other ways to accomplish the same tasks people are attempting. For example, is using a private temp store necessary vs. storing relevant data in the order itself? Same for creating an account without the original session - could the necessary data not have been stored in the order data array and then used to create the account upon successful payment?

    Absent some other way to tell a page request, "Make sure this happens last," I don't think we have another option for when / how we run the PaymentOrderUpdater updates. If this is the case, perhaps we just need to add documentation to the service and to the order paid event to make it clear why session initiating code should not be used in this context?

Production build 0.69.0 2024