Skip to content
Merged
Changes from 1 commit
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
37 changes: 37 additions & 0 deletions packages/next/src/server/lib/clone-response.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
// @ts-ignore https://nodejs.org/api/stream.html#streamreadableisdisturbedstream
import { isDisturbed, isErrored } from 'node:stream'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I read the docs correctly, these are statics on Readable. Can't use node: either since it needs to work in Edge runtimes.

Suggested change
import { isDisturbed, isErrored } from 'node:stream'
import { Readable } from 'stream'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, let me update this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

I'm still not able to get the isDisturbed or isErrored checks working type-wise. However, I'm also not certain they're needed at all.

The stream you are trying to cancel is not a ReadableStream, or it is locked.

https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/cancel#exceptions

Additionally, we're also catching the error, if one were to occur.


// https://github.com/nodejs/undici/blob/c399fdf4246be4992e89efe42aa83062edd0609f/lib/web/fetch/body.js#L32
const noop = () => {}
const registry = new FinalizationRegistry(
(weakRef: WeakRef<ReadableStream>) => {
const stream = weakRef.deref()
if (
stream &&
!stream.locked &&
!isDisturbed(stream) &&
!isErrored(stream as any as NodeJS.ReadableStream)
Copy link
Member

@eps1lon eps1lon Aug 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
!isDisturbed(stream) &&
!isErrored(stream as any as NodeJS.ReadableStream)
!Readable.isDisturbed(stream) &&
!Readable.isErrored(stream)

) {
stream.cancel('Response object has been garbage collected').then(noop)
}
}
)

/**
* Clones a response by teeing the body so we can return two independent
* ReadableStreams from it. This avoids the bug in the undici library around
Expand Down Expand Up @@ -33,6 +52,24 @@ export function cloneResponse(original: Response): [Response, Response] {
writable: false,
})

// The Fetch Standard allows users to skip consuming the response body by
// relying on garbage collection to release connection resources.
// https://github.com/nodejs/undici?tab=readme-ov-file#garbage-collection
//
// To cancel the stream you then need to cancel both resulting branches.
// Teeing a stream will generally lock it for the duration, preventing other
// readers from locking it.
// https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/tee

// cloned2 is stored in a react cache and cloned for subsequent requests.
// It is the original request, and is is garbage collected by a
// FinalizationRegistry in Undici, but since we're tee-ing the stream
// ourselves, we need to cancel clone1's stream (the response returned from
// our dedupe fetch) when clone1 is reclaimed, otherwise we leak memory.
if (cloned1.body) {
registry.register(cloned1, new WeakRef(cloned1.body))
}

const cloned2 = new Response(body2, {
status: original.status,
statusText: original.statusText,
Expand Down
Loading