You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
React 18: "The stream is not in a state that permits close" in renderToReadableStream #22772
When using renderToReadableStream for e.g. Cloudflare Workers runtimes, I'm noticing this error on the server:
TypeError: The stream is not in a state that permits close
at ReadableStreamDefaultController.close (/Users/joshlarson/src/github.com/jplhomer/vite-streaming-ssr-demo/node_modules/web-streams-polyfill/src/lib/readable-stream/default-controller.ts:72:13)
at Mc (/Users/joshlarson/src/github.com/jplhomer/vite-streaming-ssr-demo/dist/worker/worker.js:3636:145)
at Bc (/Users/joshlarson/src/github.com/jplhomer/vite-streaming-ssr-demo/dist/worker/worker.js:3457:33)
at ping (/Users/joshlarson/src/github.com/jplhomer/vite-streaming-ssr-demo/dist/worker/worker.js:3128:24)
GET / 200 OK (2091.13ms)
I'm assuming this results in the following malformed chunk being sent to the stream:
<div hidden id="<div hidden id="S:1">
Which results in the client failing:
Uncaught TypeError: Cannot read properties of null (reading 'parentNode')
at $RS (127.0.0.1/:11)
at 127.0.0.1/:11
Maybe something's wrong with Miniflare's polyfill and this is a non-issue, coming from web-streams-polyfill?
Finally, it's possible something is wrong with the Vite version of the example I've put together. I could try rebuilding with webpack if it's determined none of these items apply.
Have you looked at what chain of assumptions leads to this? It seems like it would be good to understand what different state machines are involved and why something's happening in CF that doesn't happen in the browser.
changed the title [-]React 18: "The stream is not in a state that permits close" in `pipeToReadableStream`[/-][+]React 18: "The stream is not in a state that permits close" in `renderToReadableStream`[/+]on Dec 1, 2021
Have you looked at what chain of assumptions leads to this?
@gaearon after lots of digging, here's what we've discovered:
The difference between Cloudflare/Oxygen Workers runtimes and the browser comes down to how the stream is consumed after being passed to Response.
In the browser and related fizz tests, this is how the stream (via response.body) is consumed:
awaitresponse.blob();
This presumably results in a single pull.
In the worker runtime, response.body is read through an async iterator:
forawait(constchunkofresponse.body){// write chunk from server}
Importantly, this results in pull getting called multiple times as more data is enqueued and as the reader sees fit. This is an assumption on my part, so I could be wrong here.
When combined with Suspense boundaries, pingTask callbacks trigger flushCompletedQueues. At the same time, repeated pull requests from the stream reader also call flushCompletedQueues.
This results in the following error on the server:
Error: Aborted, errored or already flushed boundaries should not be flushed again. This is a bug in React.
at flushSubtree (/Users/joshlarson/src/github.com/jplhomer/vite-streaming-ssr-demo/dist/worker/worker.js:12538:15)
at flushSegment (/Users/joshlarson/src/github.com/jplhomer/vite-streaming-ssr-demo/dist/worker/worker.js:12545:14)
at flushSegmentContainer (/Users/joshlarson/src/github.com/jplhomer/vite-streaming-ssr-demo/dist/worker/worker.js:12583:5)
at flushPartiallyCompletedSegment (/Users/joshlarson/src/github.com/jplhomer/vite-streaming-ssr-demo/dist/worker/worker.js:12622:7)
at flushCompletedBoundary (/Users/joshlarson/src/github.com/jplhomer/vite-streaming-ssr-demo/dist/worker/worker.js:12591:7)
at flushCompletedQueues (/Users/joshlarson/src/github.com/jplhomer/vite-streaming-ssr-demo/dist/worker/worker.js:12659:14)
at performWork (/Users/joshlarson/src/github.com/jplhomer/vite-streaming-ssr-demo/dist/worker/worker.js:12497:9)
at /Users/joshlarson/src/github.com/jplhomer/vite-streaming-ssr-demo/dist/worker/worker.js:11752:16
at scheduleWork (/Users/joshlarson/src/github.com/jplhomer/vite-streaming-ssr-demo/dist/worker/worker.js:8042:5)
at pingTask (/Users/joshlarson/src/github.com/jplhomer/vite-streaming-ssr-demo/dist/worker/worker.js:11751:7)
You can also reproduce this directly in the browser by attempting to read the stream with response.body.getReader(). Here is a new fixture with a reproduction of the issue, including a couple test cases that show the different errors you'll see based on the number of Suspense boundaries you have:
We'll fix this reentrancy bug somehow. However, right now it's a bit lower priority because CloudFlare technically don't support ReadableStreams and so you have to go through the polyfill and any subtle differences might be due to the polyfill. To really know if we fixed this issue we should apply this to native CloudFlare ReadableStreams... which I'm told is coming. So that's how we know if it actually worked. Until then, the support for CloudFlare workers is... partial.
On a second thought this should probably fix it. I think I finally understand how it's supposed to work now. #23342
Activity
gaearon commentedon Nov 18, 2021
Have you looked at what chain of assumptions leads to this? It seems like it would be good to understand what different state machines are involved and why something's happening in CF that doesn't happen in the browser.
[-]React 18: "The stream is not in a state that permits close" in `pipeToReadableStream`[/-][+]React 18: "The stream is not in a state that permits close" in `renderToReadableStream`[/+]jplhomer commentedon Dec 1, 2021
@gaearon after lots of digging, here's what we've discovered:
The difference between Cloudflare/Oxygen Workers runtimes and the browser comes down to how the
streamis consumed after being passed toResponse.In the browser and related fizz tests, this is how the stream (via
response.body) is consumed:This presumably results in a single
pull.In the worker runtime,
response.bodyis read through an async iterator:Importantly, this results in
pullgetting called multiple times as more data is enqueued and as the reader sees fit. This is an assumption on my part, so I could be wrong here.When combined with Suspense boundaries,
pingTaskcallbacks triggerflushCompletedQueues. At the same time, repeatedpullrequests from the stream reader also callflushCompletedQueues.This results in the following error on the server:
You can also reproduce this directly in the browser by attempting to read the stream with
response.body.getReader(). Here is a new fixture with a reproduction of the issue, including a couple test cases that show the different errors you'll see based on the number of Suspense boundaries you have:https://github.com/jplhomer/react/blob/jl-fizz-ssr-streaming-browser/fixtures/fizz-ssr-browser-streaming/index.html
If this seems like a legitimate bug to you, a couple ideas I had:
if (isAlreadyFlushingQueues) returninflushCompletedQueues?Here are some other resources and scratch notes: jplhomer/vite-streaming-ssr-demo#1 (comment)
fix: prevent concurrent flushing issues by tracking flushing status.
sebmarkbage commentedon Feb 23, 2022
We'll fix this reentrancy bug somehow. However, right now it's a bit lower priority because CloudFlare technically don't support ReadableStreams and so you have to go through the polyfill and any subtle differences might be due to the polyfill. To really know if we fixed this issue we should apply this to native CloudFlare ReadableStreams... which I'm told is coming. So that's how we know if it actually worked. Until then, the support for CloudFlare workers is... partial.On a second thought this should probably fix it. I think I finally understand how it's supposed to work now. #23342
Re-add reentrancy avoidance
Re-add reentrancy avoidance
Re-add reentrancy avoidance (#23342)
sebmarkbage commentedon Feb 23, 2022
@jplhomer Please take another look and see if this fixes all the issues you've noticed.
jplhomer commentedon Feb 23, 2022
@sebmarkbage works like a charm! Thanks for taking the time to look at it and get it fixed 🎉
Re-add reentrancy avoidance (react#23342)
Re-add reentrancy avoidance (react#23342)