Cloudflare ate our ETag
As part of the July latency arc, we built a proper conditional-request pipeline for vibe pages: an input-derived ETag on the root HTML, an If-None-Match fast path that answers 304 Not Modified without rendering, an SWR edge cache that serves conditionals from memory. Unit tests green, wrangler dev green, headers present in every environment we could see.
None of it ever fired for a real visitor. The response left our worker with a strong ETag and arrived at the browser with none. No validator, no If-None-Match on the next visit, no 304 — the whole pipeline was a well-tested corridor to a door that had been bricked over (#3213).
Three plausible fixes, three corpses
Theory one: streaming. Our HTML streams, and HTTP folklore says streamed responses lose length-dependent headers. The buffered asset path kept its ETag, which fit the theory perfectly. So we buffered the HTML, deployed to a PR preview, and curled the wire: still no ETag, still Transfer-Encoding: chunked — even on a bodyless HEAD. The fix that fit every unit test was falsified by one curl -sD - against a deployed host (PR #3228 has the whole trail).
The real mechanism, half-understood. Cloudflare's default-on HTML features mark every text/html response transformable — the edge might rewrite the body, so it discards validators that would no longer match the bytes. That explained the pattern: JS assets sailed through untouched, HTML lost its tag. Two features are famous for this, Email Obfuscation and Automatic HTTPS Rewrites. We also minted the validator weak (W/"…", etag-utils.ts) — the honest semantic anyway, since our tag hashes the render inputs, not the response bytes, and compression legitimately weakens strong tags.
Get posts like this in your inbox
One email field. Real updates. No algorithm required.
Theory three: the famous two. We turned both features off. The ETag still died on the wire — weak or strong, buffered or streamed.
The 304 that testified against the edge
The breakthrough was an asymmetry: a 304 served from the same cached entry kept its ETag while the 200 lost it. That's a perfect bisect. If the origin weren't sending the tag, neither response would have it; if the edge were eating it unconditionally, both would lose it. Only "the edge strips validators from transformable bodies" explains a bodyless 304 keeping what a 200 loses. The origin was innocent, and the culprit list was incomplete.
So instead of toggling dashboard checkboxes one page at a time, we dumped the zone's full settings API (GET /zones/:id/settings) and audited every entry. Two more default-on HTML rewriters surfaced, hiding outside the obvious dashboard spots: server_side_exclude and replace_insecure_js. Four features, all quietly marking HTML transformable; all four had to go off before a validator reached a browser (#3233, flipped on both zones on July 4). The tags flow now.
A few dead ends worth recording, because they'll tempt you too: the request-side Cache-Control: no-transform directive does not help. Response-side no-transform would work — but it also opts you out of brotli, so you'd trade validators for compression. And no repo credential can flip any of this: zone settings are dashboard-or-API-token territory, which is exactly why the failure lived in a layer no unit test could reach.
Never claim a header ships until you've seen it on the wire
The lesson we wrote into our runbooks is one line: a header doesn't exist until curl -sD - shows it on the deployed host. Unit tests exercise your code; wrangler dev exercises your worker; neither exercises the CDN's opinion of your content type. Between your return new Response(...) and the user's browser sits a stack of default-on features you never enabled, silently editing your headers — and the only instrument that sees the whole stack is a request from the outside.
The first fix passed every test we had and was wrong. The second was principled and insufficient. The third was the documented answer and still incomplete. What ended it wasn't a better theory — it was refusing to trust any layer's self-report and reading the full config plus the actual wire. When a header vanishes, don't re-litigate the famous causes: dump everything that's on, and let the 200/304 asymmetry tell you which side of the edge to blame.
The speed is the product
We chase every millisecond between your prompt and your running app — including the ones a CDN quietly steals.
Start building →