Skip to content

Commit a449e4a

Browse files
authored
fix: clone response in first handler to prevent race (#70082)
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.
1 parent ce9c28f commit a449e4a

File tree

1 file changed

+35
-14
lines changed

1 file changed

+35
-14
lines changed

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

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -859,34 +859,55 @@ export function createPatchedFetcher(
859859
})
860860
}
861861

862-
/**
863-
* We used to just resolve the Response and clone it however for static generation
864-
* with dynamicIO we need the response to be able to be resolved in a microtask
865-
* and Response#clone() will never have a body that can resolve in a microtask in node (as observed through experimentation)
866-
* So instead we await the body and then when it is available we construct manually
867-
* cloned Response objects with the body as an ArrayBuffer. This will be resolvable in
868-
* a microtask making it compatiable with dynamicIO
869-
*/
862+
// We used to just resolve the Response and clone it however for
863+
// static generation with dynamicIO we need the response to be able to
864+
// be resolved in a microtask and Response#clone() will never have a
865+
// body that can resolve in a microtask in node (as observed through
866+
// experimentation) So instead we await the body and then when it is
867+
// available we construct manually cloned Response objects with the
868+
// body as an ArrayBuffer. This will be resolvable in a microtask
869+
// making it compatible with dynamicIO.
870870
const pendingResponse = doOriginalFetch(true, cacheReasonOverride)
871+
871872
const nextRevalidate = pendingResponse
872873
.then(async (response) => {
874+
// Clone the response here. It'll run first because we attached
875+
// the resolve before we returned below. We have to clone it
876+
// because the original response is going to be consumed by
877+
// at a later point in time.
878+
const clonedResponse = response.clone()
879+
873880
return {
874-
body: await response.arrayBuffer(),
875-
headers: response.headers,
876-
status: response.status,
877-
statusText: response.statusText,
881+
body: await clonedResponse.arrayBuffer(),
882+
headers: clonedResponse.headers,
883+
status: clonedResponse.status,
884+
statusText: clonedResponse.statusText,
878885
}
879886
})
880887
.finally(() => {
881-
staticGenerationStore.pendingRevalidates ??= {}
888+
// If the pending revalidate is not present in the store, then
889+
// we have nothing to delete.
890+
if (
891+
!staticGenerationStore.pendingRevalidates?.[
892+
pendingRevalidateKey
893+
]
894+
) {
895+
return
896+
}
897+
882898
delete staticGenerationStore.pendingRevalidates[
883899
pendingRevalidateKey
884900
]
885901
})
902+
903+
// Attach the empty catch here so we don't get a "unhandled promise
904+
// rejection" warning
886905
nextRevalidate.catch(() => {})
906+
887907
staticGenerationStore.pendingRevalidates[pendingRevalidateKey] =
888908
nextRevalidate
889-
return (await pendingResponse).clone()
909+
910+
return pendingResponse
890911
} else {
891912
return doOriginalFetch(false, cacheReasonOverride)
892913
}

0 commit comments

Comments
 (0)