Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update ISR error handling #34931

Merged
merged 2 commits into from Mar 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 21 additions & 9 deletions packages/next/server/response-cache.ts
Expand Up @@ -60,15 +60,17 @@ type ResponseGenerator = (
hadCache: boolean
) => Promise<ResponseCacheEntry | null>

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<IncrementalCacheItem>
set: (
key: string,
data: IncrementalCacheValue | null,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
44 changes: 44 additions & 0 deletions test/e2e/prerender.test.ts
Expand Up @@ -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,
Expand Down
18 changes: 17 additions & 1 deletion 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 {
Expand Down