fix(cache): rotas privadas em cache público + endurecimento de bypass - #512
Open
JonasJesus42 wants to merge 3 commits into
Open
fix(cache): rotas privadas em cache público + endurecimento de bypass#512JonasJesus42 wants to merge 3 commits into
JonasJesus42 wants to merge 3 commits into
Conversation
Every response currently ships `CDN-Cache-Control: no-store`, which hides a few real gaps in how routes are classified. They are harmless only while nothing is cached at the CDN; enabling that turns each one into a leak. - `PRIVATE_PREFIX_RE` only matched a short list, was case-sensitive and anchored at the root, so `/listadedesejos`, `/wishlist`, `/favoritos`, `/orders`, `/profile`, `/logout`, `/cadastro` and returns routes all fell through to the cacheable `listing` default (public, 120s edge) — as did `/Checkout` and `/pt/checkout`. Rebuilt from a `PRIVATE_SEGMENTS` list, case-insensitive, tolerating a locale prefix. - `setCacheProfile` would happily flip `private`/`cart`/`none` to public via a props bag. Now refused with a warning unless `allowPublicPrivateProfile()` is called first — the escape hatch has a name you have to type. - `registerCachePattern` is evaluated before the built-ins "so they can override defaults", which let a broad site pattern capture `/checkout` and make it public. Custom patterns can still tighten anything; they can no longer make a private path public. - Adds `registerPrivatePaths()`, the safe half of cache configuration: it can only restrict, and (because the Worker is the source of truth for cacheability) it propagates to the CDN with no rule change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…response The VTEX app middleware wraps `handleRequest`, so it runs after the entire edge-cache layer and is the last writer of `Cache-Control` — including on a cache HIT. It overwrote unconditionally, which downgraded a home page the cache layer had resolved as `s-maxage=900` to `vtexCacheControl`'s generic `s-maxage=60`, throwing away the per-profile TTL. It now only speaks up for the case it actually knows better about — a logged-in or custom-pricing request — and when it does, it clears `CDN-Cache-Control` too. Otherwise a response could go out as `Cache-Control: private, no-store` alongside `CDN-Cache-Control: public, max-age=300`, and Cloudflare gives the CDN header precedence. Same pairing the Worker's own bypasses and `utils/proxy.ts` already use. Exports `vtexMiddleware` so the behaviour is testable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three gaps that are currently masked by every response shipping `CDN-Cache-Control: no-store`. They are harmless only while nothing is cached in front of the Worker — the moment anything is (Workers Cache, a CDN rule), each one becomes a cross-user leak. - `hasOnlySafeCookies` was fail-open: a response that HAS a `set-cookie` whose names failed to parse was treated as safe, i.e. cacheable. The parser's fallback path is documented as unreliable, and the two outcomes are not symmetric — guessing "safe" caches a personalized response into the shared entry. Now fail-closed. - `bypassPaths` REPLACED the framework defaults instead of extending them, so a site adding one path silently lost `/deco/`, `/live/` and `/.decofile`. Now always merged. - `CDN-Cache-Control` is decided at the single response exit. Previously a dozen bypass call sites each decided for themselves: some deleted the header, some didn't, and several still emitted the profile's public `Cache-Control` (`public, s-maxage=900`) on the way out. Branches that return before the cache layer (`?asJson`, `?renderJson`, proxy, redirects) emitted nothing at all. Now: bypass forces `no-store`, an absent header defaults to `no-store`, and a value the cache layer already decided is left alone — so an early return can only ever be more restrictive, never accidentally public. Also warns at boot when `buildSegment` is missing, since the logged-in bypass reads `segment.loggedIn` and is inert without it — authenticated and anonymous visitors then share one edge entry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Separado do #510 a pedido do review — estas correções não dependem de nada, valem sozinhas e podem mergear
sem esperar o review da feature de CDN.
Todas são bugs que já estão em produção hoje. Estavam mascarados pelo fato de toda resposta sair com
CDN-Cache-Control: no-store: são inofensivos enquanto não existe nada cacheando na frente do Worker, e viramvazamento entre usuários no momento em que existir (Workers Cache, regra de CDN).
1. Rotas privadas caindo em cache público
PRIVATE_PREFIX_REcobria uma lista curta, era case-sensitive e ancorada na raiz. Tudo que não batia caía nodefault
listing— público, 120s de edge:/listadedesejos,/wishlist,/favoritos,/orders,/order-placed,/profile,/perfil,/logout,/sair,/cadastro,/signup,/register,/assinaturas,/troca,/devolucao— e também/Checkout(maiúscula) e
/pt/checkout(prefixo de locale).Reconstruída a partir de uma lista
PRIVATE_SEGMENTS, case-insensitive, tolerando prefixo de locale.2.
hasOnlySafeCookiesera fail-openUma resposta que tem
set-cookiemas cujos nomes o parser não conseguiu extrair era tratada como segura,isto é, cacheável. O caminho de fallback do parser é documentado no próprio código como pouco confiável, e os
dois desfechos não são simétricos: chutar "seguro" cacheia uma resposta personalizada na entrada compartilhada.
Agora falha fechado.
3.
bypassPathssubstituía os defaultsUm site que passava
bypassPathspara acrescentar um path perdia silenciosamente/deco/,/live/e/.decofile. Agora sempre soma.4.
CDN-Cache-Controldecidido num ponto sóAntes, uma dúzia de call sites de bypass decidia cada um por si: alguns deletavam o header, outros não, e
vários ainda emitiam o
Cache-Controlpúblico do perfil (public, s-maxage=900) na saída. Branches queretornam antes da camada de cache (
?asJson,?renderJson, proxy, redirects) não emitiam nada.Agora, no exit único: bypass força
no-store, header ausente virano-store, e um valor que a camada decache já decidiu é preservado. Um early return só pode ser mais restritivo, nunca acidentalmente público.
5. Configuração pelo site: livre para apertar, ruidosa para afrouxar
Adicionar restrição é sempre seguro; remover é o que vaza dado. Então:
registerCachePatterncontinua vencendo os builtins, exceto que não consegue mais tornar pública umarota privada — um pattern amplo do site capturava
/checkout.setCacheProfile("private", { isPublic: true })é recusado com aviso.allowPublicPrivateProfile()é asaída, e tem um nome que você precisa digitar.
6. Middleware VTEX sobrescrevia
Cache-Controlem toda respostaEle envolve
handleRequest, então roda depois de toda a camada de cache e é o último a escrever o header —inclusive em HIT. Rebaixava uma home de
s-maxage=900para os-maxage=60genérico devtexCacheControl.Agora só age no caso que ele realmente conhece melhor (logado ou custom pricing) e, quando age, limpa também
o
CDN-Cache-Control— senão saiprivate, no-storeao lado depublic, max-age=300, e a Cloudflare dáprecedência ao segundo.
Além disso
Avisa no boot quando falta
buildSegment, já que o bypass de logado lêsegment.loggedIne é inerte sem ele— usuário autenticado e anônimo compartilham a mesma entrada.
Validação
2553testes passando. Typecheck limpo nos três pacotes. Conferido também no worker real de um site buildado:/listadedesejos,/Checkoute/pt/minha-contasaemprivate, no-store; antes eramlistingpúblico.🤖 Generated with Claude Code
Summary by cubic
Closes cache-classification bugs that are latent today — every response ships
CDN-Cache-Control: no-store— but would leak authenticated pages between users once CDN caching is enabled.Bug Fixes
listing.hasOnlySafeCookiesnow fails closed when aset-cookieheader can't be parsed into names.bypassPathsnow always merges with/deco/,/live/and/.decofileinstead of replacing them.CDN-Cache-Controlis decided at one exit point: bypasses and early returns (?asJson, redirects, proxy) forceno-store, and a value the cache layer set is preserved.s-maxage=60; it only does for logged-in or custom-pricing requests, and clearsCDN-Cache-Controlthen.buildSegmentis missing, since the logged-in bypass readssegment.loggedInand is inert without it.Migration
setCacheProfile("private", { isPublic: true })is now refused with a warning; callallowPublicPrivateProfile()if a site genuinely needs it.registerPrivatePaths()(only restricts) to add site-specific private routes.Written for commit a03f048. Summary will update on new commits.