feat: VTEX upstream resilience (abort, breaker, stale-if-error, anti-poisoning, error boundary)#384
Open
JonasJesus42 wants to merge 6 commits into
Open
feat: VTEX upstream resilience (abort, breaker, stale-if-error, anti-poisoning, error boundary)#384JonasJesus42 wants to merge 6 commits into
JonasJesus42 wants to merge 6 commits into
Conversation
…e-if-error Wrap all VTEX egress (createVtexFetch baseFetch) with a real AbortController timeout (frees the socket → kills client-abort 499s on SSR hangs), an idempotent-only retry with a per-host token-bucket budget (checkout POSTs are never retried; no retry storm), and a per-host circuit breaker (fail-fast + load shedding under a broad outage). fetchCache: treat circuit-open/timeout as non-retryable (fast-fail), and bound stale-if-error serving to a 24h window (was effectively unbounded) so warm keys survive a full outage with zero upstream pressure, then surface the error. ON by default; VTEX_RESILIENCE_DISABLED=true is a runtime kill-switch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cmsRouteConfig / cmsHomeRouteConfig had no errorComponent and
loadCmsPageInternal has no try/catch, so an uncaught throw in page resolution
(resolveDecoPage / buildPageSeo / resolveSiteGlobals) during an outage became a
raw 500 white screen. Ship a default branded CmsPageErrorFallback (retry
button) as the cold-cache last resort — the edge still serves STALE-ERROR from
a warm entry first. Sites override via cmsRouteConfig({ errorComponent }).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a critical section loader throws, the section renders with raw/empty props (e.g. an empty shelf during a VTEX outage) and the page is emitted as a healthy 200. The edge then caches that broken page for the whole retention window and stale-if-error never fires — a seconds-long blip becomes hours of empty pages. - blocks/sectionLoaders: record each critical section degradation on a request-scoped collector (getDegradedSections). Sections are critical by default; registerNonCriticalSections opts decorative ones out. - tanstack/cmsRoute: emit X-Deco-Degraded: true when any critical section degraded, and expose degraded on loader data. - tanstack/workerEntry: treat a 200 + X-Deco-Degraded like a 5xx — never store it, and prefer a warm stale entry (STALE-ERROR); cold cache serves it no-store. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Union RequestContext.signal (client disconnect / request-level SSR deadline) with the caller signal and the per-attempt timeout, so a disconnected client or an aborted request cancels the in-flight VTEX call and frees the socket instead of letting the SSR render hang — the remaining 499 vector after the per-attempt abort landed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- resilience: a caller/ambient abort (client disconnect via RequestContext.signal) is no longer counted as a breaker failure or retried — only our own timeout and real network/5xx failures trip the breaker. Prevents client behavior from opening the circuit against a healthy VTEX. - fetchCache: single attempt — the resilience layer owns retries + breaker. Eliminates the double-retry (up to 9 upstream calls) and the ~3x breaker over-counting on 5xx. Raise the inflight backstop timeout to 15s (above the resilience 12s total) so it never kills a slow-but-recoverable response, discards the body, or defeats dedup. - resolve: instrument the commerce-loader catch (VTEX PDP/PLP data — the actual incident path) with markSectionDegraded, so an empty product page is not cached as healthy. - cmsRoute: wire the homepage (loadCmsHomePage) into the degraded signal; emit X-Deco-Degraded via the route headers() callback (reliable document channel, not just setResponseHeader); gate deferred sections so a degraded shelf is not marked X-Deco-Cacheable. - sectionLoaders: stop marking LAYOUT (Header/Footer) failures as degraded — a flaky layout loader would flip the whole site to uncacheable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ic values
- New utils/constants.ts is the single place every timeout/limit/threshold for
the resilience + fetch-cache layer lives, each documented in one spot instead
of scattered across fetchCache.ts and resilience.ts (DEFAULT_RESILIENCE_CONFIG,
TTL_BY_STATUS, STALE_IF_ERROR_MS, FETCH_TIMEOUT_MS, DEFAULT_MAX_ENTRIES).
- fetchCache.ts: TTL_BY_STATUS's opaque "2xx"/"404"/"5xx" string-keyed record
becomes FETCH_CACHE_FRESH_TTL_MS.{success,notFound,serverError} — reads at the
call site without cross-referencing the map. ttlForStatus -> freshTtlForStatus
(was ambiguous against the new stale-if-error TTL).
- instrumentedFetch.ts: replace the inline status/100-floor expression (opaque
without re-deriving what it computes) with the already-exported
statusClassFor helper from @decocms/blocks/sdk/observability — also removes
a second, drifting implementation of the same status-class mapping.
- resilience.ts re-exports ResilienceConfig/DEFAULT_RESILIENCE_CONFIG from
constants.ts so the one external import site is unaffected.
No behavior change — same defaults, same tests (232/548/109 still pass).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Contexto
Lentidão do upstream VTEX vinha derrubando storefronts TanStack (Bagaggio 2026-07-18; antes dental-partner, vicbeaute, meiasola) com a mesma assinatura, que cascateia em três falhas:
fetchCacheera umPromise.raceque só liberava o slot de dedup, não o socket);stale-if-errornunca dispara e um blip de segundos vira horas de páginas vazias.Este PR resolve no framework para que todos os sites herdem. Princípio: uma chamada VTEX lenta/com erro nunca deve virar 500 nem um 200-quebrado cacheável — sempre degradar para (1) último dado bom, (2) fail-fast do breaker, (3) página branded, nessa ordem.
O que muda
1. Resiliência na camada de fetch VTEX (
apps-vtex) —997ce97,d97e659utils/resilience.ts, composto sob a instrumentação emcreateVtexFetch({ baseFetch })→ cobre 100% da egress (GET cacheado, IS e checkout/cart POSTs), sem tocar em call-sites.RequestContext.signal(disconnect do cliente / deadline) ao fetch → cancelamento propaga.VTEX_RESILIENCE_DISABLED=true.2. Stale-if-error na camada de dados (
apps-vtex/fetchCache) —997ce97VtexCircuitOpenError/timeout são não-retryáveis (fast-fail).3. Anti-cache-poisoning: sinal
X-Deco-Degraded(blocks+tanstack) —388d147blocks/sectionLoaders: registra cada seção crítica que degradou num coletor request-scoped (getDegradedSections). Crítico por default;registerNonCriticalSectionsfaz opt-out de decorativas.tanstack/cmsRoute: emiteX-Deco-Degraded: truequando há degradação crítica.tanstack/workerEntry: trata200 + X-Deco-Degradedcomo 5xx — nunca armazena, e prefere entrada quente (STALE-ERROR); cache frio serveno-store.4.
errorComponentdefault nas rotas CMS (tanstack/cmsRoute) —327b1e4cmsRouteConfig/cmsHomeRouteConfigganham umCmsPageErrorFallbackbranded (com "tentar de novo") em vez de 500 cru. É o último recurso de cache frio — o edge serveSTALE-ERRORantes disso. Sobreponível viacmsRouteConfig({ errorComponent }).Testes
Todos verdes localmente:
apps-vtex: 230 (novos: 9 resilience + 2 fetchCache SIE/fast-fail)blocks: 547 (novos: 4 de degradação de seção)tanstack: 109 (novos: 2 do branch degraded no worker)biome checketsc --noEmitlimpos nos arquivos alterados.Notas de rollout / follow-up
examples/tanstack-smoke) confirmando queX-Deco-Degraded(set viasetResponseHeaderno loader SSR) chega aoworkerEntryno caminho do documento — segue o mesmo mecanismo doX-Deco-Cacheablejá existente, mas não é coberto por unit test.sectionLoaders/resolvecommerce loader) ficaram de fora — o abort real por-chamada + breaker já limitam bastante o hang; podem entrar como hardening depois.fetch.ts(fetchSafe/fetchAPI, usado por sitemap/loaders ad-hoc) não passa pelo_fetche segue sem resiliência — fora de escopo.🤖 Generated with Claude Code
Summary by cubic
Adds upstream resilience for VTEX calls and anti-cache-poisoning for degraded pages, so storefronts keep serving last-good data during VTEX slowness instead of 499/500 or empty cached pages. Also adds a default CMS error boundary to avoid raw 500s.
New Features
apps-vtex: Resilient VTEX fetch undercreateVtexFetch(real per-attempt abort/timeout, idempotent-only retry with backoff + per-host token-bucket, per-host circuit breaker with half-open probes, propagatesRequestContext.signal, caller/ambient aborts do not trip the breaker; kill-switchVTEX_RESILIENCE_DISABLED=true). All resilience/cache knobs are centralized inutils/constants.ts; histograms usestatusClassFor.apps-vtex/fetchCache: Single attempt (retries live in the resilience layer); stale-if-error serves last-good for up to 24h; circuit-open and timeout errors are non-retryable (fast-fail); inflight backstop timeout raised to 15s to avoid killing near-complete responses.blocks+tanstack: Track critical section degradations (commerce loaders flagged; layout sections excluded) and emitX-Deco-Degraded: truevia route headers; deferred sections are not marked cacheable when degraded; the worker treats a degraded 200 like 5xx (serve warmSTALE-ERROR, elseno-store).tanstack/cmsRoute: DefaultCmsPageErrorFallback(retry button), overridable viacmsRouteConfig({ errorComponent }); homepage wired into the degraded signal.Migration
VTEX_RESILIENCE_DISABLED=trueto disable in emergencies.registerNonCriticalSections([...])if their empty state is safe to cache.Written for commit 23810b2. Summary will update on new commits.