From 131b10bbe828df049716c40f33ce6564043f7252 Mon Sep 17 00:00:00 2001 From: Brent Rager Date: Fri, 12 Jun 2026 12:47:27 -0400 Subject: [PATCH] SmooaiNextEdge: include Next.js RSC headers in the HTML cache key (0.1.6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The HTML cache policy used headerBehavior:'none', so CloudFront cached the HTML document and the React Server Component (RSC/Flight) payload for the SAME URL under the SAME key — Next.js serves both at one URL, differentiated only by the `RSC` request header. A cached RSC payload then got served for a document request: pages rendered as raw text (`1:"$Sreact.fragment"…`), never hydrated, forms were dead, sign-in "did nothing". Hit live on smoo.ai's apex cutover. Whitelist RSC / Next-Router-Prefetch / Next-Router-State-Tree / Next-Url in the HTML cache key so the document and each RSC variant get distinct entries (same set OpenNext / sst.aws.Nextjs use). Bumps @smooai/deploy -> 0.1.6. Co-Authored-By: Claude Opus 4.8 --- sst/package.json | 2 +- sst/src/components/smooai-next-edge.ts | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/sst/package.json b/sst/package.json index e1073b8..77b4736 100644 --- a/sst/package.json +++ b/sst/package.json @@ -1,6 +1,6 @@ { "name": "@smooai/deploy", - "version": "0.1.5", + "version": "0.1.6", "description": "Shared SmooAI deploy primitives — reusable SST v4 constructs (API Gateway WebSocket + Rust Lambda + DynamoDB single-table + S3 blob bucket + S3 Vectors placeholder). Consumed by smooth-operator and dogfooded by smooai.", "license": "MIT", "type": "module", diff --git a/sst/src/components/smooai-next-edge.ts b/sst/src/components/smooai-next-edge.ts index 152bcc2..8754c3c 100644 --- a/sst/src/components/smooai-next-edge.ts +++ b/sst/src/components/smooai-next-edge.ts @@ -452,6 +452,16 @@ export class SmooaiNextEdge { // year-long s-maxage at the edge. Cache key includes all cookies/qs so // authenticated pages get their own (effectively uncached, per-session) // entries rather than serving one user's HTML to another. + // + // CRITICAL for Next.js App Router: the cache key MUST include the RSC + // request headers. Next serves the SAME URL as both the HTML document + // (no `RSC` header) and the React Server Component / Flight payload + // (`RSC: 1`, prefetch, router-state-tree, next-url). With these NOT in + // the cache key the two responses collide — a cached RSC/Flight payload + // gets served for a document request, so the page renders as raw text + // (`1:"$Sreact.fragment"…`) and the app never hydrates (forms dead, + // sign-in "does nothing"). Whitelisting them gives the document and each + // RSC variant distinct cache entries. (Matches OpenNext / sst.aws.Nextjs.) const htmlPolicy = new aws.cloudfront.CachePolicy(`${name}HtmlPolicy`, { name: $interpolate`${$app.name}-${$app.stage}-${name}-html`, defaultTtl: htmlDefaultTtl, @@ -459,7 +469,7 @@ export class SmooaiNextEdge { minTtl: 0, parametersInCacheKeyAndForwardedToOrigin: { cookiesConfig: { cookieBehavior: 'all' }, - headersConfig: { headerBehavior: 'none' }, + headersConfig: { headerBehavior: 'whitelist', headers: { items: ['RSC', 'Next-Router-Prefetch', 'Next-Router-State-Tree', 'Next-Url'] } }, queryStringsConfig: { queryStringBehavior: 'all' }, enableAcceptEncodingGzip: true, enableAcceptEncodingBrotli: true,