Skip to content

fix(nginx): stop caching 404s for 30 days on legacy media - #141

Merged
ssavutu merged 1 commit into
mainfrom
fix/nginx-no-cache-404-headers
Aug 1, 2026
Merged

fix(nginx): stop caching 404s for 30 days on legacy media#141
ssavutu merged 1 commit into
mainfrom
fix/nginx-no-cache-404-headers

Conversation

@ssavutu

@ssavutu ssavutu commented Aug 1, 2026

Copy link
Copy Markdown
Member

Problem

The /wp-content/ block sets its cache header with add_header ... always. That flag applies the header to error responses too, so every 404 from the media tree went out with:

Cache-Control: public, max-age=2592000, immutable

Cloudflare took that at face value and pinned "this file does not exist" at the edge for 30 days.

The consequence: any file added to the corpus after something first requested it stays invisible for a month — on disk, served correctly by nginx, still 404 to every visitor. It reads as a failed copy rather than a cache hit, which is what makes it expensive to diagnose.

How it surfaced

Found while migrating wp-content/uploads/newsletter/ to CephFS. The files landed and the origin served them, but the edge kept replaying a 404 cached minutes before the copy finished:

Request Result
…/nl-header-1-1200x0.png 404, cf-cache-status: HIT, age: 35385
…/nl-header-1-1200x0.png?v=1 200 image/png, 90,191 bytes, cf-cache-status: MISS

Same path, same origin — the only difference is a cache key Cloudflare hadn't poisoned. Origin curl on Delta returned 200 throughout.

Fix

Drop always from the Cache-Control add_header only. Without the flag it applies to 2xx/3xx, so misses fall back to Cloudflare's short default 404 TTL and a later-added file self-heals.

X-Content-Type-Options and Content-Security-Policy keep always deliberately — those must apply to error responses.

Immutable caching for real images is unchanged.

Scope

  • Does not purge anything already cached; existing poisoned entries age out on their own (~Aug 30 for the newsletter paths).
  • Host-level config, so merging does not deploy it. Needs sudo cp + nginx -t + systemctl reload nginx on Delta.
  • Related workaround shipped separately in Scalene (CMS-Testing, 400337c): the /proxy/ route now retries once past an edge-cached 404. That rescues already-poisoned paths; this PR fixes the mechanism.

Verification after deploy

curl -sI http://localhost/wp-content/uploads/newsletter/does-not-exist-xyz.png | grep -i cache-control

Expected: no Cache-Control line on the 404, while a real image still returns public, max-age=2592000, immutable.

🤖 Generated with Claude Code

`add_header ... always` applies the header to error responses too, so every
404 from the /wp-content/ block went out with `Cache-Control: public,
max-age=2592000, immutable`. Cloudflare took that at face value and pinned
"this file does not exist" at the edge for a month.

The consequence is that any file added to the corpus AFTER something first
requested it stays invisible for 30 days -- on disk, served correctly by
this nginx, still 404 to every visitor. It reads as a failed copy rather
than a cache hit, which is what makes it expensive to diagnose.

Hit while migrating wp-content/uploads/newsletter/ to CephFS: the files
landed, the origin served them, and the edge kept replaying a 404 cached
minutes earlier. Confirmed by cf-cache-status: HIT with a stale age, while
the same path with a cache-busting query string returned 200.

Drop `always` from the Cache-Control add_header only. Without it the header
applies to 2xx/3xx, and misses fall back to Cloudflare's short default 404
TTL, so a later-added file self-heals. X-Content-Type-Options and
Content-Security-Policy keep `always` on purpose -- those must apply to
error responses.

This does not purge anything already cached; existing poisoned entries age
out on their own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ssavutu
ssavutu merged commit d559e63 into main Aug 1, 2026
6 checks passed
@ssavutu
ssavutu deleted the fix/nginx-no-cache-404-headers branch August 1, 2026 19:14
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.

1 participant