Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix streaming SSR in react-dom/server.browser #22889

Closed

Conversation

jplhomer
Copy link
Contributor

@jplhomer jplhomer commented Dec 9, 2021

Summary

In #22772, I document an issue we've discovered while using react-dom/server.browser in both a web browser and a Workers runtime (like Cloudflare Workers or Shopify Oxygen).

The root of the problem appears to be related to the fact that the reader of a ReadableStream can call pull whenever it pleases, which triggers flushCompletedQueues. This conflicts with Suspense boundaries which enqueue additional calls to flushCompletedQueues via pingTask.

This problem has not yet surfaced in existing fixtures (fizz-ssr-browser) because await response.blob() is called, which results in pull being called exactly once more after the stream is locked.

This PR adds a new status to Request called FLUSHING which gets set at the top of flushCompletedQueues and returned back to OPEN in the finally block.

I have no idea if this is the correct solution, but it seems to solve the problem in the test fixture I've provided in this PR.

How did you test this change?

To replicate this problem, comment out the changes made to flushCompletedQueues in this PR and run:

yarn build react/index,react-dom --type=UMD
open fixtures/fizz-ssr-browser-streaming/index.html

This should open a page in your web browser which results in the following:

Screen Shot 2021-12-08 at 9 47 41 PM

You can get a slightly different error if you add an additional Suspense boundary! Uncomment the second boundary in the fixtures/fizz-ssr-browser-streaming/index.html file and reload the page:

Screen Shot 2021-12-08 at 9 52 29 PM

To resolve this issue, you can uncomment the changes made in this PR to flushCompletedQueues and run this again:

yarn build react/index,react-dom --type=UMD
open fixtures/fizz-ssr-browser-streaming/index.html

Now you should see the streaming resolve fine without competing calls to flushCompletedQueues (for a single and for multiple Suspense boundaries):

Screen Shot 2021-12-08 at 9 57 20 PM

@facebook-github-bot
Copy link

Hi @jplhomer!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at [email protected]. Thanks!

@sizebot
Copy link

sizebot commented Dec 9, 2021

Comparing: f2a59df...1eb23c3

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.min.js = 129.54 kB 129.54 kB = 41.51 kB 41.51 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js = 134.68 kB 134.68 kB = 43.02 kB 43.02 kB
facebook-www/ReactDOM-prod.classic.js = 427.78 kB 427.78 kB = 78.55 kB 78.55 kB
facebook-www/ReactDOM-prod.modern.js = 417.47 kB 417.47 kB = 77.07 kB 77.07 kB
facebook-www/ReactDOMForked-prod.classic.js = 427.78 kB 427.78 kB = 78.55 kB 78.55 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
facebook-www/ReactDOMServer-prod.modern.js +0.40% 76.22 kB 76.52 kB +0.17% 16.30 kB 16.33 kB
oss-stable-semver/react-server/cjs/react-server.production.min.js +0.20% 19.65 kB 19.69 kB +0.19% 6.86 kB 6.87 kB
oss-stable/react-server/cjs/react-server.production.min.js +0.20% 19.65 kB 19.69 kB +0.19% 6.86 kB 6.87 kB
oss-experimental/react-server/cjs/react-server.production.min.js +0.20% 19.83 kB 19.87 kB +0.19% 6.91 kB 6.93 kB

Generated by 🚫 dangerJS against 1eb23c3

@jplhomer
Copy link
Contributor Author

jplhomer commented Dec 9, 2021

FYI: this PR looks super similar, so I decided to test my fixture against it: #22726

I'm still getting the same error, so unfortunately that wasn't the fix, either.

@facebook-github-bot
Copy link

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

@bvaughn bvaughn requested a review from sebmarkbage December 9, 2021 14:47
@sebmarkbage
Copy link
Collaborator

sebmarkbage commented Dec 9, 2021

This seems similar to the hack I removed in #22446

I was able to work around that specific issue in another way. Ideally we should try working around it here too.

It's a bit annoying because we'd like to not have these hacks - and especially in the core implementation rather than a specific runtime.

