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

fix: clone response in first handler to prevent race #70082

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 35 additions & 14 deletions packages/next/src/server/lib/patch-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -859,34 +859,55 @@ export function createPatchedFetcher(
})
}

/**
* We used to just resolve the Response and clone it however for static generation
* with dynamicIO we need the response to be able to be resolved in a microtask
* and Response#clone() will never have a body that can resolve in a microtask in node (as observed through experimentation)
* So instead we await the body and then when it is available we construct manually
* cloned Response objects with the body as an ArrayBuffer. This will be resolvable in
* a microtask making it compatiable with dynamicIO
*/
// We used to just resolve the Response and clone it however for
// static generation with dynamicIO we need the response to be able to
// be resolved in a microtask and Response#clone() will never have a
// body that can resolve in a microtask in node (as observed through
// experimentation) So instead we await the body and then when it is
// available we construct manually cloned Response objects with the
// body as an ArrayBuffer. This will be resolvable in a microtask
// making it compatible with dynamicIO.
const pendingResponse = doOriginalFetch(true, cacheReasonOverride)

const nextRevalidate = pendingResponse
.then(async (response) => {
// Clone the response here. It'll run first because we attached
// the resolve before we returned below. We have to clone it
// because the original response is going to be consumed by
// at a later point in time.
const clonedResponse = response.clone()

return {
body: await response.arrayBuffer(),
headers: response.headers,
status: response.status,
statusText: response.statusText,
body: await clonedResponse.arrayBuffer(),
headers: clonedResponse.headers,
status: clonedResponse.status,
statusText: clonedResponse.statusText,
}
})
.finally(() => {
staticGenerationStore.pendingRevalidates ??= {}
// If the pending revalidate is not present in the store, then
// we have nothing to delete.
if (
!staticGenerationStore.pendingRevalidates?.[
pendingRevalidateKey
]
) {
return
}

delete staticGenerationStore.pendingRevalidates[
pendingRevalidateKey
]
})

// Attach the empty catch here so we don't get a "unhandled promise
// rejection" warning
nextRevalidate.catch(() => {})

staticGenerationStore.pendingRevalidates[pendingRevalidateKey] =
nextRevalidate
return (await pendingResponse).clone()

return pendingResponse
} else {
return doOriginalFetch(false, cacheReasonOverride)
}
Expand Down
Loading