Skip to content

Commit 9431336

Browse files
authored
fix fetch lock not being consistently released (#75028)
1 parent 25e8443 commit 9431336

File tree

4 files changed

+190
-115
lines changed

4 files changed

+190
-115
lines changed

packages/next/src/server/lib/patch-fetch.ts

+122-115
Original file line numberDiff line numberDiff line change
@@ -610,132 +610,139 @@ export function createPatchedFetcher(
610610
next: { ...init?.next, fetchType: 'origin', fetchIdx },
611611
}
612612

613-
return originFetch(input, clonedInit).then(async (res) => {
614-
if (!isStale && fetchStart) {
615-
trackFetchMetric(workStore, {
616-
start: fetchStart,
617-
url: fetchUrl,
618-
cacheReason: cacheReasonOverride || cacheReason,
619-
cacheStatus:
620-
finalRevalidate === 0 || cacheReasonOverride
621-
? 'skip'
622-
: 'miss',
623-
cacheWarning,
624-
status: res.status,
625-
method: clonedInit.method || 'GET',
626-
})
627-
}
628-
if (
629-
res.status === 200 &&
630-
incrementalCache &&
631-
cacheKey &&
632-
(isCacheableRevalidate || requestStore?.serverComponentsHmrCache)
633-
) {
634-
const normalizedRevalidate =
635-
finalRevalidate >= INFINITE_CACHE
636-
? CACHE_ONE_YEAR
637-
: finalRevalidate
638-
const externalRevalidate =
639-
finalRevalidate >= INFINITE_CACHE ? false : finalRevalidate
640-
641-
if (workUnitStore && workUnitStore.type === 'prerender') {
642-
// We are prerendering at build time or revalidate time with dynamicIO so we need to
643-
// buffer the response so we can guarantee it can be read in a microtask
644-
const bodyBuffer = await res.arrayBuffer()
645-
646-
const fetchedData = {
647-
headers: Object.fromEntries(res.headers.entries()),
648-
body: Buffer.from(bodyBuffer).toString('base64'),
613+
return originFetch(input, clonedInit)
614+
.then(async (res) => {
615+
if (!isStale && fetchStart) {
616+
trackFetchMetric(workStore, {
617+
start: fetchStart,
618+
url: fetchUrl,
619+
cacheReason: cacheReasonOverride || cacheReason,
620+
cacheStatus:
621+
finalRevalidate === 0 || cacheReasonOverride
622+
? 'skip'
623+
: 'miss',
624+
cacheWarning,
649625
status: res.status,
650-
url: res.url,
651-
}
652-
653-
// We can skip checking the serverComponentsHmrCache because we aren't in
654-
// dev mode.
655-
656-
await incrementalCache.set(
657-
cacheKey,
658-
{
659-
kind: CachedRouteKind.FETCH,
660-
data: fetchedData,
661-
revalidate: normalizedRevalidate,
662-
},
663-
{
664-
fetchCache: true,
665-
revalidate: externalRevalidate,
666-
fetchUrl,
667-
fetchIdx,
668-
tags,
626+
method: clonedInit.method || 'GET',
627+
})
628+
}
629+
if (
630+
res.status === 200 &&
631+
incrementalCache &&
632+
cacheKey &&
633+
(isCacheableRevalidate ||
634+
requestStore?.serverComponentsHmrCache)
635+
) {
636+
const normalizedRevalidate =
637+
finalRevalidate >= INFINITE_CACHE
638+
? CACHE_ONE_YEAR
639+
: finalRevalidate
640+
const externalRevalidate =
641+
finalRevalidate >= INFINITE_CACHE ? false : finalRevalidate
642+
643+
if (workUnitStore && workUnitStore.type === 'prerender') {
644+
// We are prerendering at build time or revalidate time with dynamicIO so we need to
645+
// buffer the response so we can guarantee it can be read in a microtask
646+
const bodyBuffer = await res.arrayBuffer()
647+
648+
const fetchedData = {
649+
headers: Object.fromEntries(res.headers.entries()),
650+
body: Buffer.from(bodyBuffer).toString('base64'),
651+
status: res.status,
652+
url: res.url,
669653
}
670-
)
671-
await handleUnlock()
672654

673-
// We we return a new Response to the caller.
674-
return new Response(bodyBuffer, {
675-
headers: res.headers,
676-
status: res.status,
677-
statusText: res.statusText,
678-
})
679-
} else {
680-
// We're cloning the response using this utility because there
681-
// exists a bug in the undici library around response cloning.
682-
// See the following pull request for more details:
683-
// https://github.com/vercel/next.js/pull/73274
684-
const [cloned1, cloned2] = cloneResponse(res)
685-
686-
// We are dynamically rendering including dev mode. We want to return
687-
// the response to the caller as soon as possible because it might stream
688-
// over a very long time.
689-
cloned1
690-
.arrayBuffer()
691-
.then(async (arrayBuffer) => {
692-
const bodyBuffer = Buffer.from(arrayBuffer)
693-
694-
const fetchedData = {
695-
headers: Object.fromEntries(cloned1.headers.entries()),
696-
body: bodyBuffer.toString('base64'),
697-
status: cloned1.status,
698-
url: cloned1.url,
655+
// We can skip checking the serverComponentsHmrCache because we aren't in
656+
// dev mode.
657+
658+
await incrementalCache.set(
659+
cacheKey,
660+
{
661+
kind: CachedRouteKind.FETCH,
662+
data: fetchedData,
663+
revalidate: normalizedRevalidate,
664+
},
665+
{
666+
fetchCache: true,
667+
revalidate: externalRevalidate,
668+
fetchUrl,
669+
fetchIdx,
670+
tags,
699671
}
672+
)
673+
await handleUnlock()
700674

701-
requestStore?.serverComponentsHmrCache?.set(
702-
cacheKey,
703-
fetchedData
704-
)
705-
706-
if (isCacheableRevalidate) {
707-
await incrementalCache.set(
675+
// We return a new Response to the caller.
676+
return new Response(bodyBuffer, {
677+
headers: res.headers,
678+
status: res.status,
679+
statusText: res.statusText,
680+
})
681+
} else {
682+
// We're cloning the response using this utility because there
683+
// exists a bug in the undici library around response cloning.
684+
// See the following pull request for more details:
685+
// https://github.com/vercel/next.js/pull/73274
686+
687+
const [cloned1, cloned2] = cloneResponse(res)
688+
689+
// We are dynamically rendering including dev mode. We want to return
690+
// the response to the caller as soon as possible because it might stream
691+
// over a very long time.
692+
cloned1
693+
.arrayBuffer()
694+
.then(async (arrayBuffer) => {
695+
const bodyBuffer = Buffer.from(arrayBuffer)
696+
697+
const fetchedData = {
698+
headers: Object.fromEntries(cloned1.headers.entries()),
699+
body: bodyBuffer.toString('base64'),
700+
status: cloned1.status,
701+
url: cloned1.url,
702+
}
703+
704+
requestStore?.serverComponentsHmrCache?.set(
708705
cacheKey,
709-
{
710-
kind: CachedRouteKind.FETCH,
711-
data: fetchedData,
712-
revalidate: normalizedRevalidate,
713-
},
714-
{
715-
fetchCache: true,
716-
revalidate: externalRevalidate,
717-
fetchUrl,
718-
fetchIdx,
719-
tags,
720-
}
706+
fetchedData
721707
)
722-
}
723-
})
724-
.catch((error) =>
725-
console.warn(`Failed to set fetch cache`, input, error)
726-
)
727-
.finally(handleUnlock)
728708

729-
return cloned2
709+
if (isCacheableRevalidate) {
710+
await incrementalCache.set(
711+
cacheKey,
712+
{
713+
kind: CachedRouteKind.FETCH,
714+
data: fetchedData,
715+
revalidate: normalizedRevalidate,
716+
},
717+
{
718+
fetchCache: true,
719+
revalidate: externalRevalidate,
720+
fetchUrl,
721+
fetchIdx,
722+
tags,
723+
}
724+
)
725+
}
726+
})
727+
.catch((error) =>
728+
console.warn(`Failed to set fetch cache`, input, error)
729+
)
730+
.finally(handleUnlock)
731+
732+
return cloned2
733+
}
730734
}
731-
}
732735

733-
// we had response that we determined shouldn't be cached so we return it
734-
// and don't cache it. This also needs to unlock the cache lock we acquired.
735-
await handleUnlock()
736+
// we had response that we determined shouldn't be cached so we return it
737+
// and don't cache it. This also needs to unlock the cache lock we acquired.
738+
await handleUnlock()
736739

737-
return res
738-
})
740+
return res
741+
})
742+
.catch((error) => {
743+
handleUnlock()
744+
throw error
745+
})
739746
}
740747

741748
let cacheReasonOverride
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { nextTestSetup } from 'e2e-utils'
2+
3+
describe('app-fetch-errors', () => {
4+
const { next } = nextTestSetup({
5+
files: __dirname,
6+
})
7+
8+
it('should still successfully render when a fetch request that acquires a cache lock errors', async () => {
9+
const browser = await next.browser('/1')
10+
11+
expect(await browser.elementByCss('body').text()).toBe('Hello World 1')
12+
})
13+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use server'
2+
3+
import { Metadata } from 'next'
4+
5+
export async function generateMetadata({
6+
params,
7+
}: {
8+
params: Promise<{ id: string }>
9+
}): Promise<Metadata> {
10+
const { id } = await params
11+
12+
try {
13+
// this fetch request will error
14+
await fetch('http://localhost:8000', {
15+
cache: 'force-cache',
16+
next: { tags: ['id'] },
17+
})
18+
} catch (err) {
19+
console.log(err)
20+
}
21+
22+
return {
23+
title: id,
24+
}
25+
}
26+
27+
export default async function Error({
28+
params,
29+
}: {
30+
params: Promise<{ id: string }>
31+
}) {
32+
const { id } = await params
33+
34+
try {
35+
// this fetch request will error
36+
await fetch('http://localhost:8000', {
37+
cache: 'force-cache',
38+
next: { tags: ['id'] },
39+
})
40+
} catch (err) {
41+
console.log(err)
42+
}
43+
44+
return <div>Hello World {id}</div>
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export default function Layout({ children }) {
2+
return (
3+
<html lang="en">
4+
<head>
5+
<title>my static site</title>
6+
</head>
7+
<body>{children}</body>
8+
</html>
9+
)
10+
}

0 commit comments

Comments
 (0)