Configuration / Serving and Caching Settings
Static Asset Settings
Cache lifetimes for static assets, the response headers a request carries, and asset aggregation.
Static Asset Cache Lifetimes
assets/_headers sets Cache-Control on the files the asset layer serves. Without it every asset
answers public, max-age=0, must-revalidate, so a repeat view revalidates each stylesheet, script
and font.
| path | Cache-Control |
|---|---|
/agg/*.css, /agg/*.js |
public, max-age=31536000, immutable |
/core/*, /modules/*, /themes/* |
public, max-age=31536000 |
An aggregate's file name is the first 16 hex characters of the SHA-256 of its body
(scripts/pack-aggregates.ts), so a changed file gets a new URL and a year is safe. /agg/manifest.json
has no hash in its name and keeps the default.
The three Drupal prefixes get a year, which is what Drupal's own .htaccess allows
(drupal-src/.htaccess:42). The ?token Drupal appends to those URLs is not tied to the shipped
files. It comes from state key asset.css_js_query_string, and only drupal_flush_all_caches(), a
site install and the assets flush step of the update chain reset it (drupal-src/core/includes/common.inc:429,
src/site/php/updb/unit.php:252). Reconciliation does not, so a pack update that changes a /core
file leaves an existing site's URLs the same, and a visitor keeps the old copy until it expires or
the token changes.
_headers has no allow line in assets/.assetsignore and must not get one. Wrangler ignores it by
default and reads the rules from the directory; an allow line uploads it as an ordinary asset, and a
deployed worker then answers /_headers with the file (200, 282 bytes, measured on a deployed worker).
HTML comes from the Worker and is not affected. /robots.txt, /driver.json, /prefill.json,
/drupal-pf/* and /drupal-sql/* have no rule.
Request Headers
Drupal receives the request's headers, not only its cookie. Authorization, X-Api-Key, Origin,
User-Agent and any other header an API reads reach $request->headers and $_SERVER. Before this
the render was handed the cookie, Accept, the content type and the client address and nothing
else, so every bearer-token route on a headless site answered as anonymous.
Three rules keep that safe. A request that carries Authorization is treated like one that carries
a session: it is rendered per caller and never read from or written to the stored page, the KV tier or
the edge cache. An Authorization that is this deployment's owner token is dropped before PHP sees
it. cf-*, x-cfw-* and the hop-by-hop headers are dropped too, and at most 64 headers and 16 KiB
are forwarded, whole or not at all. The previous request's headers are removed from $_SERVER before
the next is built, so a token never outlives the request that carried it.
ASSET_AGGREGATES
Drupal's own css.preprocess and js.preprocess ship off, because the source CSS and JS files are not
in the interpreter's filesystem and its aggregate route answers a licence header alone. bun run assets:agg reads the library definitions at build time, where the files do exist, and emits immutable
per-library aggregates plus a file-to-library index. With ASSET_AGGREGATES=1, a page's contiguous run
of tags for a library is replaced by that library's single aggregate tag as the page is stored.
The substitution is all or nothing per library: either every tag in the run is replaced or none is. A partial replacement would store a page that references an aggregate for some of its CSS and not the rest.
It is on KV_OVERRIDABLE, so turning it on needs no redeploy.
The aggregates have to be published as well as built, or the lever cannot work at any setting.
assets/.assetsignore denies by default, so it must carry !/agg/. Without that the substitution
rewrites every asset tag to a URL the asset layer does not publish, the Worker answers it as a Drupal
path, and the page renders with no CSS. That is the same failure the all-or-nothing rule above exists
to prevent, arriving from the other side.
tests/unit/runtime/assets-ignore.spec.ts resolves every URL the substitution emits, so a page whose
stylesheets 404 fails the test suite.
Turning it on without running assets:agg changes nothing rather than breaking a page: an absent
manifest means no library matches.
What it buys is render time and stored bytes, not Worker requests. An individual core stylesheet,
such as /core/themes/olivero/css/base/base.css, is answered by the asset layer without running the
Worker, so the individual files already cost no Worker request.