From 8b90fdd0ffc59c57bf46c0bb1e4fa55c7f88692e Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Tue, 23 Jun 2026 19:05:51 +0200 Subject: [PATCH 1/3] Fix cache interception for index route --- .changeset/fix-cache-interceptor-index-route.md | 5 +++++ .../open-next/src/core/routing/cacheInterceptor.ts | 14 +++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 .changeset/fix-cache-interceptor-index-route.md diff --git a/.changeset/fix-cache-interceptor-index-route.md b/.changeset/fix-cache-interceptor-index-route.md new file mode 100644 index 000000000..173c09a37 --- /dev/null +++ b/.changeset/fix-cache-interceptor-index-route.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/aws": patch +--- + +Fix cache interception for index routes by normalizing the route path to `/` while reading the generated cache asset from `/index`. diff --git a/packages/open-next/src/core/routing/cacheInterceptor.ts b/packages/open-next/src/core/routing/cacheInterceptor.ts index 7e1fd217e..1974f7337 100644 --- a/packages/open-next/src/core/routing/cacheInterceptor.ts +++ b/packages/open-next/src/core/routing/cacheInterceptor.ts @@ -265,23 +265,23 @@ export async function cacheInterceptor( localizedPath = localizedPath.replace(/\/$/, ""); // Then we decode the path params - localizedPath = decodePathParams(localizedPath); + localizedPath = decodePathParams(localizedPath) || "/"; + + // The route is keyed as `/` in the prerender manifest, but the generated + // cache asset for the app index route is uploaded as `/index`. + const cacheKey = localizedPath === "/" ? "/index" : localizedPath; debug("Checking cache for", localizedPath, PrerenderManifest); const isISR = - Object.keys(PrerenderManifest?.routes ?? {}).includes( - localizedPath ?? "/", - ) || + Object.keys(PrerenderManifest?.routes ?? {}).includes(localizedPath) || Object.values(PrerenderManifest?.dynamicRoutes ?? {}).some((dr) => new RegExp(dr.routeRegex).test(localizedPath), ); debug("isISR", isISR); if (isISR) { try { - const cachedData = await globalThis.incrementalCache.get( - localizedPath ?? "/index", - ); + const cachedData = await globalThis.incrementalCache.get(cacheKey); debug("cached data in interceptor", cachedData); if (!cachedData?.value) { From f654a442e13358f9ed1a3cd7968999c762232a69 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Wed, 24 Jun 2026 10:16:26 +0200 Subject: [PATCH 2/3] Fix index route tag cache key --- packages/open-next/src/core/routing/cacheInterceptor.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/open-next/src/core/routing/cacheInterceptor.ts b/packages/open-next/src/core/routing/cacheInterceptor.ts index 1974f7337..eb93b17c2 100644 --- a/packages/open-next/src/core/routing/cacheInterceptor.ts +++ b/packages/open-next/src/core/routing/cacheInterceptor.ts @@ -295,7 +295,7 @@ export async function cacheInterceptor( ) { const _hasBeenRevalidated = cachedData.shouldBypassTagCache ? false - : await hasBeenRevalidated(localizedPath, tags, cachedData); + : await hasBeenRevalidated(cacheKey, tags, cachedData); if (_hasBeenRevalidated) { return event; @@ -305,11 +305,7 @@ export async function cacheInterceptor( // Check if the cache entry is stale (valid but needs background revalidation) const _isStale = cachedData.shouldBypassTagCache ? false - : await isStale( - localizedPath, - tags, - cachedData.lastModified ?? Date.now(), - ); + : await isStale(cacheKey, tags, cachedData.lastModified ?? Date.now()); const host = event.headers.host; switch (cachedData?.value?.type) { From 8cc70bc80df382196326a0ba399ac47f09c70689 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Wed, 24 Jun 2026 10:16:26 +0200 Subject: [PATCH 3/3] Add cache interceptor index route tests --- .../core/routing/cacheInterceptor.test.ts | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts b/packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts index b464f4be5..b87461299 100644 --- a/packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts +++ b/packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts @@ -11,6 +11,11 @@ vi.mock("@opennextjs/aws/adapters/config/index.js", () => ({ NextConfig: {}, PrerenderManifest: { routes: { + "/": { + initialRevalidateSeconds: 120, + srcRoute: "/", + dataRoute: "/index.rsc", + }, "/albums": { initialRevalidateSeconds: false, srcRoute: "/albums", @@ -144,6 +149,15 @@ describe("cacheInterceptor", () => { const body = await fromReadableStream(result.body); expect(body).toEqual("Hello, world!"); + expect(incrementalCache.get).toHaveBeenCalledWith("/albums"); + expect(tagCache.getLastModified).toHaveBeenCalledWith( + "/albums", + expect.any(Number), + ); + expect(tagCache.isStale).toHaveBeenCalledWith( + "/albums", + expect.any(Number), + ); expect(result).toEqual( expect.objectContaining({ type: "core", @@ -159,6 +173,70 @@ describe("cacheInterceptor", () => { ); }); + it("should retrieve index app router content from the index cache key", async () => { + const event = createEvent({ + url: "/", + }); + incrementalCache.get.mockResolvedValueOnce({ + value: { + type: "app", + html: "Index page", + }, + lastModified: new Date("2024-01-01T23:59:30Z").getTime(), + }); + + const result = await cacheInterceptor(event); + + const body = await fromReadableStream(result.body); + expect(body).toEqual("Index page"); + expect(incrementalCache.get).toHaveBeenCalledWith("/index"); + expect(tagCache.getLastModified).toHaveBeenCalledWith( + "/index", + expect.any(Number), + ); + expect(tagCache.isStale).toHaveBeenCalledWith("/index", expect.any(Number)); + expect(result).toEqual( + expect.objectContaining({ + headers: expect.objectContaining({ + "cache-control": "s-maxage=90, stale-while-revalidate=2592000", + "x-opennext-cache": "HIT", + }), + }), + ); + }); + + it("should revalidate stale index content using the route path", async () => { + const event = createEvent({ + url: "/", + }); + incrementalCache.get.mockResolvedValueOnce({ + value: { + type: "app", + html: "Index page", + }, + lastModified: new Date("2024-01-01T23:57:00Z").getTime(), + }); + + const result = await cacheInterceptor(event); + + expect(incrementalCache.get).toHaveBeenCalledWith("/index"); + expect(queue.send).toHaveBeenCalledWith( + expect.objectContaining({ + MessageBody: expect.objectContaining({ + url: "/", + }), + }), + ); + expect(result).toEqual( + expect.objectContaining({ + headers: expect.objectContaining({ + "cache-control": "s-maxage=1, stale-while-revalidate=2592000", + "x-opennext-cache": "STALE", + }), + }), + ); + }); + it("should take no action when tagCache lasModified is -1 for app type", async () => { const event = createEvent({ url: "/albums",