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:
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
catch