It's extra annoying because the browser implementation doesn't follow the spec here and so it's not covered by the non-browser test. Is this also a case where Workers aren't following the spec?

@jplhomer
Copy link
Contributor Author

jplhomer commented Dec 9, 2021

@sebmarkbage it looks like the workaround in the PR you linked was to prevent calling startFlowing until stream.locked was true, but it's already behind stream.locked in FizzServer.

Could you share what specific thing the browser implementation isn't following in the spec here? Happy to dig in further and maybe add a workaround in the *Browser config instead.

So far, we've observed this behavior in:

  • The browser
  • Shopify's Oxygen runtime (response.body.getReader reading chunks into v8go)
  • Node's new Web Streams API

sebmarkbage added a commit to sebmarkbage/react that referenced this pull request Feb 23, 2022
I removed the equivalency of this in facebook#22446. However, I didn't fully
understand the intended semantics of the spec but I understand this better.

I believe this fixes facebook#22772.

This includes the test from facebook#22889 but should ideally have one for Fizz.
sebmarkbage added a commit to sebmarkbage/react that referenced this pull request Feb 23, 2022
I removed the equivalency of this in facebook#22446. However, I didn't fully
understand the intended semantics of the spec but I understand this better
now.

The spec is not actually recursive. It won't call pull again inside of a
pull. It might not call it inside startWork neither which the original
issue avoided. However, it will call pull if you enqueue to the controller
without filling up the desired size outside any call.

We could avoid that by returning a Promise from pull that we wait to
resolve until we've performed all our pending tasks. That would be the
more idiomatic solution. That's a bit more involved but since we know
understand it, we can readd the reentrancy hack since we have an easy place
to detect it. If anything, it should probably throw or log here otherwise.

I believe this fixes facebook#22772.

This includes the test from facebook#22889 but should ideally have one for Fizz.
sebmarkbage added a commit that referenced this pull request Feb 23, 2022
* tests: add failing test to demonstrate bug in ReadableStream implementation

* Re-add reentrancy avoidance

I removed the equivalency of this in #22446. However, I didn't fully
understand the intended semantics of the spec but I understand this better
now.

The spec is not actually recursive. It won't call pull again inside of a
pull. It might not call it inside startWork neither which the original
issue avoided. However, it will call pull if you enqueue to the controller
without filling up the desired size outside any call.

We could avoid that by returning a Promise from pull that we wait to
resolve until we've performed all our pending tasks. That would be the
more idiomatic solution. That's a bit more involved but since we know
understand it, we can readd the reentrancy hack since we have an easy place
to detect it. If anything, it should probably throw or log here otherwise.

I believe this fixes #22772.

This includes the test from #22889 but should ideally have one for Fizz.

Co-authored-by: Josh Larson <[email protected]>
@jplhomer
Copy link
Contributor Author

Fixed by #23342

@jplhomer jplhomer closed this Feb 23, 2022
@jplhomer jplhomer deleted the jl-fizz-ssr-streaming-browser branch February 23, 2022 16:33
zhengjitf pushed a commit to zhengjitf/react that referenced this pull request Apr 15, 2022
* tests: add failing test to demonstrate bug in ReadableStream implementation

* Re-add reentrancy avoidance

I removed the equivalency of this in facebook#22446. However, I didn't fully
understand the intended semantics of the spec but I understand this better
now.

The spec is not actually recursive. It won't call pull again inside of a
pull. It might not call it inside startWork neither which the original
issue avoided. However, it will call pull if you enqueue to the controller
without filling up the desired size outside any call.

We could avoid that by returning a Promise from pull that we wait to
resolve until we've performed all our pending tasks. That would be the
more idiomatic solution. That's a bit more involved but since we know
understand it, we can readd the reentrancy hack since we have an easy place
to detect it. If anything, it should probably throw or log here otherwise.

I believe this fixes facebook#22772.

This includes the test from facebook#22889 but should ideally have one for Fizz.

Co-authored-by: Josh Larson <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants