Skip to content

Commit

Permalink
fix: clone response in first handler to prevent race (#70082)
Browse files Browse the repository at this point in the history
This fixes a race where if the body was resolved before the clone
operation, it would clone later, resulting in an error being thrown due
to the body already being consumed.
  • Loading branch information
wyattjoh authored Sep 13, 2024
1 parent ce9c28f commit a449e4a
Showing 1 changed file with 35 additions and 14 deletions.
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

0 comments on commit a449e4a

Please sign in to comment.