diff --git a/packages/next/src/server/lib/incremental-cache/fetch-cache.ts b/packages/next/src/server/lib/incremental-cache/fetch-cache.ts index 4ed3cf0ad7292..f2e62275d3a56 100644 --- a/packages/next/src/server/lib/incremental-cache/fetch-cache.ts +++ b/packages/next/src/server/lib/incremental-cache/fetch-cache.ts @@ -176,29 +176,32 @@ export default class FetchCache implements CacheHandler { return } - try { - const res = await fetchRetryWithTimeout( - `${this.cacheEndpoint}/v1/suspense-cache/revalidate?tags=${tags - .map((tag) => encodeURIComponent(tag)) - .join(',')}`, - { - method: 'POST', - headers: this.headers, - // @ts-expect-error not on public type - next: { internal: true }, - } - ) + for (let i = 0; i < Math.ceil(tags.length / 64); i++) { + const currentTags = tags.slice(i * 64, i * 64 + 64) + try { + const res = await fetchRetryWithTimeout( + `${this.cacheEndpoint}/v1/suspense-cache/revalidate?tags=${currentTags + .map((tag) => encodeURIComponent(tag)) + .join(',')}`, + { + method: 'POST', + headers: this.headers, + // @ts-expect-error not on public type + next: { internal: true }, + } + ) - if (res.status === 429) { - const retryAfter = res.headers.get('retry-after') || '60000' - rateLimitedUntil = Date.now() + parseInt(retryAfter) - } + if (res.status === 429) { + const retryAfter = res.headers.get('retry-after') || '60000' + rateLimitedUntil = Date.now() + parseInt(retryAfter) + } - if (!res.ok) { - throw new Error(`Request failed with status ${res.status}.`) + if (!res.ok) { + throw new Error(`Request failed with status ${res.status}.`) + } + } catch (err) { + console.warn(`Failed to revalidate tag`, currentTags, err) } - } catch (err) { - console.warn(`Failed to revalidate tag ${tags}`, err) } } diff --git a/test/production/app-dir/fetch-cache/app/api/revalidate-alot/route.ts b/test/production/app-dir/fetch-cache/app/api/revalidate-alot/route.ts new file mode 100644 index 0000000000000..a418da3eff3cb --- /dev/null +++ b/test/production/app-dir/fetch-cache/app/api/revalidate-alot/route.ts @@ -0,0 +1,11 @@ +import { revalidateTag } from 'next/cache' +import { NextRequest, NextResponse } from 'next/server' + +export const dynamic = 'force-dynamic' + +export function GET(req: NextRequest) { + for (let i = 0; i < 130; i++) { + revalidateTag(`thankyounext-${i}`) + } + return NextResponse.json({ done: true }) +} diff --git a/test/production/app-dir/fetch-cache/fetch-cache.test.ts b/test/production/app-dir/fetch-cache/fetch-cache.test.ts index fd9cdb0f311a9..78b7559ad307e 100644 --- a/test/production/app-dir/fetch-cache/fetch-cache.test.ts +++ b/test/production/app-dir/fetch-cache/fetch-cache.test.ts @@ -18,6 +18,7 @@ describe('fetch-cache', () => { let nextInstance: any let fetchGetReqIndex = 0 let revalidateReqIndex = 0 + let revalidateReqShouldTimeout = false let fetchGetShouldError = false let fetchCacheServer: http.Server let fetchCacheRequests: Array<{ @@ -101,6 +102,7 @@ describe('fetch-cache', () => { fetchCacheRequests = [] storeCacheItems = false fetchGetShouldError = false + revalidateReqShouldTimeout = false fetchCacheServer = http.createServer(async (req, res) => { console.log(`fetch cache request ${req.url} ${req.method}`, req.headers) const parsedUrl = new URL(req.url || '/', 'http://n') @@ -114,7 +116,8 @@ describe('fetch-cache', () => { if (parsedUrl.pathname === '/v1/suspense-cache/revalidate') { revalidateReqIndex += 1 // timeout unless it's 3rd retry - const shouldTimeout = revalidateReqIndex % 3 !== 0 + const shouldTimeout = + revalidateReqShouldTimeout && revalidateReqIndex % 3 !== 0 if (shouldTimeout) { console.log('not responding for', req.url, { revalidateReqIndex }) @@ -220,10 +223,26 @@ describe('fetch-cache', () => { }) it('should retry 3 times when revalidate times out', async () => { - await fetchViaHTTP(appPort, '/api/revalidate') + revalidateReqShouldTimeout = true + try { + await fetchViaHTTP(appPort, '/api/revalidate') + + await retry(() => { + expect(revalidateReqIndex).toBe(3) + }) + expect(cliOuptut).not.toContain('Failed to revalidate') + expect(cliOuptut).not.toContain('Error') + } finally { + revalidateReqShouldTimeout = false + } + }) + + it('should batch revalidate tag requests if > 64', async () => { + const revalidateReqIndexStart = revalidateReqIndex + await fetchViaHTTP(appPort, '/api/revalidate-alot') await retry(() => { - expect(revalidateReqIndex).toBe(3) + expect(revalidateReqIndex).toBe(revalidateReqIndexStart + 3) }) expect(cliOuptut).not.toContain('Failed to revalidate') expect(cliOuptut).not.toContain('Error') diff --git a/test/turbopack-build-tests-manifest.json b/test/turbopack-build-tests-manifest.json index 5ce14b64b4976..59c96caf3710a 100644 --- a/test/turbopack-build-tests-manifest.json +++ b/test/turbopack-build-tests-manifest.json @@ -14633,7 +14633,8 @@ "fetch-cache should have correct fetchUrl field for fetches and unstable_cache", "fetch-cache should not retry for failed fetch-cache GET", "fetch-cache should retry 3 times when revalidate times out", - "fetch-cache should update cache TTL even if cache data does not change" + "fetch-cache should update cache TTL even if cache data does not change", + "fetch-cache should batch revalidate tag requests if > 64" ], "pending": [], "flakey": [],