Problem/Motivation

Short video showing the problem: https://www.youtube.com/watch?v=NNC7vLwjswA

Steps to reproduce

  1. Clean install Drupal
  2. Install Admin
  3. Enable it as admin theme
  4. 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:

  1. default_admin/init is declared with header: true and depends on core/drupalSettings in default_admin.libraries.yml:349.

    Contrib Gin has the identical gin_init pattern, which explains why only these two themes reproduce the issue. Olivero and Stark have no header-scoped libraries, so drupalSettingsLoader.js remains in the footer and everything works.

  2. During page rendering, AssetResolver::getJsAssets() expands header libraries with their dependencies in AssetResolver.php:390, so core/drupalSettings is 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 at JsCollectionOptimizerLazy.php:139.

    The generated URL contains:

    ?scope=header&libraries=core/drupalSettings

    rather than the full include, exclude, and delta context.

  3. When the browser requests that URL, JsAssetController::getGroups() re-resolves only core/drupalSettings, without its dependents.

    Header placement is computed solely from the header flag on the libraries being resolved in AssetResolver.php:353-358. However, core/drupalSettings does not itself declare header: 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) throws BadRequestHttpException('Invalid filename.').
    • The request returns a 400 response with a text/plain content type.
    • drupalSettings is not defined errors cascade throughout the page.

I confirmed this with a Drush simulation:

  • Resolving core/drupalSettings by 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 a libraries= request, trust the requested scope query 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 the libraries= 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.

Issue fork drupal-3614911

Command icon 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

mherchel created an issue. See original summary.

cilefen’s picture

Can we get the browser console errors in the issue summary instead of only in a video?

mherchel’s picture

Issue summary: View changes

Ha. 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?

jurgenhaas’s picture

I 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.

mherchel’s picture

Thanks for the info.

Still no luck

  1. I upgraded ddev to the latest
  2. Did a ddev delete -O and ddev config

Still no luck

I also tried other themes.

  • I'm unable to reproduce the issue with either Olivero or Stark
  • I was able to reproduce the issue with Gin (contrib)

I'm going to ask around on Slack to see if others can reproduce.

cilefen’s picture

Does Drupal log anything?

mherchel’s picture

Issue summary: View changes

Good question. 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	
mherchel’s picture

Some additional troubleshooting.

  • Going to the URL (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).
  • The js_Br68L5jck8RMhQvktzTv3SEbkgMaOza2J-Nf8Z-KvCY.js file does not exist
  • If I manually create the file, the file will load as expected (which means the filename format works just fine).
  • The filename does not change when I do a drush si. I expected the filenames to be random.
bernardm28’s picture

I 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.

missing chevrons

The error that it gives me when aggregation is on.

Uncaught ReferenceError: drupalSettings is not defined
    at js_1dF6Mh9GvTVkJTBBCP1paeYTVN-JV5zTEQdjl8ivqaY.js?scope=footer&language=en&theme=default_admin&libraries=eJxLzi9K1c_JT0zJKtZJBrFTikoLEnP0MkpyKwCbYwrR:4:1255
js_LnBtFYOj5NfP8x5BvJjY2zy-H9PFWvDUh_PL5byfn4M.js?scope=footer&language=en&theme=default_admin&libraries=eJxLykyPL8gsSNVPgjIAO9cGrg:2 Uncaught ReferenceError: drupalSettings is not defined
    at js_LnBtFYOj5NfP8x5BvJjY2zy-H9PFWvDUh_PL5byfn4M.js?scope=footer&language=en&theme=default_admin&libraries=eJxLykyPL8gsSNVPgjIAO9cGrg:2:1326
js_DM9n59Vhx1-DPvrJaRnVYqRlE-LKqJEKgXm0eDqBCFY.js?scope=footer&language=en&theme=default_admin&libraries=eJxLzs8rSa0oKU3M0U8pKi1IzNFLhovo5mTmZRfrJOcXpcIkE5NLMstSwRIo4gVF-elFqcXFAOCzHwI:2 Uncaught ReferenceError: drupalSettings is not defined
    at js_DM9n59Vhx1-DPvrJaRnVYqRlE-LKqJEKgXm0eDqBCFY.js?scope=footer&language=en&theme=default_admin&libraries=eJxLzs8rSa0oKU3M0U8pKi1IzNFLhovo5mTmZRfrJOcXpcIkE5NLMstSwRIo4gVF-elFqcXFAOCzHwI:2:6740
