Skip to content

Commit 7e17085

Browse files
vibeguiclaude
andauthored
feat(proxy): add cacheControl override; force immutable on VTEX /files/*?v=* (#1590)
The generic proxy handler in website/handlers/proxy.ts passes upstream Cache-Control verbatim. For VTEX-proxied versioned static assets like /files/checkout6-custom.css?v=920636e4 the upstream sends short TTLs, even though the `?v=HASH` query param makes the content immutable by definition. Production CDN data on lojastorra.com.br showed 51%-81% expired rates on these checkout assets. This commit adds an opt-in `cacheControl` prop on the proxy: cacheControl?: { value: string; matchQueryParam?: string; }; When `matchQueryParam` is set, the override applies only if the incoming request URL has that param — letting us safely opt in only versioned variants of a path. The override applies only to 2xx responses so error pages keep their upstream cache semantics. In vtex/loaders/proxy.ts, /files/*, /assets/*, /arquivos/* are now wired to the override with `value: "public, max-age=31536000, immutable"` and `matchQueryParam: "v"`. Unversioned requests on the same paths keep upstream Cache-Control. Follows the immutable-asset idiom already used in website/loaders/asset.ts (s-maxage=15552000, max-age=15552000, immutable). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bb78556 commit 7e17085

2 files changed

Lines changed: 45 additions & 0 deletions

File tree

vtex/loaders/proxy.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,17 @@ const buildProxyRoutes = (
6868
const urlToProxy = `https://${hostname}`;
6969
const hostToUse = hostname;
7070

71+
// Versioned VTEX static assets (e.g. /files/foo.css?v=HASH) are immutable
72+
// by definition: the hash changes whenever the content does. VTEX upstream
73+
// serves them with short TTLs, so we force `immutable` on the proxy hop
74+
// when the request carries a `v` query param. Unversioned variants of the
75+
// same path keep upstream Cache-Control.
76+
const IMMUTABLE_ASSET_PATHS = new Set([
77+
"/files/*",
78+
"/assets/*",
79+
"/arquivos/*",
80+
]);
81+
7182
const routeFromPath = (pathTemplate: string): Route => {
7283
const handlerValue = {
7384
__resolveType: "website/handlers/proxy.ts",
@@ -77,6 +88,12 @@ const buildProxyRoutes = (
7788
includeScriptsToBody,
7889
removeDirtyCookies: true,
7990
pathsThatRequireSameReferer: VTEX_PATHS_THAT_REQUIRES_SAME_REFERER,
91+
...(IMMUTABLE_ASSET_PATHS.has(pathTemplate) && {
92+
cacheControl: {
93+
value: "public, max-age=31536000, immutable",
94+
matchQueryParam: "v",
95+
},
96+
}),
8097
};
8198

8299
return ({

website/handlers/proxy.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,26 @@ export interface Props {
8686
removeDirtyCookies?: boolean;
8787
excludeHeaders?: string[];
8888
pathsThatRequireSameReferer?: string[];
89+
/**
90+
* @description Override Cache-Control on proxied responses. Use when the
91+
* upstream (e.g. VTEX IO) sends a Cache-Control that does not
92+
* match the resource's true cacheability — for example
93+
* versioned static assets at /files/*?v=HASH which are
94+
* immutable but get short upstream TTLs.
95+
*
96+
* When matchQueryParam is set, the override only applies if
97+
* that query param is present on the incoming request URL.
98+
* This lets you safely opt in only the versioned variant of a
99+
* path (e.g. matchQueryParam: "v" → /files/x.js?v=abc gets
100+
* overridden, /files/x.js does not).
101+
*
102+
* Only applied when the upstream response is 2xx, so error
103+
* responses retain their upstream caching semantics.
104+
*/
105+
cacheControl?: {
106+
value: string;
107+
matchQueryParam?: string;
108+
};
89109
}
90110
/**
91111
* @title Proxy
@@ -104,6 +124,7 @@ export default function Proxy({
104124
replaces,
105125
removeDirtyCookies = false,
106126
pathsThatRequireSameReferer = [],
127+
cacheControl,
107128
}: Props): Handler {
108129
return async (req, _ctx) => {
109130
const url = new URL(req.url);
@@ -218,6 +239,13 @@ export default function Proxy({
218239
responseHeaders.set("location", location.replace(proxyUrl, url.origin));
219240
}
220241
}
242+
if (cacheControl && response.ok) {
243+
const matches = !cacheControl.matchQueryParam ||
244+
url.searchParams.has(cacheControl.matchQueryParam);
245+
if (matches) {
246+
responseHeaders.set("Cache-Control", cacheControl.value);
247+
}
248+
}
221249
let text: undefined | string = undefined;
222250
if (replaces && replaces.length > 0 && contentType?.includes("text/html")) {
223251
if (response.ok) {

0 commit comments

Comments
 (0)