From b731fe28cc492cb36c51c89866f5b63a3ffae2aa Mon Sep 17 00:00:00 2001 From: Christian Van <113378434+cvan20191@users.noreply.github.com> Date: Wed, 17 Dec 2025 06:22:26 -0500 Subject: [PATCH] Improve cyclic thenable detection in ReactFlightReplyServer (#35369) ## Summary This PR improves cyclic thenable detection in `ReactFlightReplyServer.js`. Fixes #35368. The previous fix only detected direct self-references (`inspectedValue === chunk`) and relied on the `cycleProtection` counter to eventually bail out of longer cycles. This change keeps the existing MAX_THENABLE_CYCLE_DEPTH ($1000$) `cycleProtection` cap as a hard guardrail and adds a visited set so that we can detect self-cycles and multi-node cycles as soon as any `ReactPromise` is revisited and while still bounding the amount of work we do for deep acyclic chains via `cycleProtection`. ## How did you test this change? - Ran the existing test suite for the server renderer: ```bash yarn test react-server yarn test --prod react-server yarn flow dom-node yarn linc ``` --------- Co-authored-by: Hendrik Liebau --- packages/react-server/src/ReactFlightReplyServer.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/react-server/src/ReactFlightReplyServer.js b/packages/react-server/src/ReactFlightReplyServer.js index 4d5a6da8b8..88781cc6eb 100644 --- a/packages/react-server/src/ReactFlightReplyServer.js +++ b/packages/react-server/src/ReactFlightReplyServer.js @@ -133,14 +133,20 @@ ReactPromise.prototype.then = function ( // Recursively check if the value is itself a ReactPromise and if so if it points // back to itself. This helps catch recursive thenables early error. let cycleProtection = 0; + const visited = new Set(); while (inspectedValue instanceof ReactPromise) { cycleProtection++; - if (inspectedValue === chunk || cycleProtection > 1000) { + if ( + inspectedValue === chunk || + visited.has(inspectedValue) || + cycleProtection > 1000 + ) { if (typeof reject === 'function') { reject(new Error('Cannot have cyclic thenables.')); } return; } + visited.add(inspectedValue); if (inspectedValue.status === INITIALIZED) { inspectedValue = inspectedValue.value; } else {