js_1e6PW4sXdKfOxbrajBZ45zGz02RUlQNHSYbJMfT50r0.js?scope=footer&language=en&theme=default_admin&libraries=eJxtzmEOgzAIBeALGT2SQcoMrqMMqJm3n4tb4hp_8vHyAIvRkKwq5B4WeHX4AZYgk52WZyXbxsqHf4OJXTMg_eER7bU4BxfpEt2g5hghPVgGQCSJpgVymS_o9MjPrehUI_baMwdMmYxciziv1JwkR1A6hmYlsPIMF296MN63NxSVa4o:2 Uncaught ReferenceError: drupalSettings is not defined
    at js_1e6PW4sXdKfOxbrajBZ45zGz02RUlQNHSYbJMfT50r0.js?scope=footer&language=en&theme=default_admin&libraries=eJxtzmEOgzAIBeALGT2SQcoMrqMMqJm3n4tb4hp_8vHyAIvRkKwq5B4WeHX4AZYgk52WZyXbxsqHf4OJXTMg_eER7bU4BxfpEt2g5hghPVgGQCSJpgVymS_o9MjPrehUI_baMwdMmYxciziv1JwkR1A6hmYlsPIMF296MN63NxSVa4o:2:19455
js_1jRH8QsOCn68JOJjtI6RyJOkufMvJ3cq56en7mVRq44.js?scope=footer&language=en&theme=default_admin&libraries=eJxLzi9K1U8pKi1IzAEAGfkEYQ:2 Uncaught TypeError: Drupal.displace is not a function
    at initDisplace (js_P4YdJzuKD82kzPDAsChRH0C1Dcckr1Jg7yleHAxsuto.js?scope=footer&language=en&theme=default_admin&libraries=eJxtyksKwEAIBNEL5XMmYcwgSLc4Mrl-ltm4rVeQbVPKiNtQmhC_RjIGXxxosEgvi9aCwa3Z2pJHz0oTTNf2-NsHW_I7GA:10:2115)
    at Array.forEach (<anonymous>)
    at Object.attach (js_P4YdJzuKD82kzPDAsChRH0C1Dcckr1Jg7yleHAxsuto.js?scope=footer&language=en&theme=default_admin&libraries=eJxtyksKwEAIBNEL5XMmYcwgSLc4Mrl-ltm4rVeQbVPKiNtQmhC_RjIGXxxosEgvi9aCwa3Z2pJHz0oTTNf2-NsHW_I7GA:10:2255)
    at js_1jRH8QsOCn68JOJjtI6RyJOkufMvJ3cq56en7mVRq44.js?scope=footer&language=en&theme=default_admin&libraries=eJxLzi9K1U8pKi1IzAEAGfkEYQ:2:427
    at Array.forEach (<anonymous>)
    at Drupal.attachBehaviors (js_1jRH8QsOCn68JOJjtI6RyJOkufMvJ3cq56en7mVRq44.js?scope=footer&language=en&theme=default_admin&libraries=eJxLzi9K1U8pKi1IzAEAGfkEYQ:2:353)
    at js_1jRH8QsOCn68JOJjtI6RyJOkufMvJ3cq56en7mVRq44.js?scope=footer&language=en&theme=default_admin&libraries=eJxLzi9K1U8pKi1IzAEAGfkEYQ:3:373
    at HTMLDocument.listener (js_1jRH8QsOCn68JOJjtI6RyJOkufMvJ3cq56en7mVRq44.js?scope=footer&language=en&theme=default_admin&libraries=eJxLzi9K1U8pKi1IzAEAGfkEYQ:3:161)
cilefen’s picture

They 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.

cilefen’s picture

Oh, you did say in the issue summary that disabling aggregation resolves this. Anyway, no, they are not random.

mherchel’s picture

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:

  1. default_admin/init is declared with header: true and depends on core/drupalSettings in default_admin.libraries.yml:349.

    Contrib Gin has the identical gin_init pattern, which explains why only these two themes reproduce the issue. Olivero and Stark have no header-scoped libraries, so drupalSettingsLoader.js remains in the footer and everything works.

  2. During page rendering, AssetResolver::getJsAssets() expands header libraries with their dependencies in AssetResolver.php:390, so core/drupalSettings is 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 at JsCollectionOptimizerLazy.php:139.

    The generated URL contains:

    ?scope=header&amp;libraries=core/drupalSettings

    rather than the full include, exclude, and delta context.

  3. When the browser requests that URL, JsAssetController::getGroups() re-resolves only core/drupalSettings, without its dependents.

    Header placement is computed solely from the header flag on the libraries being resolved in AssetResolver.php:353-358. However, core/drupalSettings does not itself declare header: 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) throws BadRequestHttpException('Invalid filename.').
    • The request returns a 400 response with a text/plain content type.
    • drupalSettings is not defined errors cascade throughout the page.

