Issue Summary updated from comment #11 by @sdjili.
Problem/Motivation
The DatabaseCacheTagsChecksum service registers rootTransactionEndCallback as a post-transaction callback. This callback runs doInvalidateTags() which updates the cachetags table via the database Connection.
When PHP shutdown begins, object destruction order is undefined. If Connection::__destruct runs before Transaction::__destruct, the Connection sets $this->connection = NULL (PDO) before the post-transaction callbacks execute. When rootTransactionEndCallback → purge() → processPostTransactionCallbacks() runs, it uses a Connection whose PDO is already null, causing the error and preventing the cachetags table from being updated.
The issue can also happen with any module declaring a post transaction callback via \Drupal\Core\Database\Transaction\TransactionManagerInterface::addPostTransactionCallback.
Steps to reproduce
Reproduction seem complicated as it's related to a race condition in the garbage collector.
Proposed resolution
The commitAll() method in TransactionManagerBase already unpiles the transaction stack during shutdown. We should also run processPostTransactionCallbacks() there, before any destructors run. This ensures cache tag invalidation (and any other post-transaction callbacks) execute while the Connection is still valid.
Original report by @titouille
The error occurs when I create an article and suppress it when I'm on admin administration content page.
Steps to reproduce
- Install a fresh version of drupal 11.3.2 with standard profile.
- Go to /admin/content
- Create a new article content and save it.
- Return on /admin/content
- Delete the content.
The stack trace displayed at screen :
( ! ) Fatal error: Uncaught TypeError: Drupal\Core\Database\StatementWrapperIterator::__construct(): Argument #2 ($clientConnection) must be of type object, null given, called in /web/core/lib/Drupal/Core/Database/Connection.php on line 441 and defined in /web/core/lib/Drupal/Core/Database/StatementWrapperIterator.php on line 38
[...]I was using drupal 11.2 and all worked fine, before I updated to drupal 11.3...
using httpd (homebrew) with php8.3 on macos.
| Comment | File | Size | Author |
|---|
Issue fork drupal-3569316
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
titouilleI think this problem should be classified as major, even critical, because it poses a real issue for end users. In my case, for example, users can add, modify, and delete content. If they encounter an error like this (stack error message), the situation becomes untenable in production.
The problem occurs when the system try to execute the cache invalidation. The database connection is closed before and the invalidation cannot be done.
I'm actually surprised that no one else has reported this problem before me.
Comment #3
mondrakeIn different nuances, I think this is a known problem, see referenced issues. #3447097-10: TransactionNameNonUniqueException: A transaction named drupal_transaction is already in use especially seems very close to the issue here.
It may be worth trying to apply #3406985: Convert all transactions in core to use explicit ::commitOrRelease(), but that will not likely solve the post-commit callback issue reported.
Comment #4
mondrakeComment #5
mondrakeComment #6
titouilleHi Mondrake and thank you for your quick reply. I can confirm that with the fork drupal-3495554 (Error unpiling database transactions with MySQL driver in kernel tests) the error message disapears.
drupal-3447097 (TransactionNameNonUniqueException: A transaction named drupal_transaction is already in use)
and drupal-3406985 (Convert all transactions in core to use explicit ::commitOrRelease(), deprecate implicit commit-on-destruct)
continue to display the same error.
Comment #7
titouilleActually I do not have any solution instead of overriding DatabaseCacheTagsChecksums and add a try/catch to manage the cache invalidation in another way when the connection is already lost.
The deferred cache tags store use a file to store tags, because nor State nor $_SESSION nor database can be used at the time of the process :
And an event listener for kernel.response is used to really invalidate the cache and redirect the user to the same page, to avoid any remanent data / error messages.
And finally, in the services.yml file :
This is the solution I found after tried a lot of other solutions.
This is just a fallback. If the problem is resolved in the core, this code will work like the core DatabaseCacheTagsChecksum works actually.
Now if anyone has better I'm listening on it.
Comment #8
sdjili commentedThis is a production-critical issue. We see the same
StatementWrapperIterator::__construct(): Argument #2 ($clientConnection) must be of type object, null given errorin our live environment.
In our case, the error does not follow a single, reproducible path. Random occurrence It can occur during:
- Node edit and save, AJAX form submissions
- Media library operations (editing media, opening the media library modal)
- Various content management operations
We have not been able to identify a scenario where it happens every time.
It seems to be timing-related and tied to object destruction order during PHP-FPM request shutdown.
Same underlying error
Our stack trace matches the one in this ticket: the error happens in Connection::prepareStatement when $clientConnection is null, triggered during cache tag invalidation in post-transaction callbacks (DatabaseCacheTagsChecksum::doInvalidateTags → rootTransactionEndCallback).
Environment
Drupal 11.3.2
drupal/mysql57 driver
multi-site, replicas
Any fix that addresses the destructor-order problem would help all affected use cases, not only the create/delete article flow.
Thank you for looking into this.
Comment #9
adraco commentedHi all, I can confirm I’m seeing this as well.
On Drupal 11.3.3 with PostgreSQL, I intermittently hit:
TypeError: Drupal\Core\Database\StatementWrapperIterator::__construct(): Argument #2 ($clientConnection) must be of type object, null given
It seems to happen when Drupal invalidates cache tags at the end of the request (CacheTagsChecksum / rootTransactionEndCallback). When it triggers, cache invalidation doesn’t complete properly and I get stale results (e.g. deleted nodes still appearing in a View/frontpage until drush cr, and occasionally new content not showing up until caches are rebuilt).
PHP versions tested:
8.3.30 — no issue
8.4.17-18 — issue occurs
8.5.3 — I no longer see the CacheTagsChecksum TypeError (I do see some unrelated PHP 8.5 deprecation notices from contrib modules)
Disabling Xdebug didn’t resolve it for me on 8.4.x.
Comment #10
sdjili commentedHi, I am adding some informations :
The issue arises only in the production environment, which includes multiple front ends, two database replicas, and a shared Memcached with two web fronts.
PHP version: 8.3.27
Observable symptom:
When the bug occurs, the cachetags table is not updated after a successful node save:
Expected: rows for node:xxx, node_list, 4xx-response, etc., or their invalidations incremented.
Actual: no new rows and no increments; cachetags stays unchanged.
Step-by-step flow
1. Node save → transaction starts → node data written.
2. Cache::invalidateTags(['node:xxx', 'node_list', ...]) → called from EntityBase::invalidateTagsOnSave().
3. Deferred invalidation → inTransaction() is true → tags go into $delayedTags → rootTransactionEndCallback registered.
4. Transaction commits → form completes.
5. Shutdown → PHP destroys objects in an undefined order.
6. Bug: Connection::__destruct runs first → sets $this->connection = NULL.
7. Transaction destructor runs → purge() → processPostTransactionCallbacks() → rootTransactionEndCallback → doInvalidateTags().
8. Failure: merge('cachetags') → prepareStatement() → PDO is null → TypeError → no rows written to cachetags.
9. Result: Node save succeeds, but cachetags is never updated, so cache checksums stay stale and Memcache keeps serving old content.
Stack trace
Thanks.
Comment #11
sdjili commentedRoot Cause
The
`DatabaseCacheTagsChecksum`service registers `rootTransactionEndCallback`as a post-transaction callback. This callback runs`doInvalidateTags()`which updates the`cachetags`table via the database Connection.When PHP shutdown begins, object destruction order is undefined. If
`Connection::__destruct`runs before`Transaction::__destruct`, the Connection sets `$this->connection = NULL` (PDO)before the post-transaction callbacks execute. When`rootTransactionEndCallback` → `purge()` → `processPostTransactionCallbacks()`runs, it uses a Connection whose PDO is already null, causing the error and preventing the `cachetags` table from being updated.Proposed Fix (extends existing approach)
The `
commitAll()`method in `TransactionManagerBase`already unpiles the transaction stack during shutdown. We should also run`processPostTransactionCallbacks()`there, **before** any destructors run. This ensures cache tag invalidation (and any other post-transaction callbacks) execute while the Connection is still valid.**Patch for `core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php`:**
Testing
We applied this patch in production and confirmed:
- The `
StatementWrapperIterator'error no longer occurs- Cache tag entries and checksums are correctly inserted into the `
cachetags`table after node saves- Cache invalidation works as expected
Additional Note
Because
`commitAllOnShutdown`runs during PHP shutdown (after the HTTP response is sent), there can be a race where the redirect response from a node save is sent before cache invalidation completes. In multi-worker setups (e.g. PHP-FPM), the follow-up request may hit a different worker before the first request’s shutdown has run. A separate improvement would be to run`commitAll()`earlier in the request lifecycle (e.g. on `KernelEvents::RESPONSE`) so cache invalidation completes before the response is sent.Comment #14
jsutta commentedI ran into this issue on Drupal 11.3.5 with Drush 13.7.2.0 and PHP 8.3.30. I hope it's ok but I went ahead and added the code from #11 to a merge request after it worked for me so I could install it via Composer.
Comment #15
bobooon commentedThis worked for me as well on Drupal 11.3.5 with PHP 8.3. In my case, the issue occurred after a user changed their preferred language, either via a language switcher block or on their user account form. After applying the patch, the error no longer occurred, and the cache bins were updated accordingly.
Comment #16
spfaffly commentedCan confirm @jsutta 's fix helped this issue for me. Thanks for the MR!
Comment #17
astutonetI was having problems running update.php on a local installation with D11.3.9, PHP 8.3.6, and the code from the proposed solution in #11 solved the issue.
Comment #18
agoradesign commentedThe MR works for me too:
on my D11.3.11 site, I've added an "event" content type, having a mandatory date range field. I wanted to include the start date in the pathauto pattern. I've tried a few different patterns, but in any case I've experienced always the same error: the node incl url alias is saved correctly, but the immediate redirect to the node detail page is failing with a HTTP 500 error (because of the "client connection... must be of type..." error described in the summary). Until the cache is cleared, any further call to the node page fails with an 404 error
After patching the MR from here, the problem is gone :)
here's my pathauto pattern used: '/termine/[node:field_date:start_date:html_date]/[node:title]'
Comment #19
duaelfrI ran into that issue by using the Node Revision Delete module that uses a post transaction callback to create queue items.
It was quite hard to understand what was the issue exactly and if my custom code was involved (it was not).
I can confirm the patch from the MR is working. The code looks good and the tests failure seem unrelated.
I think we would need some maintainer opinion on this one because they might have some other way to fix the issue.
Patch attached for composer.
Comment #20
duaelfrUpdated IS.
Comment #21
smustgrave commentedmay help to get a test case showing the real world example
Comment #22
swild-ma commentedOn our D11.3.3 site, the error occurs each time when i empty the caches (/admin/flush?token=xxx). After applying the patch (https://www.drupal.org/files/issues/2026-06-16/3569316-13.patch) the error does not happen anymore. Thanks!
Comment #23
duaelfrTests coverage have been expanded and tests-only job fails as expected.
Comment #24
daffie commentedLooks good to me.
Testing has been added.
Testing fails without the fix.
For me it is RTBC.
Removing the tag "Needs subsystem maintainer review"
Comment #27
godotislateI rebased the MR because there was an upstream test failure that has since been resolved.
Test only fails as expected: https://git.drupalcode.org/project/drupal/-/jobs/11124128
Committed 3f036e3 and pushed to main. Thanks!
Holding off on 11.x and 11.4.x because there are upstream test failures that hopefully can get resolved first soon. Will check back in a bit.
Comment #31
godotislateCommitted and pushed 5111b87 to 11.x and 3fd86f3 to 11.4.x. Thanks!
Comment #35
godotislateRevert for 11.x and 11.4.x because of PHPStan failures.
11.x: https://git.drupalcode.org/project/drupal/-/jobs/11132475
11.4.x: https://git.drupalcode.org/project/drupal/-/jobs/11132455
We should open an 11.x MR to make sure. I can do it later if no one else gets to it.
Comment #39
godotislateMR 16423 with one line change to remove
// @phpstan-ignore unset.possiblyHookedPropertyfrom TransactionTest.php passes.Committed and pushed 7634c65 to 11.x and 478fb37 to 11.4.x. Thanks!