Problem/Motivation
Twig's 3.30 released just now fails Drupal tests with TypeError: Twig\Runtime\EscaperRuntime::escape(): Argument #4 ($autoescape) must be of type bool, null given
Twig 3.30 changed how its escape filter is compiled. The filter now gets its own EscapeFilter node, which compiles to a direct call to $this->escaper->escape(...). Drupal's TwigNodeVisitor::leaveNode() only swaps that node's callable to drupal_escape. Twig still emits the direct call, but builds the arguments from drupal_escape, which needs the environment. So $this->env gets passed as an extra first argument, every argument shifts one place, and null (the charset) ends up in $autoescape.
Steps to reproduce
core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php and core/tests/Drupal/KernelTests/Core/Theme/TwigMarkupInterfaceTest.php are quick ones to check as they fail.
Proposed resolution
In core/lib/Drupal/Core/Template/TwigNodeVisitor.php, instead of changing the callable on Twig's node, leaveNode() now returns a new plain FilterExpression that uses drupal_escape with the original argument node. That way Twig's escape node is never compiled at all.
The composer.lock would need to be updated temporarily too to verify it passes on Twig 3.30. (Not in the MR).
Remaining tasks
User interface changes
Introduced terminology
API changes
Data model changes
Release notes snippet
LLM disclosure
LLM assistance was used to identify and propose a resolution to this issue.
Issue fork drupal-3625969
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:
- 3625969-11.3.x
changes, plain diff MR !17259
- 3625969-10.6.x
changes, plain diff MR !17250
- 3625969-11.x
changes, plain diff MR !17251
- 3625969-twigs-3.30-typeerror-with-twig-update-for-testing-only
changes, plain diff MR !17243
- 3625969-twigs-3.30-typeerror
changes, plain diff MR !17242
- 3625969-alternative-approach
changes, plain diff MR !17244
Comments
Comment #2
gábor hojtsy@jurgenhaas is saying its due to the "Speed up escaping by fetching the escaper runtime once per template" change in 3.30.
Comment #4
hfernandes commentedJust faced this issue while upgrading the core to 10.6.17.
In the meantime I had to lock the twig version with
composer require twig/twig:~3.29.0util this issue gets sorted out.Comment #5
gábor hojtsyMy LLM says "Twig 3.30's new
EscapeFilternode compiles to$this->escaper->escape(...), but Drupal'sTwigNodeVisitoronly swaps in drupal_escape without adjusting for the argument shift." and is experimenting with a solution :) Will report when it has it :DComment #6
gábor hojtsyComment #7
gábor hojtsyComment #8
phenaproximaGábor's Opus concurs with my Opus regarding both the cause and the fix.
Comment #10
socketwench commentedCan confirm that this resolves the WSOD on core 11.4.7 and Twig 3.30.
Comment #11
phenaproximaComment #12
phenaproximaHmmm...maybe not. There's a test failing in the "for testing only" branch, due to a deprecation in Twig itself.
Comment #13
gábor hojtsyThe fails on the updated real 3.30 MR are:
We would fix the deprecation notice I think but the performance number should be only fixed if we actually update to 3.30.
The test only changed did not run any tests in the composer lock update MR because no tests were changed, so making a dummy change there just to verify that :D
Comment #14
gábor hojtsyComment #15
jurgenhaasIt's been "discussed" over in the Twig issue queue as well: https://github.com/twigphp/Twig/issues/4952
Comment #16
godotislateJudging from the merged PR to Twig, tt seems like we could also just do this change:
I haven't tested this, though. Also we'd have to bump the Twig version to 3.30.0 to test in CI.
Comment #17
catchI'm unable to reproduce the umami performance test failure locally so far - it passes fine.
Comment #18
gábor hojtsy@godotislate: yeah we have two MRs so we can test the fix in parallel with 3.30 and current 3.28 on main.
Comment #21
gábor hojtsyThe suggestion from the Twig side issue is also to
which is what we are doing here.
Comment #22
longwaveAlternative approach in MR!17244 that should be more performant as well - instead of swapping out the filter at parse time, we just swap out the definitions of
escapeandefilters with our version.This approach was assisted by Claude Opus 5.5.
Comment #23
gábor hojtsyTest only job of https://git.drupalcode.org/project/drupal/-/jobs/12447670 now reproduces the fail with dummy test changes and the composer.lock update only in that MR. 😅
@catch: Umami fail magically disappeared with the one twig file fixed that was wrong for 3.30.
Comment #24
f0ns commentedTested both MRs on a real site running Drupal 11.4.7.
Before: with Twig 3.30.0 every page WSODs with
EscaperRuntime::escape(): Argument #4 ($autoescape) must be of type bool, null given.MR !17242: the
TwigNodeVisitor.phpchange fixes the WSOD on Twig 3.30.0. Theviews-view-table.html.twighunk doesn't apply to 11.4.x, which still hasgin_is_sticky.MR !17244: the first
TwigExtension.phphunk needs a context tweak for 11.4.x, wheregetFilters()has no: arrayreturn type. With that, it fixes the WSOD on Twig 3.30.0 and also works on Twig 3.28.0.For each setup I compared escaping output against stock Twig 3.28 for
{{ a }},{{ a|e }}, aMarkupobject with and without|escape,|escape('js')and render arrays with and without|e. The output was identical in every case.Comment #25
ykhalid commentedHere is a reproduction with plain Twig and no Drupal, in case it helps whoever reviews the two MRs. It registers a filter the way drupal_escape is registered (needs_environment) and swaps the callable of every escape and e node the way TwigNodeVisitor does, then prints the compiled escape calls. In an empty directory run
composer require twig/twig:3.29.0(or 3.30.0), thenphp repro.php.Twig 3.29.0 renders and compiles the swapped filter call:
yield (string) $this->extensions['DrupalLikeExtension']->escapeFilter($this->env, ($context["v"] ?? null), "html", null, true);Twig 3.30.0 throws the same TypeError as the site does and compiles a direct runtime call with the swapped filter's argument list, so the Environment lands in the first slot and everything shifts:
yield (string) $this->escaper->escape($this->env, ($context["v"] ?? null), "html", null, true);It runs in a second, so it is a quick way to check an approach against both versions without a site. I also opened the Twig side report, which is what jurgenhaas linked above.
Comment #26
kevinquillen commentedConfirmed, had to pin to a previous version for build to work.
Comment #27
junaidpvI am facing this issue
Comment #28
ayoub.elmansouri commentedI tested MR !17242 on core 11.4.7 and Twig 3.30 and it works as expected. The fix resolves the issue perfectly without any regressions.
Comment #29
godotislateI bumped Twig locally to 3.30.
For MR 17244:
TwigExtensionTest fails as expected without the changes to TwigExtension and TwigNodeVisitor, and the changes make the test pass.
I think 17244 is the best way forward for the performance benefits cited in #22. A Twig method is being copied to TwigExtension, and so there's a risk we could miss upstream changes, but on the other hand, the Twig method is marked
@internal, so I'm not sure which way is better, but I think it's fine as is.But it otherwise is good for RTBC.
Looks like we'll also need a separate 11.4.x MR, and maybe also for 11.3.x and 10.6.x as well?
Comment #30
cilefen commented#3626013: TypeError: Twig\Runtime\EscaperRuntime::escape(): Argument #2 ($strategy) must be of type string, null given
Comment #31
godotislateIn the meantime while this issue is open and the fix has a release, sites can have their Twig version reverted with
composer update twig/twig:3.28.0. This will set3.28.0as the version incomposer.lockwithout adjusting constraints incomposer.json.Comment #34
catchAgreed with taking this out of runtime.
I've committed/pushed https://git.drupalcode.org/project/drupal/-/merge_requests/17244. to main and 12.0.x
Moving to 11.x for backport.
Comment #37
lnunesbrHave tested MR !17250 with drupal 10.6.x and works fine.
Comment #38
godotislateMR for 11.x (applies clean to 11.4.x and 11.3.x as well): https://git.drupalcode.org/project/drupal/-/merge_requests/17251
MR for 10.6.x: https://git.drupalcode.org/project/drupal/-/merge_requests/17250
Comment #39
mw4ll4c3 commentedConfirming MR 17251 works on 11.4.7
Comment #40
majorrobot commentedI tested the MR for 11.x in 11.4.7 locally, and it worked like a charm.
Comment #41
majorrobot commentedTested the MR for 10.6.x, and works well there, too.
Comment #42
godotislateNote that I added the 10.6.x MR because in #3612448: Widen constraints in core-recommended for 11.3.x and 10.6.x, we relaxed the Twig constraint, but I'm not sure the changes here will be backported to 11.3.x or 10.6.x since they are security-only branches.
Comment #43
elc commentedIf any site running 11.3.x and 10.6.x no longer works when they are updated, I would consider that to be a sufficiently serious issue to warrant back-porting to a supported branch that is meant to continue working for another 3 months.
This is a downside of the loosening of the constraints - we are no longer guaranteed a tested set of dependencies in core-recommended and a breaking release can occur between scheduled release cycles, just like a security release of a dependency used to get stuck on constraints.
If the constraints are going to be left relaxed and Twig 3.30.x is now a possible dependency, then Drupal must be compatible with it.
It makes for the case that there are circumstances when an off schedule release is warranted. This seem to be one of them as the main Drupal product is effectively broken until the next published release that fixes this. Only a small percentage of site admins are going to find their way to this issue for the fix. The rest need it to just work.
Comment #44
jaydip makawana commentedI tested the MR for 11.x in 11.4.7 locally, its working as expected.
Comment #45
camerongreen commentedPop something like this in your composer.json and update:
"twig/twig": "^3.29.0, !=3.30.0"Comment #49
catchCommitted/pushed the backports to 11.x/11.4.x and 10.6.x, thanks!
Comment #51
damienmckennaThanks everyone!
Comment #52
damienmckennaMight it be possible to get releases tagged with this fix? This bug is causing all functional tests to fail for all contrib modules using D10, D11 or D12. Thank you again.
Comment #53
longwaveYes, the release managers are already discussing this, but the timing is not great with Drupalcon next week.
Comment #54
just_like_good_vibeshello,
about pipelines failures on contrib modules, you can use a temporary solution if you like, with variables in the
.gitlab-ci.ymlfile.For example
Comment #55
jonathan1055 commentedIf the actual quick releases of 11.4.8, 11.3.18 and 10.6.18 are going to be a problem, then in Gitlab Templates we could make the above changes to the
CORE_variables and release it as default and this would fix all Contrib projects right away. The values could be reverted when the real release is made.Just for info, I tested phpunit on Core 10.6.17 (previous major) and this fails as expected, because is installs Twig v3.30.0. However Core 11.3.17 (previous minor) does not fail, because it install Twig v3.27.1.
Comment #56
jonathan1055 commentedI've just tested in a CI pipeline with the three updated -dev core versions. All three load Twig 3.30 and Core
11.4.8-devand10.6.18-devphpunit tests now work correctly.But
11.3.18-devfails, with the originalTypeError: Twig\Runtime\EscaperRuntime::escape(): Argument #4 ($autoescape) must be of type bool, null given.So it appears that 11.3.17 wasn't broken before, but it is broken now.
Or maybe I'm missing something and have got this completely wrong. It would be good for someone else to confirm my findings.
Comment #57
longwaveThe fix did not get cherry picked to 11.3 so that makes sense. Let's do that as we will need to release 11.3 as well.
Comment #59
majorrobot commentedMR for 11.3.x with the fix cherry-picked is ready with passing pipelines.
Just needs a review!
Comment #61
catchMoving back to fixed.
Comment #63
catchI've tagged 11.4.8 and 10.6.18 which include the fix here.
I think 11.3 still has the patch level constraints for core-recommended so can probably wait for #3625998: Update CKEditor5 version for Drupal core 10.6, 11.3, 11.4, 11.5.
Comment #64
jonathan1055 commentedThanks. I confirm that phpunit jobs in Contrib pipelines using the new releases
11.4.8and10.6.18run OK and pass. The unchanged11.3.17is fine, as this still uses Twig 3.27. Here's my test pipeline with override variables.Gitlab Templates will be updated tomorrow - here is the issue and MR to update those core version variables
Comment #65
dadderley commentedA couple of days ago (Friday night), I updated a site from drupal 10.6.16 to 10.6.17.
Immediately got a WSOD with this error.
I pasted the error message info Google AI and it returned:
It gave this solution:
I did this downgrade and the site immediately came back to life.
Comment #66
xmacinfoDo we need Core to pin down twig versions?
Comment #67
elc commentedAll constraints were loosened on purpose after the restricted constraints prevented a dependency security update from being allowed to apply. The core-recommended project is now identical to using the core project.
@see
In an ideal world we would have the tested dependency constraints of core-recommended, with the ability to update them without needing to do a full core release. Perhaps automatically updated after a CI run (in this case they would have failed and not updated); triggered by the release of a dependency. I'm happy I caught this one on the dev sites, but I wasted a lot of time working on d.o contrib modules.
Comment #68
xmacinfoThanks for the reminder about loosening constraints. This makes it easy to update security fixes of those packages (the issues documented above) or rollback the packages when a package update cause a fatal error (this issue).
I am not sure how any CI run would have caught the problem.
Comment #69
longwaveYeah, we can't win here. If we pin dependencies then it's on us to update as soon as there is a security release; if we don't pin dependencies then we risk running into issues when there is any kind of BC break. On balance we think not pinning is slightly preferable as people want to be able to update as soon as there is a security issue.
The only way to catch this early would be to run tests against untagged prerelease versions of all dependencies, but currently we don't have a pipeline that does this, and even then we might not catch things in time.
Comment #70
mikelutz@catch I've got a bunch of sites using drupal/core:~11.3.0 would be nice to get an 11.3 hotfix. Is this fix backwards compatible with twig 3.29, or If I pin my projects to 3.29 will they break again with 11.3.18 comes out with this fix and I'm still pinned to twig 3.29?
Comment #71
jonathan1055 commentedGitlab Templates has been updated to use 11.4.8 and 10.6.18. Release
1.17.1is tagged anddefault-refupdated to point to this, so all Contrib using the default reference for their pipelines will now not get the Twig error and the phpunit jobs should run as before.Comment #72
gábor hojtsyJust noting here that @longwave made Drupal 11.3.18 available for those that do not use core-recommended about an hour ago at https://www.drupal.org/project/drupal/releases/11.3.18