-
Notifications
You must be signed in to change notification settings - Fork 49.9k
[Flight] Fix broken byte stream parsing caused by buffer detachment #35127
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
Merged
unstubbable
merged 4 commits into
facebook:main
from
unstubbable:fix-byte-stream-parsing
Nov 13, 2025
Merged
[Flight] Fix broken byte stream parsing caused by buffer detachment #35127
unstubbable
merged 4 commits into
facebook:main
from
unstubbable:fix-byte-stream-parsing
Nov 13, 2025
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This PR fixes a critical bug where `ReadableStream({type: 'bytes'})`
instances passed through React Server Components (RSC) would stall after
reading only the first chunk or the first few chunks in the client. This
issue was masked by using `web-streams-polyfill` in tests, but manifests
with native Web Streams implementations.
The root cause is that when a chunk is enqueued to a
`ReadableByteStreamController`, the spec requires the underlying
ArrayBuffer to be synchronously transferred/detached. In the React
Flight Client's chunk parsing, embedded byte stream chunks are created
as views into the incoming RSC stream chunk buffer using `new
Uint8Array(chunk.buffer, offset, length)`. When the first embedded byte
stream chunk is enqueued, it detaches the shared buffer, leaving the
entire RSC stream parsing in a broken state.
The fix is to buffer all embedded byte stream chunks during each
`processBinaryChunk()` call and enqueue them at the end, after parsing
is complete. This ensures the parser never accesses detached buffers,
and it allows us to determine if we need to copy chunks. When there's
only a single embedded stream with a single chunk in the RSC stream
chunk, we use a zero-copy optimization and enqueue the buffer directly.
When there are multiple embedded streams in the same RSC stream chunk,
each chunk must be copied to avoid buffer detachment. When the same
stream has multiple chunks in the RSC stream chunk, we concatenate them
into a single contiguous buffer before enqueueing to reduce downstream
overhead.
A future follow-up will implement server-side optimizations to
concatenate multiple embedded byte stream rows before flushing,
increasing the likelihood of hitting the zero-copy path on the client
and reducing per-row RSC protocol overhead.
Tests now use the proper Jest environment with native Web Streams
instead of polyfills, exposing and validating the fix for this issue.
This simplifies the hot-path logic, but also means we're copying more often than in the previous approach, at least for small 'o' chunks.
1fe7fda to
b6fbec3
Compare
This allows us to avoid using the `_pendingByteStreamIDs` Set in the client to track which streams are byte streams.
335effd to
485953e
Compare
sebmarkbage
reviewed
Nov 13, 2025
sebmarkbage
approved these changes
Nov 13, 2025
Collaborator
sebmarkbage
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some nits on the forking. I generally try to avoid boolean flags because passing arguments and checking if conditions in hot code is bad. Better to fork and create a specialized path to avoid adding the forks to the hot path.
036146e to
b3ab687
Compare
b3ab687 to
77cf366
Compare
This was referenced Nov 13, 2025
manNomi
pushed a commit
to manNomi/react
that referenced
this pull request
Nov 15, 2025
…acebook#35127) This PR fixes a critical bug where `ReadableStream({type: 'bytes'})` instances passed through React Server Components (RSC) would stall after reading only the first chunk or the first few chunks in the client. This issue was masked by using `web-streams-polyfill` in tests, but manifests with native Web Streams implementations. The root cause is that when a chunk is enqueued to a `ReadableByteStreamController`, the spec requires the underlying ArrayBuffer to be synchronously transferred/detached. In the React Flight Client's chunk parsing, embedded byte stream chunks are created as views into the incoming RSC stream chunk buffer using `new Uint8Array(chunk.buffer, offset, length)`. When embedded byte stream chunks are enqueued, they can detach the shared buffer, leaving the RSC stream parsing in a broken state. The fix is to copy embedded byte stream chunks before enqueueing them, preventing buffer detachment from affecting subsequent parsing. To not affect performance too much, we use a zero-copy optimization: when a chunk ends exactly at the end of the RSC stream chunk, or when the row spans into the next RSC chunk, no further parsing will access that buffer, so we can safely enqueue the view directly without copying. We now also enqueue embedded byte stream chunks immediately as they are parsed, without waiting for the full row to complete. To simplify the logic in the client, we introduce a new `'b'` protocol tag specifically for byte stream chunks. The server now emits `'b'` instead of `'o'` for `Uint8Array` chunks from byte streams (detected via `supportsBYOB`). This allows the client to recognize byte stream chunks without needing to track stream IDs. Tests now use the proper Jest environment with native Web Streams instead of polyfills, exposing and validating the fix for this issue.
github-actions bot
pushed a commit
to code/lib-react
that referenced
this pull request
Nov 16, 2025
…acebook#35127) This PR fixes a critical bug where `ReadableStream({type: 'bytes'})` instances passed through React Server Components (RSC) would stall after reading only the first chunk or the first few chunks in the client. This issue was masked by using `web-streams-polyfill` in tests, but manifests with native Web Streams implementations. The root cause is that when a chunk is enqueued to a `ReadableByteStreamController`, the spec requires the underlying ArrayBuffer to be synchronously transferred/detached. In the React Flight Client's chunk parsing, embedded byte stream chunks are created as views into the incoming RSC stream chunk buffer using `new Uint8Array(chunk.buffer, offset, length)`. When embedded byte stream chunks are enqueued, they can detach the shared buffer, leaving the RSC stream parsing in a broken state. The fix is to copy embedded byte stream chunks before enqueueing them, preventing buffer detachment from affecting subsequent parsing. To not affect performance too much, we use a zero-copy optimization: when a chunk ends exactly at the end of the RSC stream chunk, or when the row spans into the next RSC chunk, no further parsing will access that buffer, so we can safely enqueue the view directly without copying. We now also enqueue embedded byte stream chunks immediately as they are parsed, without waiting for the full row to complete. To simplify the logic in the client, we introduce a new `'b'` protocol tag specifically for byte stream chunks. The server now emits `'b'` instead of `'o'` for `Uint8Array` chunks from byte streams (detected via `supportsBYOB`). This allows the client to recognize byte stream chunks without needing to track stream IDs. Tests now use the proper Jest environment with native Web Streams instead of polyfills, exposing and validating the fix for this issue. DiffTrain build for [93fc574](facebook@93fc574)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This PR fixes a critical bug where
ReadableStream({type: 'bytes'})instances passed through React Server Components (RSC) would stall after reading only the first chunk or the first few chunks in the client. This issue was masked by usingweb-streams-polyfillin tests, but manifests with native Web Streams implementations.An example for that scenario is if the body of a
fetchresponse is returned from a server action, and then this stream is read in a client component.The root cause is that when a chunk is enqueued to a
ReadableByteStreamController, the spec requires the underlying ArrayBuffer to be synchronously transferred/detached. In the React Flight Client's chunk parsing, embedded byte stream chunks are created as views into the incoming RSC stream chunk buffer usingnew Uint8Array(chunk.buffer, offset, length). When embedded byte stream chunks are enqueued, they can detach the shared buffer, leaving the RSC stream parsing in a broken state.The fix is to copy embedded byte stream chunks before enqueueing them, preventing buffer detachment from affecting subsequent parsing. To not affect performance too much, we use a zero-copy optimization: when a chunk ends exactly at the end of the RSC stream chunk, or when the row spans into the next RSC chunk, no further parsing will access that buffer, so we can safely enqueue the view directly without copying.
We now also enqueue embedded byte stream chunks immediately as they are parsed, without waiting for the full row to complete.
To simplify the logic in the client, we introduce a new
'b'protocol tag specifically for byte stream chunks. The server now emits'b'instead of'o'forUint8Arraychunks from byte streams (detected viasupportsBYOB). This allows the client to recognize byte stream chunks without needing to track stream IDs.Tests now use the proper Jest environment with native Web Streams instead of polyfills, exposing and validating the fix for this issue.