Skip to content

fix(web): Workers appends header values, so no-cache defeated every immutable asset - #334

Merged
rainmanjam merged 1 commit into
mainfrom
fix/workers-headers-concatenate
Aug 14, 2026
Merged

fix(web): Workers appends header values, so no-cache defeated every immutable asset#334
rainmanjam merged 1 commit into
mainfrom
fix/workers-headers-concatenate

Conversation

@rainmanjam

Copy link
Copy Markdown
Owner

A regression from my own Workers migration (#332), found by measuring the live site — no test failed.

web/public/_headers set Cache-Control: no-cache on /* and relied on later rules replacing it. Its comment said so: "a later rule overrides an earlier one for the same header name." That was true for Pages. Workers Static Assets concatenates:

/_astro/*.css   no-cache, public, max-age=31536000, immutable
/fonts/*.woff2  no-cache, public, max-age=604800

RFC 9111 gives no-cache the last word. Every immutable asset revalidated on every navigation.

Measured cost: 11 of 11 assets returning 304 on repeat navigation; the render-blocking stylesheet costing ~588 ms in front of first paint on each internal navigation over Slow 4G.

Nothing failed. Nothing looked wrong. It was merely slower — which is why the migration passed review.

The guard enforces the mechanical rule that follows: no rule may set a header /* also sets, because the specific rule doesn't win, it joins. Disjoint paths setting the same header are fine (/fonts/* and /shots/* never match one request), so it checks for a catch-all rather than duplication. Proven able to fail by restoring the old line.

The comment that misled wasn't wrong when written. It documented Pages accurately, and I changed the platform underneath it.

…mmutable asset

A regression from my own migration to Workers Static Assets, found by measuring
the live site rather than by any test failing.

web/public/_headers set `Cache-Control: no-cache` on /* and relied on the later,
more specific rules replacing it. Its own comment said so: "a later rule
overrides an earlier one for the same header name, which is why the cache rules
come after the catch-all." That was TRUE FOR PAGES. Workers Static Assets
CONCATENATES, so the live site served every content-hashed asset as

    Cache-Control: no-cache, public, max-age=31536000, immutable

and RFC 9111 gives `no-cache` the last word: revalidate before use. Confirmed
against the running site -- /_astro/*.css and /fonts/*.woff2 both carry the
joined value.

Measured cost: 11 of 11 assets returning 304 on a repeat navigation, and the
stylesheet -- the only render-blocking resource -- costing about 588ms in front
of first paint on every internal navigation over Slow 4G. Nothing failed.
Nothing looked wrong. It was merely slower than it had been, in a way only a
measurement would show, which is why the migration passed review.

The security headers stay on /* because they belong on everything.
Cache-Control is now stated once per path class, with the HTML rule scoped to
the HTML instead of to /*.

TestNoTwoHeaderRulesSetTheSameHeader enforces the narrow mechanical rule that
follows: no rule may set a header that /* also sets, because on Workers the
specific rule does not win, it joins. Disjoint paths setting the same header are
fine -- /fonts/* and /shots/* never match one request -- so the guard checks for
a catch-all rather than for duplication. Proven able to fail by restoring
`Cache-Control: no-cache` to the /* block.

The comment that misled was not wrong when it was written. It documented Pages
semantics accurately and I changed the platform underneath it.
Copilot AI lite review requested due to automatic review settings August 14, 2026 03:13
@sonarqubecloud

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a performance regression in the web deploy by restructuring Cloudflare _headers so that Cache-Control is never unintentionally concatenated onto immutable, content-hashed assets under Workers Static Assets (where header values append rather than override).

Changes:

  • Updates web/public/_headers to scope Cache-Control: no-cache to HTML routes only, while keeping long-lived caching for content-hashed/static assets.
  • Adds a Go guard test that fails if the catch-all /* rule sets any header that is also set by a more specific rule (preventing reintroduction of the concatenation bug).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
web/public/_headers Rescopes cache-control rules to avoid Workers header concatenation defeating immutable caching.
internal/testenv/pagesdeploy_test.go Adds a regression guard to prevent /* from setting headers that are also set elsewhere in _headers.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread web/public/_headers
Comment on lines +31 to +33
# So no rule may set a header that a later rule also sets. The security headers
# stay on /* because they belong on everything; Cache-Control is now stated once
# per path class, and the HTML rule is scoped to the HTML rather than to /*.
Comment on lines +555 to +561
// The rule this enforces is narrow and mechanical: no two rules may set the same
// header, because on Workers the second does not win -- it joins.
//
// Proven able to fail against the committed tree by adding `Cache-Control:
// no-cache` back to the `/*` block: this reports /_astro/* and /* both setting
// Cache-Control.
func TestNoTwoHeaderRulesSetTheSameHeader(t *testing.T) {
@rainmanjam
rainmanjam merged commit e6704ad into main Aug 14, 2026
29 checks passed
@rainmanjam
rainmanjam deleted the fix/workers-headers-concatenate branch August 14, 2026 03:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants