- 🇩🇪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))
- 🇸🇬Singapore vhin0210
Subscribed. I have the same issue as #5 🐛 RuntimeException: Failed to start the session because headers have already been sent by Response.php Active
- 🇮🇱Israel jsacksick
This is due to 🐛 ReplicaKillSwitch unnecessarily starts a session on each request Needs work .
- 🇩🇪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.
- 🇮🇱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? - 🇸🇰Slovakia kaszarobert
We experienced the same with commerce_email module-defined e-mails for the Order paid event after an offsite payment gateway returned to the website. We solved that by postponing the e-mail building and sending later during cron where there are no problems with sessions.