Skip to content

feat: let the CDN keep a purgeable response until it is purged - #2216

Open
netomi wants to merge 1 commit into
feat/cdn-purgefrom
feat/cdn-surrogate-ttl
Open

netomi wants to merge 1 commit into
feat/cdn-purgefrom
feat/cdn-surrogate-ttl

Conversation

@netomi

@netomi netomi commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Third step towards #1889. Stacked on #2215 (which is stacked on #2214) — review those first; this PR's own diff is one class, one property and its tests.

What changes

With purging in place, the responses about one extension or namespace no longer have to expire to stop being wrong — they are dropped when they stop being true. So the CDN may keep them for a week instead of asking the registry every time.

Cache-Control:     no-cache, public        ← unchanged; browsers still revalidate every time
Surrogate-Control: max-age=604800          ← new; the CDN keeps it until it is purged

Surrogate-Control rather than a longer Cache-Control, because a browser cache cannot be purged: a week there would pin an old version in front of a reader with no way to reach them. The CDN reads the header and strips it before the response reaches anyone else, so what changes is not how often browsers revalidate — it is who answers the revalidation.

Where it applies, and why that list

A ResponseBodyAdvice adds the header only where all of this holds:

condition what it rules out
purging is configured nothing would drop it, so nothing may keep it for a week
the response carries a surrogate key /api/-/search, /api/-/query, extensionquery — about no single extension, so nothing to purge them by
its endpoint marked it public authenticated responses. /api/{namespace}/verify-pat sets no Cache-Control at all, and is left alone
a GET that succeeded writes, and errors

The third one is the important one: the endpoint has already decided whether a shared cache may hold its response, and this only extends how long — it never makes something cacheable that was not.

ResponseBodyAdvice rather than an interceptor because postHandle runs after a large body may already have been flushed, and a committed response takes no further headers; beforeBodyWrite runs while they are still ours to set.

Configuration

property default
ovsx.cdn.surrogate-cache-duration P7D

Since what the CDN holds is dropped when it stops being true, the duration bounds how long a purge that never landed stays visible, rather than how fresh the registry looks. Zero turns the header off.

Not included

File TTLs. StorageUtil.getCacheControl still caps files at 1 day, down from 30, with a TODO explaining that CDN invalidation was missing — but files are served by redirect to storage, so their cached response carries no surrogate key and is not purgeable. Raising that number needs the key stored as blob metadata at upload time, which is its own change.

Testing

SurrogateCacheControlAdviceTest drives the advice through the tagging interceptor, since what it does depends on what that put there: a purgeable response gets the header; an untagged one, a response never marked public, a failed one, a write, and a registry with no configured provider do not.

Full server suite green (1443 tests); pre-commit hooks pass.

One thing worth flagging from building this: the advice is picked up by @WebMvcTest slices, where its configuration bean is not, and the first version of it took CdnPurgeConfig by constructor — which broke 389 tests across every slice. It resolves the bean through an ObjectProvider instead, so a context without it is simply a context with no CDN.

🤖 Generated with Claude Code

Third step of #1889. With the purge in place, the responses about one
extension or namespace no longer have to expire to stop being wrong - they
are dropped when they stop being true - so the CDN may keep them for a week
instead of revalidating against the registry every time.

Surrogate-Control rather than a longer Cache-Control, because a browser cache
cannot be purged: a week there would pin an old version in front of a reader
with no way to reach them. The CDN reads the header and strips it, so the
endpoints keep the Cache-Control they had - most of these say no-cache, and
browsers go on revalidating every time. What changes is who answers the
revalidation.

Applied only where all of it holds: purging is configured, so something will
actually drop it; the response carries a surrogate key, so there is a key to
drop it by; and its endpoint marked it public, so an authenticated response
is left alone. A GET that succeeded, at that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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