Skip to content

Commit 863c0dd

Browse files
Abhishek Gargclaude
andcommitted
fix(aso-overlay): add Surrogate-Key header so Fastly purge actually invalidates (SITES-48140)
Alina's live-repro on mystique#3381 caught that our Fastly purge is currently a silent no-op: - Fastly's `POST /service/{sid}/purge/<path>` endpoint is a SURROGATE-KEY purge (the path after `/purge/` is the key name), not a URL purge. - Our 200 response emitted no Surrogate-Key header. - So mystique was purging a key nothing was tagged with -> HTTP 200 + zero effect (verified by `x-cache=HIT` age incrementing instead of resetting). The URL-purge alternative (`POST /purge/{host}{path}`) is blocked by the VCL `recv 40` aso-overlay-route auth gate (purge requests carry no X-ASO-API-Key). Fix: emit `Surrogate-Key: aso-overlay-<service>` on both 200 and 304 responses. Mystique#3381 then purges that exact key via the same endpoint it's already calling, which now does something real. Key format: - `aso-overlay-<service>` where <service> is the CM identifier (cm-p<N>-e<N>) - Origin route is `/config/:service/redirects.txt` (Fastly strips tier prefix), so per-service uniqueness is sufficient — no tier segment needed. - Namespace prefix `aso-overlay-` guards against key collisions with future overlays under other routes. Coordination with mystique#3381: Maksym updates `fastly_purge.py` to call `POST /service/{sid}/purge/aso-overlay-<service>` (drop the full URL path; use the same key format). Unit tests there still pass because they mock the HTTP call — end-to-end verification requires a live Fastly service. Also included on the 304 response so any stragglers still cached at that status get purged in the same call (RFC 7232 requires cache-control + etag on 304; Surrogate-Key is an operationally-linked companion). Tests: 54 passing (added Surrogate-Key assertions to the 200 and 304 tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5c65b58 commit 863c0dd

2 files changed

Lines changed: 17 additions & 0 deletions

File tree

src/controllers/redirects.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,11 @@ function RedirectsController(ctx) {
272272
'content-type': 'text/plain; charset=utf-8',
273273
...(etag ? { etag } : {}),
274274
'cache-control': OVERLAY_CACHE_CONTROL,
275+
// Same Surrogate-Key as the 200 so any 304 stragglers still on the
276+
// old TTL can be purged by the same call. Fastly stores the header
277+
// for edge state; RFC 7232 requires we carry cache-control + etag,
278+
// and Surrogate-Key is an operationally-linked companion.
279+
'surrogate-key': `aso-overlay-${service}`,
275280
});
276281
}
277282

@@ -286,6 +291,12 @@ function RedirectsController(ctx) {
286291
'cache-control': OVERLAY_CACHE_CONTROL,
287292
// Include ETag so a subsequent poll can conditionally revalidate.
288293
...(etag ? { etag } : {}),
294+
// Fastly surrogate key so Mystique can targeted-purge this tenant's
295+
// overlay on Deploy (see mystique#3381). Namespaced with `aso-overlay-`
296+
// prefix so future overlays under different routes don't collide with
297+
// this key space. Fastly VCL strips the /config/<tier>/ prefix before
298+
// reaching origin, so we only need per-service uniqueness (not per-tier).
299+
'surrogate-key': `aso-overlay-${service}`,
289300
});
290301
} catch (err) {
291302
const code = err.$metadata?.httpStatusCode;

test/controllers/redirects.test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ describe('RedirectsController', () => {
9898
expect(response.headers.get('content-type')).to.equal('text/plain; charset=utf-8');
9999
expect(response.headers.get('cache-control')).to.equal('max-age=10');
100100
expect(response.headers.get('etag')).to.equal(ETAG);
101+
// Surrogate-Key enables Mystique to targeted-purge this tenant's overlay
102+
// via Fastly's key-purge endpoint (POST /service/<sid>/purge/<key>) after
103+
// a successful S3 write. See mystique#3381.
104+
expect(response.headers.get('surrogate-key')).to.equal(`aso-overlay-${SERVICE}`);
101105
expect(await response.text()).to.equal(overlay);
102106
// Resolves the site by the p<program>/e<env> external ids parsed from the service.
103107
expect(mockDataAccess.Site.findByExternalOwnerIdAndExternalSiteId
@@ -154,6 +158,8 @@ describe('RedirectsController', () => {
154158
expect(response.headers.get('etag')).to.equal(ETAG);
155159
expect(response.headers.get('cache-control')).to.equal('max-age=10');
156160
expect(response.headers.get('content-type')).to.equal('text/plain; charset=utf-8');
161+
// Surrogate-Key echoed on 304 so purges hit both HIT and NOT_MODIFIED entries.
162+
expect(response.headers.get('surrogate-key')).to.equal(`aso-overlay-${SERVICE}`);
157163
expect(await response.text()).to.equal('');
158164
// Body never deserialized when returning 304 — the transformToString cost is elided.
159165
expect(bodyStub.called).to.be.false;

0 commit comments

Comments
 (0)