Problem/Motivation
Short video showing the problem: https://www.youtube.com/watch?v=NNC7vLwjswA
Steps to reproduce
- Clean install Drupal
- Install Admin
- Enable it as admin theme
- JS is now broken (see console + navigation doesn't work)
Note that clearing cache does not fix the issue, but disabling aggregation does fix the issue.
Errors below:
Note that the first file a 404. That has to be the root cause.
js_Br68L5jck8RMhQvkt…za2J-Nf8Z-KvCY.js:1
Failed to load resource: the server responded with a status of 400 ()
appearance:1 Refused to execute script from 'https://drupal-admin.ddev.site/sites/default/files/js/js_Br68L5jck8RMhQvktz…uage=en&theme=default_admin&libraries=eJxLzi9K1U8pKi1IzAlOLSnJzEsvBgBLhwey' because its MIME type ('text/plain') is not executable, and strict MIME type checking is enabled.
js_AIv6U2Md9_sRSnZbZ…LEnP0MkpyKwCbYwrR:4 Uncaught ReferenceError: drupalSettings is not defined
at js_AIv6U2Md9_sRSnZbZ…MkpyKwCbYwrR:4:1255
js_SHbVJ_ww_2CqgPwyB…8gsSNVPgjIAO9cGrg:2 Uncaught ReferenceError: drupalSettings is not defined
at js_SHbVJ_ww_2CqgPwyB…VPgjIAO9cGrg:2:1326
js_qWeh8V-sd8v4VBId4…Vcl_M9ugnjdUfQ9og:2 Uncaught ReferenceError: drupalSettings is not defined
at js_qWeh8V-sd8v4VBId4…9ugnjdUfQ9og:2:6740
Watchdog is showing the following error:
Type client error
Date Wednesday, 5 August 2026 - 08:40
User admin
Location https://drupal-admin.ddev.site/sites/default/files/js/js_Br68L5jck8RMhQvktzTv3SEbkgMaOza2J-Nf8Z-KvCY.js?language=en&libraries=eJxLzi9K1U8pKi1IzAlOLSnJzEsvBgBLhwey&scope=header&theme=default_admin
Referrer https://drupal-admin.ddev.site/admin/appearance
Message Symfony\Component\HttpKernel\Exception\BadRequestHttpException: Invalid filename. in Drupal\system\Controller\AssetControllerBase->getGroup() (line 245 of /var/www/html/core/modules/system/src/Controller/AssetControllerBase.php).
Severity Warning
Hostname 172.18.0.5
Operations I had Claude (Fable) troubleshoot, and it's indicating a core aggregation bug:
From Claude:
Diagnosis
The failing request is the header-scope aggregate for exactly one library, core/drupalSettings. I decoded the compressed libraries= query parameter.
The controller returns the 400 response from getGroup() because it resolves that library to zero header assets, so there is no group at delta 0 to serve.
The causal chain is:
-
default_admin/initis declared withheader: trueand depends oncore/drupalSettingsindefault_admin.libraries.yml:349.Contrib Gin has the identical
gin_initpattern, which explains why only these two themes reproduce the issue. Olivero and Stark have no header-scoped libraries, sodrupalSettingsLoader.jsremains in the footer and everything works. -
During page rendering,
AssetResolver::getJsAssets()expands header libraries with their dependencies inAssetResolver.php:390, socore/drupalSettingsis pulled into the header.Its loader file becomes its own aggregate group. Because the group maps one-to-one to a library,
JsCollectionOptimizerLazy::optimize()takes the new fast path atJsCollectionOptimizerLazy.php:139.The generated URL contains:
?scope=header&libraries=core/drupalSettingsrather than the full include, exclude, and delta context.
-
When the browser requests that URL,
JsAssetController::getGroups()re-resolves onlycore/drupalSettings, without its dependents.Header placement is computed solely from the
headerflag on the libraries being resolved inAssetResolver.php:353-358. However,core/drupalSettingsdoes not itself declareheader: true.Its header placement during the original render came entirely from
default_admin/init, which is absent from the regeneration request.As a result:
- The loader resolves to the footer.
- The header asset set is empty.
getGroup([], 0)throwsBadRequestHttpException('Invalid filename.').- The request returns a
400response with atext/plaincontent type. drupalSettings is not definederrors cascade throughout the page.
I confirmed this with a Drush simulation:
- Resolving
core/drupalSettingsby itself places the loader in the footer. - Resolving it through
default_admin/init, with dependencies, places it in the header.
This also explains the behavior described in comment #8. deliver() serves the file directly when it already exists on disk, as seen in AssetControllerBase.php:120. Therefore, manually creating the file works.
The hash remains deterministic because it is an HMAC of the aggregate group contents.
In summary, the libraries= aggregate URL fast path, introduced as part of the newer aggregate_target work on the main branch, is not safe for header-scope groups whose libraries are present in the header only transitively. The generated URL does not contain enough context to reproduce the original scope.
Fix options
-
Controller-side fix (recommended):
In
JsAssetController::getGroups(), when processing alibraries=request, trust the requestedscopequery parameter instead of recomputing the scope exclusively from the library definitions.The URL already asserts that these libraries formed a single group in that scope. This approach would also repair already-cached HTML that points to the broken URL.
One complication is that each item's scope contributes to the hash. The item scopes must therefore be changed to the requested scope before hashing; otherwise, the request could enter a redirect loop.
-
URL-generation fix:
In
JsCollectionOptimizerLazy, skip thelibraries=shortcut and fall back to the include-and-delta URL when both of the following are true:- The aggregate group is in the header scope.
- None of the group's libraries independently declare
header: true.
In other words, the shortcut should not be used when the group's scope cannot be reproduced by resolving its libraries independently.
Proposed next step
I can write the core patch and a regression test for the controller-side fix.
| Comment | File | Size | Author |
|---|---|---|---|
| #9 | Screenshot 2026-08-05 at 9.47.54 AM.png | 152.5 KB | bernardm28 |
| #9 | Screenshot 2026-08-05 at 8.57.00 AM.png | 76.98 KB | bernardm28 |
Issue fork drupal-3614911
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:
- 3614911-js-gets-broken
changes, plain diff MR !16568
Comments
Comment #2
cilefen commentedCan we get the browser console errors in the issue summary instead of only in a video?
Comment #3
mherchelHa. Thanks for asking. Added to the issue summary.
It looks like the root cause is the first aggregated JS file going 404.
Can anyone else reproduce? Is this a local problem for me?
Comment #4
jurgenhaasI can't reproduce this, neither on main nor on 11.4. But I've seen DDEV-specific caching issues in other contexts recently as well. People reported that a DDEV restart (or re-init?) fixed such stuff for them.
Comment #5
mherchelThanks for the info.
Still no luck
ddev delete -Oandddev configStill no luck
I also tried other themes.
I'm going to ask around on Slack to see if others can reproduce.
Comment #6
cilefen commentedDoes Drupal log anything?
Comment #7
mherchelGood question. Watchdog is showing the following error:
Comment #8
mherchelSome additional troubleshooting.
https://drupal-admin.ddev.site/sites/default/files/js/js_Br68L5jck8RMhQvktzTv3SEbkgMaOza2J-Nf8Z-KvCY.js?scope=header&language=en&theme=default_admin&libraries=eJxLzi9K1U8pKi1IzAlOLSnJzEsvBgBLhwey) causes a "Invalid filename." message in the browser (not sure if this is Drupal or the web server).js_Br68L5jck8RMhQvktzTv3SEbkgMaOza2J-Nf8Z-KvCY.jsfile does not existdrush si. I expected the filenames to be random.Comment #9
bernardm28 commentedI can confirm i get this issue on the main branch and on https://www.drupal.org/project/drupal/issues/3606571#comment-16719685
https://www.drupal.org/files/issues/2026-08-05/Screenshot%202026-08-05%2...
A good way of testing this issue is going to /admin/structure/menu. The chevrons are missing on the operations edit buttons and it throws a console error.
The error that it gives me when aggregation is on.
Comment #10
cilefen commentedThey are hashes, so not random. #3589208: Always use the file contents to determine asset aggregate filename hashes
You should experiment by disabling asset aggregation to verify that the asset system is the immediate problem.
Comment #11
cilefen commentedOh, you did say in the issue summary that disabling aggregation resolves this. Anyway, no, they are not random.
Comment #12
mherchelI had Claude (Fable) troubleshoot, and it's indicating a core aggregation bug:
From Claude:
Diagnosis
The failing request is the header-scope aggregate for exactly one library,
core/drupalSettings. I decoded the compressedlibraries=query parameter.The controller returns the
400response fromgetGroup()because it resolves that library to zero header assets, so there is no group at delta0to serve.The causal chain is:
default_admin/initis declared withheader: trueand depends oncore/drupalSettingsindefault_admin.libraries.yml:349.Contrib Gin has the identical
gin_initpattern, which explains why only these two themes reproduce the issue. Olivero and Stark have no header-scoped libraries, sodrupalSettingsLoader.jsremains in the footer and everything works.During page rendering,
AssetResolver::getJsAssets()expands header libraries with their dependencies inAssetResolver.php:390, socore/drupalSettingsis pulled into the header.Its loader file becomes its own aggregate group. Because the group maps one-to-one to a library,
JsCollectionOptimizerLazy::optimize()takes the new fast path atJsCollectionOptimizerLazy.php:139.The generated URL contains:
rather than the full include, exclude, and delta context.
When the browser requests that URL,
JsAssetController::getGroups()re-resolves onlycore/drupalSettings, without its dependents.Header placement is computed solely from the
headerflag on the libraries being resolved inAssetResolver.php:353-358. However,core/drupalSettingsdoes not itself declareheader: true.Its header placement during the original render came entirely from
default_admin/init, which is absent from the regeneration request.As a result:
getGroup([], 0)throwsBadRequestHttpException('Invalid filename.').400response with atext/plaincontent type.drupalSettings is not definederrors cascade throughout the page.I confirmed this with a Drush simulation:
core/drupalSettingsby itself places the loader in the footer.default_admin/init, with dependencies, places it in the header.This also explains the behavior described in comment #8.
deliver()serves the file directly when it already exists on disk, as seen inAssetControllerBase.php:120. Therefore, manually creating the file works.The hash remains deterministic because it is an HMAC of the aggregate group contents.
In summary, the
libraries=aggregate URL fast path, introduced as part of the neweraggregate_targetwork on the main branch, is not safe for header-scope groups whose libraries are present in the header only transitively. The generated URL does not contain enough context to reproduce the original scope.Fix options
Controller-side fix (recommended):
In
JsAssetController::getGroups(), when processing alibraries=request, trust the requestedscopequery parameter instead of recomputing the scope exclusively from the library definitions.The URL already asserts that these libraries formed a single group in that scope. This approach would also repair already-cached HTML that points to the broken URL.
One complication is that each item's scope contributes to the hash. The item scopes must therefore be changed to the requested scope before hashing; otherwise, the request could enter a redirect loop.
URL-generation fix:
In
JsCollectionOptimizerLazy, skip thelibraries=shortcut and fall back to the include-and-delta URL when both of the following are true:header: true.In other words, the shortcut should not be used when the group's scope cannot be reproduced by resolving its libraries independently.
Proposed next step
I can write the core patch and a regression test for the controller-side fix.
Comment #13
mherchelI'm currently having Claude write a patch and will verify it solves the issue.
I know @catch has been doing perf work involving aggregation lately. Will ping him in Slack when ready.
Comment #14
bernardm28 commentedDisabling aggregation solves the issue i can confirm that.
It seems that specifically the Aggregate JavaScript files has the issue.
If i turn that off the chevrons come back and the site seems to work as expected.
Comment #15
jurgenhaasConfirmed. I also missed the part about aggregation being part of the issue in the IS.
Now I can reproduce the issue as well when enabling JS aggregation. But only on main, not on 11.4
Comment #17
mherchelI had Claude (Fable) write the code in the MR. Although I can't speak for the code quality, I can verify that it does resolve the issue.
Here's what Claude had to say about this:
What the patch contains
The fix —
core/modules/system/src/Controller/JsAssetController.php.For
?libraries=aggregate requests,getGroups()no longer trusts the recomputed header/footer split. A library such ascore/drupalSettingsonly appears in the header transitively (viadefault_admin/init'sheader: true), and that page context is unavailable when the controller re-resolves the library on its own.Instead, the controller:
AssetResolver::sort()to restore the original render-time order.This ensures the generated hash matches the render-time hash exactly, allowing the aggregate to be served successfully and written to disk.
Regression test — a new
header_assets_testtest module.The module defines a
header: truelibrary that depends oncore/drupal(and thereforecore/drupalSettings) and attaches it to every page usinghook_page_attachments().It is wired into
AssetOptimizationTestwith an assertion verifying that the page generates a header aggregate for transitively-header libraries. The same coverage also applies toAssetOptimizationUmamiTest, which inherits from it.Validation
on the generated header aggregate.
js_Br68…aggregate URL now returns200 text/javascript, is written to disk successfully, and both/admin/contentand/admin/modulesload without any JavaScript console errors.commit-code-check.shpasses, including:Caveats
assertBrotli()fails locally regardless of this patch. Drupal core CI passes these assertions as expected.extension_discovery_scan_testsand theheader_assets_testmodule on your development site. Those changes have since been reverted, and the site is back to its originalolivero/default_adminconfiguration.Comment #18
mherchelUpdating IS.
Comment #19
mherchelSetting this to Major since it breaks what will become Drupal's default Admin theme.
Comment #20
mherchelComment #21
mherchelComment #22
jurgenhaasThis is looking good. I've tested the failing scenario locally, and the bug is gone away.
I've reviewed the code in
core/modules/system/src/Controller/JsAssetController.phpand it looks correct, although I have to admit that I'm not an asset aggregation expert and don't know if this would have any unintended side-effects. But it looks as if it just closes a gap and that shouldn't cause any harm.But I leave it at NR for others to also have a look.
Comment #23
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #24
mherchelFrom @catch in Slack https://drupal.slack.com/archives/C079NQPQUEN/p1785941457459889?thread_t...
Comment #25
mherchelFixing my bad grammar title with something more descriptive.
Comment #26
catchDon't have time for an in-depth review today, but overall I think that the diagnosis is probably correct here but not overly keen on either of the suggested fixes, from the inline comment:
Given the asset will have all assets from all libraries, I think rather than forcing scope to be what's in the URL, we should ignore scope entirely and grab all the assets, which will mean we don't need to set scope in the URL at all. Should result in the same resulting file in the end and ought to be easier to follow.
When using the include/exclude pattern we'll still need to use scope in the URL, but once we have before/after fully implemented and weights deprecated/removed we should be able to remove includes/exclude entirely.
Comment #27
catchcrosspost...
Comment #28
catchComment #29
mherchelI'm going to leave this for people who understand the code a bit better. I don't want to be a meat puppet for Claude, unless absolutely needed
Comment #30
catchPushed a commit for #26.
I didn't review the test coverage properly yet, however the new code passes the new test coverage. I did have to make one small change - the test looks for scope to determine header libraries but it won't always be there now. This may cause different test failures, but hopefully only test expectations rather than real ones..
Comment #31
catchHad a quick look at the tests. It mostly looked OK except for the filler content in the test file asset which claimed it needed to be there because web browsers skip compression under a certain length, removed it and the tests pass without it so I think that is probably Claude making things over complicated for no reason.
With the scope removed we can't absolutely be sure that the header js file being looked for is actually in the header (e.g. it could actually be in the footer instead), but we're really testing whether the header file produces a 400 response or not so I think that's probably OK? If not we'd have to parse the HTML to get URLs only before the body tag or similar. But we have more general test coverage that header files end up in the header I think.
I do get test failures running that test locally due to lack of brotli support in ddev's PHP but that's pre-existing and not affected by the MR. There's an open issue for it here but it's blocked on upstream https://github.com/ddev/ddev/issues/8549
Comment #32
catchThis completely breaks js in default_admin so moving to critical.
Comment #33
mherchelTested in default_admin, and with the MR, the JS works again in default-admin.
Thank you!
Comment #34
godotislate2 comments on the MR.
Comment #35
catchApplied both of the suggestions because they looked reasonable, but now the tests are failing.
Comment #36
godotislateFound the issue. In core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php, this change
was removing scope from the JS aggregrate URL query string, so changing it back to
fixed it.
I've confirmed in browser with standard install + default_admin that aggregated JS has no console errors.
Tests are green again, and test only fails as expected: https://git.drupalcode.org/project/drupal/-/jobs/11644653
Comment #37
catchIt should be OK to remove the scope from the query string when libraries is set, because it's ignored now in that case.
Comment #38
godotislateOh, OK, so maybe the issue is with the test.
When I debugged, before adding the scope back to the query params, none of
$script_urlscontained a scope, so$header_dependency_aggregatesends up being empty. Do we need to do something like query the DOM to see if there are script tags in the<head>for the expected aggregated JS files?Comment #39
catchSee #31 I made the check a bit looser since we're really checking whether the header is a 400. Other tests check whether files end up in the header. If we parsed the script tags it would be better but not sure it's necessary to demonstrate the bug is fixed. Test only run should definitely fail.
Comment #40
godotislateOK, removed scope from query string again. Adjusted the test just to make sure there's one script URL that has
core/drupal. Test only still fails as expected with 400: https://git.drupalcode.org/project/drupal/-/jobs/11647160Comment #41
mherchelVerified no console errors with the new changes. Tests are passing!
Comment #44
godotislateI only made a pretty minor test change, so I think I can commit this.
Committed and pushed 668d8be to main and e29fc19 to 11.x. Thanks!
Comment #46
mherchelAny chance we can get this into 11.4.x?
We can't wait for 11.5 if we want module maintainers to test their UIs in Default Admin before D12. And this bug breaks the theme .
Comment #47
catch@mherchel the issue that introduced this is only in 11.x, not 11.4: #3565258: Support library-specific aggregates