diff --git a/packages/next/server/response-cache.ts b/packages/next/server/response-cache.ts index 4887c9f31a3..037a89da33a 100644 --- a/packages/next/server/response-cache.ts +++ b/packages/next/server/response-cache.ts @@ -60,15 +60,17 @@ type ResponseGenerator = ( hadCache: boolean ) => Promise +type IncrementalCacheItem = { + revalidateAfter?: number | false + curRevalidate?: number | false + revalidate?: number | false + value: IncrementalCacheValue | null + isStale?: boolean + isMiss?: boolean +} | null + interface IncrementalCache { - get: (key: string) => Promise<{ - revalidateAfter?: number | false - curRevalidate?: number | false - revalidate?: number | false - value: IncrementalCacheValue | null - isStale?: boolean - isMiss?: boolean - } | null> + get: (key: string) => Promise set: ( key: string, data: IncrementalCacheValue | null, @@ -123,8 +125,9 @@ export default class ResponseCache { // `pendingResponses` to ensure that any any other calls will reuse the // same promise until we've fully finished our work. ;(async () => { + let cachedResponse: IncrementalCacheItem = null try { - const cachedResponse = key ? await this.incrementalCache.get(key) : null + cachedResponse = key ? await this.incrementalCache.get(key) : null if (cachedResponse && !context.isManualRevalidate) { resolve({ isStale: cachedResponse.isStale, @@ -169,6 +172,15 @@ export default class ResponseCache { ) } } catch (err) { + // when a getStaticProps path is erroring we automatically re-set the + // existing cache under a new expiration to prevent non-stop retrying + if (cachedResponse && key) { + await this.incrementalCache.set( + key, + cachedResponse.value, + Math.min(Math.max(cachedResponse.revalidate || 3, 3), 30) + ) + } // while revalidating in the background we can't reject as // we already resolved the cache entry so log the error here if (resolved) { diff --git a/test/e2e/prerender.test.ts b/test/e2e/prerender.test.ts index a519236ab4c..b6c6f8bf642 100644 --- a/test/e2e/prerender.test.ts +++ b/test/e2e/prerender.test.ts @@ -1905,6 +1905,50 @@ describe('Prerender', () => { } if (!(global as any).isNextDev) { + it('should automatically reset cache TTL when an error occurs and build cache was available', async () => { + await next.patchFile('error.txt', 'yes') + await waitFor(2000) + + for (let i = 0; i < 5; i++) { + const res = await fetchViaHTTP( + next.url, + '/blocking-fallback/test-errors-1' + ) + expect(res.status).toBe(200) + } + await next.deleteFile('error.txt') + expect( + next.cliOutput.match( + /throwing error for \/blocking-fallback\/test-errors-1/ + ).length + ).toBe(1) + }) + + it('should automatically reset cache TTL when an error occurs and runtime cache was available', async () => { + const res = await fetchViaHTTP( + next.url, + '/blocking-fallback/test-errors-2' + ) + + expect(res.status).toBe(200) + await waitFor(2000) + await next.patchFile('error.txt', 'yes') + + for (let i = 0; i < 5; i++) { + const res = await fetchViaHTTP( + next.url, + '/blocking-fallback/test-errors-2' + ) + expect(res.status).toBe(200) + } + await next.deleteFile('error.txt') + expect( + next.cliOutput.match( + /throwing error for \/blocking-fallback\/test-errors-2/ + ).length + ).toBe(1) + }) + it('should handle manual revalidate for fallback: blocking', async () => { const res = await fetchViaHTTP( next.url, diff --git a/test/e2e/prerender/pages/blocking-fallback/[slug].js b/test/e2e/prerender/pages/blocking-fallback/[slug].js index aa6aeec99ac..5665ae6f008 100644 --- a/test/e2e/prerender/pages/blocking-fallback/[slug].js +++ b/test/e2e/prerender/pages/blocking-fallback/[slug].js @@ -1,15 +1,31 @@ +import fs from 'fs' +import path from 'path' import React from 'react' import Link from 'next/link' import { useRouter } from 'next/router' export async function getStaticPaths() { return { - paths: [], + paths: [ + { + params: { slug: 'test-errors-1' }, + }, + ], fallback: 'blocking', } } export async function getStaticProps({ params }) { + if (params.slug.startsWith('test-errors')) { + const errorFile = path.join(process.cwd(), 'error.txt') + if (fs.existsSync(errorFile)) { + const data = await fs.readFileSync(errorFile, 'utf8') + if (data.trim() === 'yes') { + throw new Error('throwing error for /blocking-fallback/' + params.slug) + } + } + } + await new Promise((resolve) => setTimeout(resolve, 1000)) return {