From ae33942efe0cb1f82f7aac81bf55e93f6b225dd8 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Thu, 16 Oct 2025 10:36:48 +0200 Subject: [PATCH 1/5] test: update to work with next@canary --- .../dynamic/ttl-1year/[slug]/page.tsx | 2 +- .../dynamic/ttl-5seconds/[slug]/page.tsx | 2 +- .../static/ttl-10seconds/[slug]/page.tsx | 2 +- .../static/ttl-1year/[slug]/page.tsx | 2 +- .../dynamic/ttl-1year/[slug]/page.tsx | 2 +- .../dynamic/ttl-5seconds/[slug]/page.tsx | 2 +- .../static/ttl-10seconds/[slug]/page.tsx | 2 +- .../static/ttl-1year/[slug]/page.tsx | 2 +- .../dynamic/ttl-1year/[slug]/page.tsx | 2 +- .../dynamic/ttl-5seconds/[slug]/page.tsx | 2 +- .../static/ttl-10seconds/[slug]/page.tsx | 2 +- .../static/ttl-1year/[slug]/page.tsx | 2 +- tests/fixtures/use-cache/app/next-cache.ts | 20 +++++++++++++++++++ 13 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 tests/fixtures/use-cache/app/next-cache.ts diff --git a/tests/fixtures/use-cache/app/default/use-cache-component/dynamic/ttl-1year/[slug]/page.tsx b/tests/fixtures/use-cache/app/default/use-cache-component/dynamic/ttl-1year/[slug]/page.tsx index b6cb766e3e..024b115a88 100644 --- a/tests/fixtures/use-cache/app/default/use-cache-component/dynamic/ttl-1year/[slug]/page.tsx +++ b/tests/fixtures/use-cache/app/default/use-cache-component/dynamic/ttl-1year/[slug]/page.tsx @@ -1,4 +1,4 @@ -import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache' +import { cacheLife, cacheTag } from '../../../../../next-cache' import { BasePageComponentProps, getDataImplementation, diff --git a/tests/fixtures/use-cache/app/default/use-cache-component/dynamic/ttl-5seconds/[slug]/page.tsx b/tests/fixtures/use-cache/app/default/use-cache-component/dynamic/ttl-5seconds/[slug]/page.tsx index 2376bcefa5..52498d28a9 100644 --- a/tests/fixtures/use-cache/app/default/use-cache-component/dynamic/ttl-5seconds/[slug]/page.tsx +++ b/tests/fixtures/use-cache/app/default/use-cache-component/dynamic/ttl-5seconds/[slug]/page.tsx @@ -1,4 +1,4 @@ -import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache' +import { cacheLife, cacheTag } from '../../../../../next-cache' import { BasePageComponentProps, getDataImplementation, diff --git a/tests/fixtures/use-cache/app/default/use-cache-component/static/ttl-10seconds/[slug]/page.tsx b/tests/fixtures/use-cache/app/default/use-cache-component/static/ttl-10seconds/[slug]/page.tsx index 4e79023f1d..317b652200 100644 --- a/tests/fixtures/use-cache/app/default/use-cache-component/static/ttl-10seconds/[slug]/page.tsx +++ b/tests/fixtures/use-cache/app/default/use-cache-component/static/ttl-10seconds/[slug]/page.tsx @@ -1,4 +1,4 @@ -import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache' +import { cacheLife, cacheTag } from '../../../../../next-cache' import { BasePageComponentProps, generateStaticParamsImplementation, diff --git a/tests/fixtures/use-cache/app/default/use-cache-component/static/ttl-1year/[slug]/page.tsx b/tests/fixtures/use-cache/app/default/use-cache-component/static/ttl-1year/[slug]/page.tsx index 63fe621296..7c5d306a6b 100644 --- a/tests/fixtures/use-cache/app/default/use-cache-component/static/ttl-1year/[slug]/page.tsx +++ b/tests/fixtures/use-cache/app/default/use-cache-component/static/ttl-1year/[slug]/page.tsx @@ -1,4 +1,4 @@ -import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache' +import { cacheLife, cacheTag } from '../../../../../next-cache' import { BasePageComponentProps, generateStaticParamsImplementation, diff --git a/tests/fixtures/use-cache/app/default/use-cache-data/dynamic/ttl-1year/[slug]/page.tsx b/tests/fixtures/use-cache/app/default/use-cache-data/dynamic/ttl-1year/[slug]/page.tsx index f07a3e05e4..8733d513ec 100644 --- a/tests/fixtures/use-cache/app/default/use-cache-data/dynamic/ttl-1year/[slug]/page.tsx +++ b/tests/fixtures/use-cache/app/default/use-cache-data/dynamic/ttl-1year/[slug]/page.tsx @@ -1,4 +1,4 @@ -import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache' +import { cacheLife, cacheTag } from '../../../../../next-cache' import { BasePageComponentProps, getDataImplementation, diff --git a/tests/fixtures/use-cache/app/default/use-cache-data/dynamic/ttl-5seconds/[slug]/page.tsx b/tests/fixtures/use-cache/app/default/use-cache-data/dynamic/ttl-5seconds/[slug]/page.tsx index 25517481fd..e00d29689d 100644 --- a/tests/fixtures/use-cache/app/default/use-cache-data/dynamic/ttl-5seconds/[slug]/page.tsx +++ b/tests/fixtures/use-cache/app/default/use-cache-data/dynamic/ttl-5seconds/[slug]/page.tsx @@ -1,4 +1,4 @@ -import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache' +import { cacheLife, cacheTag } from '../../../../../next-cache' import { BasePageComponentProps, getDataImplementation, diff --git a/tests/fixtures/use-cache/app/default/use-cache-data/static/ttl-10seconds/[slug]/page.tsx b/tests/fixtures/use-cache/app/default/use-cache-data/static/ttl-10seconds/[slug]/page.tsx index 32374f0910..ae05c72f12 100644 --- a/tests/fixtures/use-cache/app/default/use-cache-data/static/ttl-10seconds/[slug]/page.tsx +++ b/tests/fixtures/use-cache/app/default/use-cache-data/static/ttl-10seconds/[slug]/page.tsx @@ -1,4 +1,4 @@ -import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache' +import { cacheLife, cacheTag } from '../../../../../next-cache' import { BasePageComponentProps, generateStaticParamsImplementation, diff --git a/tests/fixtures/use-cache/app/default/use-cache-data/static/ttl-1year/[slug]/page.tsx b/tests/fixtures/use-cache/app/default/use-cache-data/static/ttl-1year/[slug]/page.tsx index 4da6621b65..f83ba80ec6 100644 --- a/tests/fixtures/use-cache/app/default/use-cache-data/static/ttl-1year/[slug]/page.tsx +++ b/tests/fixtures/use-cache/app/default/use-cache-data/static/ttl-1year/[slug]/page.tsx @@ -1,4 +1,4 @@ -import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache' +import { cacheLife, cacheTag } from '../../../../../next-cache' import { BasePageComponentProps, generateStaticParamsImplementation, diff --git a/tests/fixtures/use-cache/app/default/use-cache-page/dynamic/ttl-1year/[slug]/page.tsx b/tests/fixtures/use-cache/app/default/use-cache-page/dynamic/ttl-1year/[slug]/page.tsx index 416e2d9ac5..4c4ef7b1a2 100644 --- a/tests/fixtures/use-cache/app/default/use-cache-page/dynamic/ttl-1year/[slug]/page.tsx +++ b/tests/fixtures/use-cache/app/default/use-cache-page/dynamic/ttl-1year/[slug]/page.tsx @@ -1,4 +1,4 @@ -import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache' +import { cacheLife, cacheTag } from '../../../../../next-cache' import { BasePageComponentProps, getDataImplementation, diff --git a/tests/fixtures/use-cache/app/default/use-cache-page/dynamic/ttl-5seconds/[slug]/page.tsx b/tests/fixtures/use-cache/app/default/use-cache-page/dynamic/ttl-5seconds/[slug]/page.tsx index a4939f2597..0a9e3c3426 100644 --- a/tests/fixtures/use-cache/app/default/use-cache-page/dynamic/ttl-5seconds/[slug]/page.tsx +++ b/tests/fixtures/use-cache/app/default/use-cache-page/dynamic/ttl-5seconds/[slug]/page.tsx @@ -1,4 +1,4 @@ -import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache' +import { cacheLife, cacheTag } from '../../../../../next-cache' import { BasePageComponentProps, getDataImplementation, diff --git a/tests/fixtures/use-cache/app/default/use-cache-page/static/ttl-10seconds/[slug]/page.tsx b/tests/fixtures/use-cache/app/default/use-cache-page/static/ttl-10seconds/[slug]/page.tsx index bc50627f89..1e99da84fe 100644 --- a/tests/fixtures/use-cache/app/default/use-cache-page/static/ttl-10seconds/[slug]/page.tsx +++ b/tests/fixtures/use-cache/app/default/use-cache-page/static/ttl-10seconds/[slug]/page.tsx @@ -1,4 +1,4 @@ -import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache' +import { cacheLife, cacheTag } from '../../../../../next-cache' import { BasePageComponentProps, generateStaticParamsImplementation, diff --git a/tests/fixtures/use-cache/app/default/use-cache-page/static/ttl-1year/[slug]/page.tsx b/tests/fixtures/use-cache/app/default/use-cache-page/static/ttl-1year/[slug]/page.tsx index ed6c3e68dc..7552666d96 100644 --- a/tests/fixtures/use-cache/app/default/use-cache-page/static/ttl-1year/[slug]/page.tsx +++ b/tests/fixtures/use-cache/app/default/use-cache-page/static/ttl-1year/[slug]/page.tsx @@ -1,4 +1,4 @@ -import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache' +import { cacheLife, cacheTag } from '../../../../../next-cache' import { BasePageComponentProps, generateStaticParamsImplementation, diff --git a/tests/fixtures/use-cache/app/next-cache.ts b/tests/fixtures/use-cache/app/next-cache.ts new file mode 100644 index 0000000000..d761050c98 --- /dev/null +++ b/tests/fixtures/use-cache/app/next-cache.ts @@ -0,0 +1,20 @@ +import * as NextCacheTyped from 'next/cache' + +const NextCache = NextCacheTyped as any + +export const cacheLife: any = + 'cacheLife' in NextCache + ? NextCache.cacheLife + : 'unstable_cacheLife' in NextCache + ? NextCache.unstable_cacheLife + : () => { + throw new Error('both unstable_cacheLife and cacheLife are missing from next/cache') + } +export const cacheTag: any = + 'cacheTag' in NextCache + ? NextCache.cacheTag + : 'unstable_cacheTag' in NextCache + ? NextCache.unstable_cacheTag + : () => { + throw new Error('both unstable_cacheTag and cacheTag are missing from next/cache') + } From 152deccae472ef62d44f7c1c0d3ec764d0a6d8b3 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Thu, 16 Oct 2025 12:26:07 +0200 Subject: [PATCH 2/5] test: nx linux issue workaround --- tests/utils/create-e2e-fixture.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/utils/create-e2e-fixture.ts b/tests/utils/create-e2e-fixture.ts index a03be0fe05..0af1e20a1d 100644 --- a/tests/utils/create-e2e-fixture.ts +++ b/tests/utils/create-e2e-fixture.ts @@ -542,12 +542,18 @@ export const fixtureFactories = { packagePath: 'apps/next-app', buildCommand: 'nx run next-app:build', publishDirectory: 'dist/apps/next-app/.next', + env: { + NX_ISOLATE_PLUGINS: 'false', + }, }), nxIntegratedDistDir: () => createE2EFixture('nx-integrated', { packagePath: 'apps/custom-dist-dir', buildCommand: 'nx run custom-dist-dir:build', publishDirectory: 'dist/apps/custom-dist-dir/dist', + env: { + NX_ISOLATE_PLUGINS: 'false', + }, }), cliBeforeRegionalBlobsSupport: () => createE2EFixture('cli-before-regional-blobs-support', { From 76bcccab15769789bc07b3d37de7c7a1189f4a71 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Thu, 16 Oct 2025 13:50:39 +0200 Subject: [PATCH 3/5] test: fetch-retry for github actions --- .github/workflows/run-tests.yml | 4 ++++ .github/workflows/test-e2e.yml | 1 + tools/fetch-retry.mjs | 41 +++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 tools/fetch-retry.mjs diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 4d4d56c649..181a3ca783 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -124,6 +124,7 @@ jobs: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NEXT_VERSION: ${{ matrix.version }} NEXT_RESOLVED_VERSION: ${{ steps.resolve-next-version.outputs.version }} + NODE_OPTIONS: --import ${{ github.workspace }}/tools/fetch-retry.mjs - name: Upload blob report to GitHub Actions Artifacts uses: actions/upload-artifact@v4 if: always() @@ -220,6 +221,7 @@ jobs: env: NEXT_VERSION: ${{ matrix.version }} NEXT_RESOLVED_VERSION: ${{ steps.resolve-next-version.outputs.version }} + NODE_OPTIONS: --import ${{ github.workspace }}/tools/fetch-retry.mjs - name: 'Unit and integration tests' run: npm run test:ci:unit-and-integration -- --shard=${{ matrix.shard }}/8 env: @@ -227,6 +229,7 @@ jobs: NEXT_VERSION: ${{ matrix.version }} NEXT_RESOLVED_VERSION: ${{ steps.resolve-next-version.outputs.version }} TEMP: ${{ github.workspace }}/.. + NODE_OPTIONS: --import ${{ github.workspace }}/tools/fetch-retry.mjs smoke: if: always() @@ -289,6 +292,7 @@ jobs: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NEXT_VERSION: ${{ matrix.version }} NEXT_RESOLVED_VERSION: ${{ steps.resolve-next-version.outputs.version }} + NODE_OPTIONS: --import ${{ github.workspace }}/tools/fetch-retry.mjs merge-reports: if: always() diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 7a955daf4f..e87a4f2ea9 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -239,6 +239,7 @@ jobs: NEXT_RESOLVED_VERSION: ${{ matrix.version_spec.version }} IS_WEBPACK_TEST: ${{ steps.decide-default-bundler.outputs.default_bundler == 'webpack' && '1' || '' }} IS_TURBOPACK_TEST: ${{ steps.decide-default-bundler.outputs.default_bundler == 'turbopack' && '1' || '' }} + NODE_OPTIONS: --import ${{ github.workspace }}/${{ env.runtime-path}}/tools/fetch-retry.mjs run: node run-tests.js -g ${{ matrix.group }}/${{ needs.setup.outputs.total }} -c ${TEST_CONCURRENCY} --type e2e working-directory: ${{ env.next-path }} diff --git a/tools/fetch-retry.mjs b/tools/fetch-retry.mjs new file mode 100644 index 0000000000..84a0fce9d4 --- /dev/null +++ b/tools/fetch-retry.mjs @@ -0,0 +1,41 @@ +// We are seeing quite a bit of 'fetch failed' cases in Github Actions that don't really reproduce +// locally. We are likely hitting some limits there when attempting to parallelize. They are not consistent +// so instead of reducing parallelism, we add a retry with backoff here. + +const originalFetch = globalThis.fetch + +const NUM_RETRIES = 5 + +globalThis.fetch = async (...args) => { + let backoff = 100 + for (let attempt = 1; attempt <= NUM_RETRIES; attempt++) { + try { + return await originalFetch.apply(globalThis, args) + } catch (error) { + let shouldRetry = false + // not ideal, but there is no error code for that + if (error.message === 'fetch failed' && attempt < NUM_RETRIES) { + // on this error we try again + shouldRetry = true + } + + if (shouldRetry) { + // leave some trace in logs what's happening + console.error('[fetch-retry] fetch thrown, retrying...', { + args, + attempt, + errorMsg: error.message, + }) + + const currentBackoff = backoff + await new Promise((resolve) => { + setTimeout(resolve, currentBackoff) + }) + backoff *= 2 + continue + } + + throw error + } + } +} From 0e951a2cc21d374a83a714354d874fc7a73d7eee Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Thu, 16 Oct 2025 15:32:23 +0200 Subject: [PATCH 4/5] test: adjust Netlify Edge stale cache-status assertion --- tests/e2e/page-router.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/page-router.test.ts b/tests/e2e/page-router.test.ts index 759928c7b2..0de4dbbe32 100644 --- a/tests/e2e/page-router.test.ts +++ b/tests/e2e/page-router.test.ts @@ -425,7 +425,7 @@ test.describe('Simple Page Router (no basePath, no i18n)', () => { const response2 = await page.goto(new URL(pathname, pageRouter.url).href) expect(response2?.status()).toBe(200) expect(response2?.headers()['cache-status']).toMatch( - /("Netlify Edge"; hit; fwd=stale|"Netlify Durable"; hit; ttl=-[0-9]+)/m, + /("Netlify Edge"; fwd=stale|"Netlify Durable"; hit; ttl=-[0-9]+)/m, ) expect(response2?.headers()['debug-netlify-cdn-cache-control']).toMatch( /s-maxage=60, stale-while-revalidate=[0-9]+, durable/, From e47ff48bba3b65d36583f85e04d948bbffcb0a57 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 17 Oct 2025 10:22:03 +0200 Subject: [PATCH 5/5] fix: recursively wait for background work --- src/run/handlers/request-context.cts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/run/handlers/request-context.cts b/src/run/handlers/request-context.cts index 9aabb7ca0a..2b7f6ff2c7 100644 --- a/src/run/handlers/request-context.cts +++ b/src/run/handlers/request-context.cts @@ -67,18 +67,28 @@ export function createRequestContext(request?: Request, context?: Context): Requ logger.debug('[NetlifyNextRuntime] Background revalidation request') } + async function recursivelyWaitForBackgroundWork() { + let settledPromisesCount = 0 + while (settledPromisesCount < backgroundWorkPromises.length) { + const currentPromiseCount = backgroundWorkPromises.length + await Promise.allSettled(backgroundWorkPromises) + settledPromisesCount = currentPromiseCount + } + } + return { isBackgroundRevalidation, captureServerTiming: request?.headers.has('x-next-debug-logging') ?? false, trackBackgroundWork: (promise) => { - if (context?.waitUntil) { - context.waitUntil(promise) - } else { - backgroundWorkPromises.push(promise) - } + backgroundWorkPromises.push(promise) }, get backgroundWorkPromise() { - return Promise.allSettled(backgroundWorkPromises) + if (context?.waitUntil) { + // when context.waitUntil is available, we offload background work awaiting to it + context.waitUntil(recursivelyWaitForBackgroundWork()) + return Promise.resolve() + } + return recursivelyWaitForBackgroundWork() }, logger, requestID: request?.headers.get('x-nf-request-id') ?? getFallbackRequestID(),