I confirmed this with a Drush simulation:

  • Resolving core/drupalSettings by 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 a libraries= request, trust the requested scope query 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 the libraries= 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.

mherchel’s picture

I'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.

bernardm28’s picture

Disabling 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.

jurgenhaas’s picture

Confirmed. 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

mherchel’s picture

I 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

  1. The fixcore/modules/system/src/Controller/JsAssetController.php.

    For ?libraries= aggregate requests, getGroups() no longer trusts the recomputed header/footer split. A library such as core/drupalSettings only appears in the header transitively (via default_admin/init's header: true), and that page context is unavailable when the controller re-resolves the library on its own.

    Instead, the controller:

    • Merges both the resolved header and footer scopes.
    • Re-sorts the assets with AssetResolver::sort() to restore the original render-time order.
    • Stamps each asset with the scope provided in the request URL.

    This ensures the generated hash matches the render-time hash exactly, allowing the aggregate to be served successfully and written to disk.

  2. Regression test — a new header_assets_test test module.

    The module defines a header: true library that depends on core/drupal (and therefore core/drupalSettings) and attaches it to every page using hook_page_attachments().

    It is wired into AssetOptimizationTest with an assertion verifying that the page generates a header aggregate for transitively-header libraries. The same coverage also applies to AssetOptimizationUmamiTest, which inherits from it.

Validation

  • With the fix reverted, the regression test fails with the exact signature of this bug:
    Current response status code is 400, but 200 expected.

    on the generated header aggregate.

  • With the fix applied, the full test suite passes (245 assertions), including the hash-mismatch redirect assertions, confirming there are no redirect loops.
  • On the reproduced DDEV site, the previously failing js_Br68… aggregate URL now returns 200 text/javascript, is written to disk successfully, and both /admin/content and /admin/modules load without any JavaScript console errors.
  • commit-code-check.sh passes, including:
    • CSpell
    • PHPStan
    • PHPCS
    • ESLint

Caveats

  • The Brotli/Gzip compression assertions are environment-sensitive. Your local DDEV Nginx instance does not include the Brotli module, so assertBrotli() fails locally regardless of this patch. Drupal core CI passes these assertions as expected.
  • To reproduce the multi-library case, I temporarily enabled extension_discovery_scan_tests and the header_assets_test module on your development site. Those changes have since been reverted, and the site is back to its original olivero/default_admin configuration.
mherchel’s picture

Issue summary: View changes

Updating IS.

mherchel’s picture

Priority: Normal » Major
Status: Active » Needs review

Setting this to Major since it breaks what will become Drupal's default Admin theme.

mherchel’s picture

Component: Admin theme » aggregator.module
mherchel’s picture

Component: aggregator.module » asset library system
jurgenhaas’s picture

This 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.php and 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.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new979 bytes

The 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.

mherchel’s picture

From @catch in Slack https://drupal.slack.com/archives/C079NQPQUEN/p1785941457459889?thread_t...

If the controller fix is correct, that's the better way to fix it because using ?libraries results in much better browser/CDN cache hit rates. It seems like it probably ought to be OK because if it's in the header with another library, it's in the header, it's not going to suddenly move unless the library definitions change.

We should try to get rid of header javascript in default_admin though, separate issue of course.
[10:53 AM]Pretty late here so can't do a proper code review (and out most of tomorrow and the day after too)

mherchel’s picture

Title: JS gets broken when enabling default_admin on clean install » JS aggregation returns 400 for libraries loaded in the header only as dependencies

Fixing my bad grammar title with something more descriptive.

catch’s picture

Title: JS aggregation returns 400 for libraries loaded in the header only as dependencies » JS gets broken when enabling default_admin on clean install
Component: asset library system » Admin theme
Priority: Major » Normal
Status: Needs work » Active

Don'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:

 // A 'libraries' query argument means the aggregate contains every JS
      // asset of the listed libraries, all in the same scope. Whether an asset
      // is resolved to the header or the footer depends on the full set of
      // libraries attached to the page it was rendered on: a library without
      // 'header: true' in its definition is still loaded in the header when a
      // header library depends on it. That page context is not available here,
      // so the resolved scope cannot be trusted. Use all resolved assets and
      // set the scope from the URL, restoring the render-time sort order so
      // the generated hash matches.

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.

catch’s picture

Title: JS gets broken when enabling default_admin on clean install » JS aggregation returns 400 for libraries loaded in the header only as dependencies
Component: Admin theme » asset library system
Priority: Normal » Major

crosspost...

catch’s picture

Status: Active » Needs work