Skip to content

Commit 0dc6373

Browse files
committed
fix: fixed dedupe response cloning
1 parent f911175 commit 0dc6373

File tree

1 file changed

+33
-24
lines changed

1 file changed

+33
-24
lines changed

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

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ function generateCacheKey(request: Request): string {
2525
}
2626

2727
export function createDedupeFetch(originalFetch: typeof fetch) {
28-
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- url is the cache key
29-
const getCacheEntries = React.cache((url: string): Array<any> => [])
28+
const getCacheEntries = React.cache(
29+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- url is the cache key
30+
(url: string): Array<[key: string, promise: Promise<Response>]> => []
31+
)
3032

3133
return function dedupeFetch(
3234
resource: URL | RequestInfo,
@@ -42,6 +44,7 @@ export function createDedupeFetch(originalFetch: typeof fetch) {
4244
// Request constructor.
4345
return originalFetch(resource, options)
4446
}
47+
4548
// Normalize the Request
4649
let url: string
4750
let cacheKey: string
@@ -73,30 +76,36 @@ export function createDedupeFetch(originalFetch: typeof fetch) {
7376
url = request.url
7477
}
7578

79+
// Get the cache entries for the given URL.
7680
const cacheEntries = getCacheEntries(url)
77-
let match
78-
if (cacheEntries.length === 0) {
79-
// We pass the original arguments here in case normalizing the Request
80-
// doesn't include all the options in this environment.
81-
match = originalFetch(resource, options)
82-
cacheEntries.push(cacheKey, match)
83-
} else {
84-
// We use an array as the inner data structure since it's lighter and
85-
// we typically only expect to see one or two entries here.
86-
for (let i = 0, l = cacheEntries.length; i < l; i += 2) {
87-
const key = cacheEntries[i]
88-
const value = cacheEntries[i + 1]
89-
if (key === cacheKey) {
90-
match = value
91-
// I would've preferred a labelled break but lint says no.
92-
return match.then((response: Response) => response.clone())
93-
}
81+
82+
// Check if there is a cached entry for the given cache key. If there is, we
83+
// return the cached response (cloned). This will keep the cached promise to
84+
// remain unused and can be cloned on future requests.
85+
for (const [key, promise] of cacheEntries) {
86+
if (key !== cacheKey) {
87+
continue
9488
}
95-
match = originalFetch(resource, options)
96-
cacheEntries.push(cacheKey, match)
89+
90+
return promise.then((response: Response) => response.clone())
9791
}
98-
// We clone the response so that each time you call this you get a new read
99-
// of the body so that it can be read multiple times.
100-
return match.then((response) => response.clone())
92+
93+
// We pass the original arguments here in case normalizing the Request
94+
// doesn't include all the options in this environment.
95+
const original = originalFetch(resource, options)
96+
97+
// We then clone the original response. We store this in the cache so that
98+
// any future requests will be using this cloned response.
99+
const cloned = original.then((response) => response.clone())
100+
101+
// Attach an empty catch here so we don't get a "unhandled promise
102+
// rejection" warning
103+
cloned.catch(() => {})
104+
105+
cacheEntries.push([cacheKey, cloned])
106+
107+
// Return the promise so that the caller can await it. We pass back the
108+
// original promise.
109+
return original
101110
}
102111
}

0 commit comments

Comments
 (0)