Skip to content

Commit dc485c7

Browse files
authored
[Flight] Fix detached ArrayBuffer error when streaming typed arrays (#34849)
Using `renderToReadableStream` in Node.js with binary data from `fs.readFileSync` (or `Buffer.allocUnsafe`) could cause downstream consumers (like compression middleware) to fail with "Cannot perform Construct on a detached ArrayBuffer". The issue occurs because Node.js uses an 8192-byte Buffer pool for small allocations (< 4KB). When React's `VIEW_SIZE` was 2KB, files between ~2KB and 4KB would be passed through as views of pooled buffers rather than copied into `currentView`. ByteStreams (`type: 'bytes'`) detach ArrayBuffers during transfer, which corrupts the shared Buffer pool and causes subsequent Buffer operations to fail. Increasing `VIEW_SIZE` from 2KB to 4KB ensures all chunks smaller than 4KB are copied into `currentView` (which uses a dedicated 4KB buffer outside the pool), while chunks 4KB or larger don't use the pool anyway. Thus no pooled buffers are ever exposed to ByteStream detachment. This adds 2KB memory per active stream, copies chunks in the 2-4KB range instead of passing them as views (small CPU cost), and buffers up to 2KB more data before flushing. However, it avoids duplicating large binary data (which copying everything would require, like the Edge entry point currently does in `typedArrayToBinaryChunk`). Related issues: - vercel/next.js#84753 - vercel/next.js#84858
1 parent c35f6a3 commit dc485c7

3 files changed

Lines changed: 47 additions & 13 deletions

File tree

‎packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010

1111
'use strict';
1212

13+
importfsfrom'fs';
14+
importosfrom'os';
15+
importpathfrom'path';
1316
import{patchSetImmediate}from'../../../../scripts/jest/patchSetImmediate';
1417

15-
global.ReadableStream=
16-
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
17-
1818
letclientExports;
1919
letwebpackMap;
2020
letwebpackModules;
@@ -1136,4 +1136,37 @@ describe('ReactFlightDOMNode', () => {
11361136
'Switched to client rendering because the server rendering errored:\n\nssr-throw',
11371137
);
11381138
});
1139+
1140+
// This is a regression test for a specific issue where byte Web Streams are
1141+
// detaching ArrayBuffers, which caused downstream issues (e.g. "Cannot
1142+
// perform Construct on a detached ArrayBuffer") for chunks that are using
1143+
// Node's internal Buffer pool.
1144+
it('should not corrupt the Node.js Buffer pool by detaching ArrayBuffers when using Web Streams',async()=>{
1145+
// Create a temp file smaller than 4KB to ensure it uses the Buffer pool.
1146+
constfile=path.join(os.tmpdir(),'test.bin');
1147+
fs.writeFileSync(file,Buffer.alloc(4095));
1148+
constfileChunk=fs.readFileSync(file);
1149+
fs.unlinkSync(file);
1150+
1151+
// Verify this chunk uses the Buffer pool (8192 bytes for files < 4KB).
1152+
expect(fileChunk.buffer.byteLength).toBe(8192);
1153+
1154+
constreadable=awaitserverAct(()=>
1155+
ReactServerDOMServer.renderToReadableStream(fileChunk,webpackMap),
1156+
);
1157+
1158+
// Create a Web Streams WritableStream that tries to use Buffer operations.
1159+
constwritable=newWritableStream({
1160+
write(chunk){
1161+
// Only write one byte to ensure Node.js is not creating a new Buffer
1162+
// pool. Typically, library code (e.g. a compression middleware) would
1163+
// call Buffer.from(chunk) or similar, instead of allocating a new
1164+
// Buffer directly. With that, the test file could only be ~2600 bytes.
1165+
Buffer.allocUnsafe(1);
1166+
},
1167+
});
1168+
1169+
// Must not throw an error.
1170+
awaitreadable.pipeTo(writable);
1171+
});
11391172
});

‎packages/react-server/src/ReactServerStreamConfigEdge.js‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ export function flushBuffered(destination: Destination) {
3737
// transform streams. https://github.com/whatwg/streams/issues/960
3838
}
3939

40-
constVIEW_SIZE=2048;
40+
// Chunks larger than VIEW_SIZE are written directly, without copying into the
41+
// internal view buffer. This must be at least half of Node's internal Buffer
42+
// pool size (8192) to avoid corrupting the pool when using
43+
// renderToReadableStream, which uses a byte stream that detaches ArrayBuffers.
44+
constVIEW_SIZE=4096;
4145
letcurrentView=null;
4246
letwrittenBytes=0;
4347

@@ -147,14 +151,7 @@ export function typedArrayToBinaryChunk(
147151
// If we passed through this straight to enqueue we wouldn't have to convert it but since
148152
// we need to copy the buffer in that case, we need to convert it to copy it.
149153
// When we copy it into another array using set() it needs to be a Uint8Array.
150-
constbuffer=newUint8Array(
151-
content.buffer,
152-
content.byteOffset,
153-
content.byteLength,
154-
);
155-
// We clone large chunks so that we can transfer them when we write them.
156-
// Others get copied into the target buffer.
157-
returncontent.byteLength>VIEW_SIZE ? buffer.slice() : buffer;
154+
returnnewUint8Array(content.buffer,content.byteOffset,content.byteLength);
158155
}
159156

160157
exportfunctionbyteLengthOfChunk(chunk: Chunk|PrecomputedChunk): number{

‎packages/react-server/src/ReactServerStreamConfigNode.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ export function flushBuffered(destination: Destination) {
3838
}
3939
}
4040

41-
constVIEW_SIZE=2048;
41+
// Chunks larger than VIEW_SIZE are written directly, without copying into the
42+
// internal view buffer. This must be at least half of Node's internal Buffer
43+
// pool size (8192) to avoid corrupting the pool when using
44+
// renderToReadableStream, which uses a byte stream that detaches ArrayBuffers.
45+
constVIEW_SIZE=4096;
4246
letcurrentView=null;
4347
letwrittenBytes=0;
4448
letdestinationHasCapacity=true;

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
[Flight] Fix detached `ArrayBuffer` error when streaming typed arrays… · react/react@dc485c7 · GitHub
Skip to content

Commit dc485c7

Browse files
authored
[Flight] Fix detached ArrayBuffer error when streaming typed arrays (#34849)
Using `renderToReadableStream` in Node.js with binary data from `fs.readFileSync` (or `Buffer.allocUnsafe`) could cause downstream consumers (like compression middleware) to fail with "Cannot perform Construct on a detached ArrayBuffer". The issue occurs because Node.js uses an 8192-byte Buffer pool for small allocations (< 4KB). When React's `VIEW_SIZE` was 2KB, files between ~2KB and 4KB would be passed through as views of pooled buffers rather than copied into `currentView`. ByteStreams (`type: 'bytes'`) detach ArrayBuffers during transfer, which corrupts the shared Buffer pool and causes subsequent Buffer operations to fail. Increasing `VIEW_SIZE` from 2KB to 4KB ensures all chunks smaller than 4KB are copied into `currentView` (which uses a dedicated 4KB buffer outside the pool), while chunks 4KB or larger don't use the pool anyway. Thus no pooled buffers are ever exposed to ByteStream detachment. This adds 2KB memory per active stream, copies chunks in the 2-4KB range instead of passing them as views (small CPU cost), and buffers up to 2KB more data before flushing. However, it avoids duplicating large binary data (which copying everything would require, like the Edge entry point currently does in `typedArrayToBinaryChunk`). Related issues: - vercel/next.js#84753 - vercel/next.js#84858
1 parent c35f6a3 commit dc485c7

3 files changed

Lines changed: 47 additions & 13 deletions

File tree

‎packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010

1111
'use strict';
1212

13+
importfsfrom'fs';
14+
importosfrom'os';
15+
importpathfrom'path';
1316
import{patchSetImmediate}from'../../../../scripts/jest/patchSetImmediate';
1417

15-
global.ReadableStream=
16-
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
17-
1818
letclientExports;
1919
letwebpackMap;
2020
letwebpackModules;
@@ -1136,4 +1136,37 @@ describe('ReactFlightDOMNode', () => {
11361136
'Switched to client rendering because the server rendering errored:\n\nssr-throw',
11371137
);
11381138
});
1139+
1140+
// This is a regression test for a specific issue where byte Web Streams are
1141+
// detaching ArrayBuffers, which caused downstream issues (e.g. "Cannot
1142+
// perform Construct on a detached ArrayBuffer") for chunks that are using
1143+
// Node's internal Buffer pool.
1144+
it('should not corrupt the Node.js Buffer pool by detaching ArrayBuffers when using Web Streams',async()=>{
1145+
// Create a temp file smaller than 4KB to ensure it uses the Buffer pool.
1146+
constfile=path.join(os.tmpdir(),'test.bin');
1147+
fs.writeFileSync(file,Buffer.alloc(4095));
1148+
constfileChunk=fs.readFileSync(file);
1149+
fs.unlinkSync(file);
1150+
1151+
// Verify this chunk uses the Buffer pool (8192 bytes for files < 4KB).
1152+
expect(fileChunk.buffer.byteLength).toBe(8192);
1153+
1154+
constreadable=awaitserverAct(()=>
1155+
ReactServerDOMServer.renderToReadableStream(fileChunk,webpackMap),
1156+
);
1157+
1158+
// Create a Web Streams WritableStream that tries to use Buffer operations.
1159+
constwritable=newWritableStream({
1160+
write(chunk){
1161+
// Only write one byte to ensure Node.js is not creating a new Buffer
1162+
// pool. Typically, library code (e.g. a compression middleware) would
1163+
// call Buffer.from(chunk) or similar, instead of allocating a new
1164+
// Buffer directly. With that, the test file could only be ~2600 bytes.
1165+
Buffer.allocUnsafe(1);
1166+
},
1167+
});
1168+
1169+
// Must not throw an error.
1170+
awaitreadable.pipeTo(writable);
1171+
});
11391172
});

‎packages/react-server/src/ReactServerStreamConfigEdge.js‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ export function flushBuffered(destination: Destination) {
3737
// transform streams. https://github.com/whatwg/streams/issues/960
3838
}
3939

40-
constVIEW_SIZE=2048;
40+
// Chunks larger than VIEW_SIZE are written directly, without copying into the
41+
// internal view buffer. This must be at least half of Node's internal Buffer
42+
// pool size (8192) to avoid corrupting the pool when using
43+
// renderToReadableStream, which uses a byte stream that detaches ArrayBuffers.
44+
constVIEW_SIZE=4096;
4145
letcurrentView=null;
4246
letwrittenBytes=0;
4347

@@ -147,14 +151,7 @@ export function typedArrayToBinaryChunk(
147151
// If we passed through this straight to enqueue we wouldn't have to convert it but since
148152
// we need to copy the buffer in that case, we need to convert it to copy it.
149153
// When we copy it into another array using set() it needs to be a Uint8Array.
150-
constbuffer=newUint8Array(
151-
content.buffer,
152-
content.byteOffset,
153-
content.byteLength,
154-
);
155-
// We clone large chunks so that we can transfer them when we write them.
156-
// Others get copied into the target buffer.
157-
returncontent.byteLength>VIEW_SIZE ? buffer.slice() : buffer;
154+
returnnewUint8Array(content.buffer,content.byteOffset,content.byteLength);
158155
}
159156

160157
exportfunctionbyteLengthOfChunk(chunk: Chunk|PrecomputedChunk): number{

‎packages/react-server/src/ReactServerStreamConfigNode.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ export function flushBuffered(destination: Destination) {
3838
}
3939
}
4040

41-
constVIEW_SIZE=2048;
41+
// Chunks larger than VIEW_SIZE are written directly, without copying into the
42+
// internal view buffer. This must be at least half of Node's internal Buffer
43+
// pool size (8192) to avoid corrupting the pool when using
44+
// renderToReadableStream, which uses a byte stream that detaches ArrayBuffers.
45+
constVIEW_SIZE=4096;
4246
letcurrentView=null;
4347
letwrittenBytes=0;
4448
letdestinationHasCapacity=true;

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [Flight] Fix detached `ArrayBuffer` error when streaming typed arrays… · react/react@dc485c7 · GitHub
Skip to content

Commit dc485c7

Browse files
authored
[Flight] Fix detached ArrayBuffer error when streaming typed arrays (#34849)
Using `renderToReadableStream` in Node.js with binary data from `fs.readFileSync` (or `Buffer.allocUnsafe`) could cause downstream consumers (like compression middleware) to fail with "Cannot perform Construct on a detached ArrayBuffer". The issue occurs because Node.js uses an 8192-byte Buffer pool for small allocations (< 4KB). When React's `VIEW_SIZE` was 2KB, files between ~2KB and 4KB would be passed through as views of pooled buffers rather than copied into `currentView`. ByteStreams (`type: 'bytes'`) detach ArrayBuffers during transfer, which corrupts the shared Buffer pool and causes subsequent Buffer operations to fail. Increasing `VIEW_SIZE` from 2KB to 4KB ensures all chunks smaller than 4KB are copied into `currentView` (which uses a dedicated 4KB buffer outside the pool), while chunks 4KB or larger don't use the pool anyway. Thus no pooled buffers are ever exposed to ByteStream detachment. This adds 2KB memory per active stream, copies chunks in the 2-4KB range instead of passing them as views (small CPU cost), and buffers up to 2KB more data before flushing. However, it avoids duplicating large binary data (which copying everything would require, like the Edge entry point currently does in `typedArrayToBinaryChunk`). Related issues: - vercel/next.js#84753 - vercel/next.js#84858
1 parent c35f6a3 commit dc485c7

3 files changed

Lines changed: 47 additions & 13 deletions

File tree

‎packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010

1111
'use strict';
1212

13+
importfsfrom'fs';
14+
importosfrom'os';
15+
importpathfrom'path';
1316
import{patchSetImmediate}from'../../../../scripts/jest/patchSetImmediate';
1417

15-
global.ReadableStream=
16-
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
17-
1818
letclientExports;
1919
letwebpackMap;
2020
letwebpackModules;
@@ -1136,4 +1136,37 @@ describe('ReactFlightDOMNode', () => {
11361136
'Switched to client rendering because the server rendering errored:\n\nssr-throw',
11371137
);
11381138
});
1139+
1140+
// This is a regression test for a specific issue where byte Web Streams are
1141+
// detaching ArrayBuffers, which caused downstream issues (e.g. "Cannot
1142+
// perform Construct on a detached ArrayBuffer") for chunks that are using
1143+
// Node's internal Buffer pool.
1144+
it('should not corrupt the Node.js Buffer pool by detaching ArrayBuffers when using Web Streams',async()=>{
1145+
// Create a temp file smaller than 4KB to ensure it uses the Buffer pool.
1146+
constfile=path.join(os.tmpdir(),'test.bin');
1147+
fs.writeFileSync(file,Buffer.alloc(4095));
1148+
constfileChunk=fs.readFileSync(file);
1149+
fs.unlinkSync(file);
1150+
1151+
// Verify this chunk uses the Buffer pool (8192 bytes for files < 4KB).
1152+
expect(fileChunk.buffer.byteLength).toBe(8192);
1153+
1154+
constreadable=awaitserverAct(()=>
1155+
ReactServerDOMServer.renderToReadableStream(fileChunk,webpackMap),
1156+
);
1157+
1158+
// Create a Web Streams WritableStream that tries to use Buffer operations.
1159+
constwritable=newWritableStream({
1160+
write(chunk){
1161+
// Only write one byte to ensure Node.js is not creating a new Buffer
1162+
// pool. Typically, library code (e.g. a compression middleware) would
1163+
// call Buffer.from(chunk) or similar, instead of allocating a new
1164+
// Buffer directly. With that, the test file could only be ~2600 bytes.
1165+
Buffer.allocUnsafe(1);
1166+
},
1167+
});
1168+
1169+
// Must not throw an error.
1170+
awaitreadable.pipeTo(writable);
1171+
});
11391172
});

‎packages/react-server/src/ReactServerStreamConfigEdge.js‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ export function flushBuffered(destination: Destination) {
3737
// transform streams. https://github.com/whatwg/streams/issues/960
3838
}
3939

40-
constVIEW_SIZE=2048;
40+
// Chunks larger than VIEW_SIZE are written directly, without copying into the
41+
// internal view buffer. This must be at least half of Node's internal Buffer
42+
// pool size (8192) to avoid corrupting the pool when using
43+
// renderToReadableStream, which uses a byte stream that detaches ArrayBuffers.
44+
constVIEW_SIZE=4096;
4145
letcurrentView=null;
4246
letwrittenBytes=0;
4347

@@ -147,14 +151,7 @@ export function typedArrayToBinaryChunk(
147151
// If we passed through this straight to enqueue we wouldn't have to convert it but since
148152
// we need to copy the buffer in that case, we need to convert it to copy it.
149153
// When we copy it into another array using set() it needs to be a Uint8Array.
150-
constbuffer=newUint8Array(
151-
content.buffer,
152-
content.byteOffset,
153-
content.byteLength,
154-
);
155-
// We clone large chunks so that we can transfer them when we write them.
156-
// Others get copied into the target buffer.
157-
returncontent.byteLength>VIEW_SIZE ? buffer.slice() : buffer;
154+
returnnewUint8Array(content.buffer,content.byteOffset,content.byteLength);
158155
}
159156

160157
exportfunctionbyteLengthOfChunk(chunk: Chunk|PrecomputedChunk): number{

‎packages/react-server/src/ReactServerStreamConfigNode.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ export function flushBuffered(destination: Destination) {
3838
}
3939
}
4040

41-
constVIEW_SIZE=2048;
41+
// Chunks larger than VIEW_SIZE are written directly, without copying into the
42+
// internal view buffer. This must be at least half of Node's internal Buffer
43+
// pool size (8192) to avoid corrupting the pool when using
44+
// renderToReadableStream, which uses a byte stream that detaches ArrayBuffers.
45+
constVIEW_SIZE=4096;
4246
letcurrentView=null;
4347
letwrittenBytes=0;
4448
letdestinationHasCapacity=true;

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [Flight] Fix detached `ArrayBuffer` error when streaming typed arrays… · react/react@dc485c7 · GitHub
Skip to content

Commit dc485c7

Browse files
authored
[Flight] Fix detached ArrayBuffer error when streaming typed arrays (#34849)
Using `renderToReadableStream` in Node.js with binary data from `fs.readFileSync` (or `Buffer.allocUnsafe`) could cause downstream consumers (like compression middleware) to fail with "Cannot perform Construct on a detached ArrayBuffer". The issue occurs because Node.js uses an 8192-byte Buffer pool for small allocations (< 4KB). When React's `VIEW_SIZE` was 2KB, files between ~2KB and 4KB would be passed through as views of pooled buffers rather than copied into `currentView`. ByteStreams (`type: 'bytes'`) detach ArrayBuffers during transfer, which corrupts the shared Buffer pool and causes subsequent Buffer operations to fail. Increasing `VIEW_SIZE` from 2KB to 4KB ensures all chunks smaller than 4KB are copied into `currentView` (which uses a dedicated 4KB buffer outside the pool), while chunks 4KB or larger don't use the pool anyway. Thus no pooled buffers are ever exposed to ByteStream detachment. This adds 2KB memory per active stream, copies chunks in the 2-4KB range instead of passing them as views (small CPU cost), and buffers up to 2KB more data before flushing. However, it avoids duplicating large binary data (which copying everything would require, like the Edge entry point currently does in `typedArrayToBinaryChunk`). Related issues: - vercel/next.js#84753 - vercel/next.js#84858
1 parent c35f6a3 commit dc485c7

3 files changed

Lines changed: 47 additions & 13 deletions

File tree

‎packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010

1111
'use strict';
1212

13+
importfsfrom'fs';
14+
importosfrom'os';
15+
importpathfrom'path';
1316
import{patchSetImmediate}from'../../../../scripts/jest/patchSetImmediate';
1417

15-
global.ReadableStream=
16-
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
17-
1818
letclientExports;
1919
letwebpackMap;
2020
letwebpackModules;
@@ -1136,4 +1136,37 @@ describe('ReactFlightDOMNode', () => {
11361136
'Switched to client rendering because the server rendering errored:\n\nssr-throw',
11371137
);
11381138
});
1139+
1140+
// This is a regression test for a specific issue where byte Web Streams are
1141+
// detaching ArrayBuffers, which caused downstream issues (e.g. "Cannot
1142+
// perform Construct on a detached ArrayBuffer") for chunks that are using
1143+
// Node's internal Buffer pool.
1144+
it('should not corrupt the Node.js Buffer pool by detaching ArrayBuffers when using Web Streams',async()=>{
1145+
// Create a temp file smaller than 4KB to ensure it uses the Buffer pool.
1146+
constfile=path.join(os.tmpdir(),'test.bin');
1147+
fs.writeFileSync(file,Buffer.alloc(4095));
1148+
constfileChunk=fs.readFileSync(file);
1149+
fs.unlinkSync(file);
1150+
1151+
// Verify this chunk uses the Buffer pool (8192 bytes for files < 4KB).
1152+
expect(fileChunk.buffer.byteLength).toBe(8192);
1153+
1154+
constreadable=awaitserverAct(()=>
1155+
ReactServerDOMServer.renderToReadableStream(fileChunk,webpackMap),
1156+
);
1157+
1158+
// Create a Web Streams WritableStream that tries to use Buffer operations.
1159+
constwritable=newWritableStream({
1160+
write(chunk){
1161+
// Only write one byte to ensure Node.js is not creating a new Buffer
1162+
// pool. Typically, library code (e.g. a compression middleware) would
1163+
// call Buffer.from(chunk) or similar, instead of allocating a new
1164+
// Buffer directly. With that, the test file could only be ~2600 bytes.
1165+
Buffer.allocUnsafe(1);
1166+
},
1167+
});
1168+
1169+
// Must not throw an error.
1170+
awaitreadable.pipeTo(writable);
1171+
});
11391172
});

‎packages/react-server/src/ReactServerStreamConfigEdge.js‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ export function flushBuffered(destination: Destination) {
3737
// transform streams. https://github.com/whatwg/streams/issues/960
3838
}
3939

40-
constVIEW_SIZE=2048;
40+
// Chunks larger than VIEW_SIZE are written directly, without copying into the
41+
// internal view buffer. This must be at least half of Node's internal Buffer
42+
// pool size (8192) to avoid corrupting the pool when using
43+
// renderToReadableStream, which uses a byte stream that detaches ArrayBuffers.
44+
constVIEW_SIZE=4096;
4145
letcurrentView=null;
4246
letwrittenBytes=0;
4347

@@ -147,14 +151,7 @@ export function typedArrayToBinaryChunk(
147151
// If we passed through this straight to enqueue we wouldn't have to convert it but since
148152
// we need to copy the buffer in that case, we need to convert it to copy it.
149153
// When we copy it into another array using set() it needs to be a Uint8Array.
150-
constbuffer=newUint8Array(
151-
content.buffer,
152-
content.byteOffset,
153-
content.byteLength,
154-
);
155-
// We clone large chunks so that we can transfer them when we write them.
156-
// Others get copied into the target buffer.
157-
returncontent.byteLength>VIEW_SIZE ? buffer.slice() : buffer;
154+
returnnewUint8Array(content.buffer,content.byteOffset,content.byteLength);
158155
}
159156

160157
exportfunctionbyteLengthOfChunk(chunk: Chunk|PrecomputedChunk): number{

‎packages/react-server/src/ReactServerStreamConfigNode.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ export function flushBuffered(destination: Destination) {
3838
}
3939
}
4040

41-
constVIEW_SIZE=2048;
41+
// Chunks larger than VIEW_SIZE are written directly, without copying into the
42+
// internal view buffer. This must be at least half of Node's internal Buffer
43+
// pool size (8192) to avoid corrupting the pool when using
44+
// renderToReadableStream, which uses a byte stream that detaches ArrayBuffers.
45+
constVIEW_SIZE=4096;
4246
letcurrentView=null;
4347
letwrittenBytes=0;
4448
letdestinationHasCapacity=true;

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' [Flight] Fix detached `ArrayBuffer` error when streaming typed arrays… · react/react@dc485c7 · GitHub
Skip to content

Commit dc485c7

Browse files
authored
[Flight] Fix detached ArrayBuffer error when streaming typed arrays (#34849)
Using `renderToReadableStream` in Node.js with binary data from `fs.readFileSync` (or `Buffer.allocUnsafe`) could cause downstream consumers (like compression middleware) to fail with "Cannot perform Construct on a detached ArrayBuffer". The issue occurs because Node.js uses an 8192-byte Buffer pool for small allocations (< 4KB). When React's `VIEW_SIZE` was 2KB, files between ~2KB and 4KB would be passed through as views of pooled buffers rather than copied into `currentView`. ByteStreams (`type: 'bytes'`) detach ArrayBuffers during transfer, which corrupts the shared Buffer pool and causes subsequent Buffer operations to fail. Increasing `VIEW_SIZE` from 2KB to 4KB ensures all chunks smaller than 4KB are copied into `currentView` (which uses a dedicated 4KB buffer outside the pool), while chunks 4KB or larger don't use the pool anyway. Thus no pooled buffers are ever exposed to ByteStream detachment. This adds 2KB memory per active stream, copies chunks in the 2-4KB range instead of passing them as views (small CPU cost), and buffers up to 2KB more data before flushing. However, it avoids duplicating large binary data (which copying everything would require, like the Edge entry point currently does in `typedArrayToBinaryChunk`). Related issues: - vercel/next.js#84753 - vercel/next.js#84858
1 parent c35f6a3 commit dc485c7

3 files changed

Lines changed: 47 additions & 13 deletions

File tree

‎packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010

1111
'use strict';
1212

13+
importfsfrom'fs';
14+
importosfrom'os';
15+
importpathfrom'path';
1316
import{patchSetImmediate}from'../../../../scripts/jest/patchSetImmediate';
1417

15-
global.ReadableStream=
16-
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
17-
1818
letclientExports;
1919
letwebpackMap;
2020
letwebpackModules;
@@ -1136,4 +1136,37 @@ describe('ReactFlightDOMNode', () => {
11361136
'Switched to client rendering because the server rendering errored:\n\nssr-throw',
11371137
);
11381138
});
1139+
1140+
// This is a regression test for a specific issue where byte Web Streams are
1141+
// detaching ArrayBuffers, which caused downstream issues (e.g. "Cannot
1142+
// perform Construct on a detached ArrayBuffer") for chunks that are using
1143+
// Node's internal Buffer pool.
1144+
it('should not corrupt the Node.js Buffer pool by detaching ArrayBuffers when using Web Streams',async()=>{
1145+
// Create a temp file smaller than 4KB to ensure it uses the Buffer pool.
1146+
constfile=path.join(os.tmpdir(),'test.bin');
1147+
fs.writeFileSync(file,Buffer.alloc(4095));
1148+
constfileChunk=fs.readFileSync(file);
1149+
fs.unlinkSync(file);
1150+
1151+
// Verify this chunk uses the Buffer pool (8192 bytes for files < 4KB).
1152+
expect(fileChunk.buffer.byteLength).toBe(8192);
1153+
1154+
constreadable=awaitserverAct(()=>
1155+
ReactServerDOMServer.renderToReadableStream(fileChunk,webpackMap),
1156+
);
1157+
1158+
// Create a Web Streams WritableStream that tries to use Buffer operations.
1159+
constwritable=newWritableStream({
1160+
write(chunk){
1161+
// Only write one byte to ensure Node.js is not creating a new Buffer
1162+
// pool. Typically, library code (e.g. a compression middleware) would
1163+
// call Buffer.from(chunk) or similar, instead of allocating a new
1164+
// Buffer directly. With that, the test file could only be ~2600 bytes.
1165+
Buffer.allocUnsafe(1);
1166+
},
1167+
});
1168+
1169+
// Must not throw an error.
1170+
awaitreadable.pipeTo(writable);
1171+
});
11391172
});

‎packages/react-server/src/ReactServerStreamConfigEdge.js‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ export function flushBuffered(destination: Destination) {
3737
// transform streams. https://github.com/whatwg/streams/issues/960
3838
}
3939

40-
constVIEW_SIZE=2048;
40+
// Chunks larger than VIEW_SIZE are written directly, without copying into the
41+
// internal view buffer. This must be at least half of Node's internal Buffer
42+
// pool size (8192) to avoid corrupting the pool when using
43+
// renderToReadableStream, which uses a byte stream that detaches ArrayBuffers.
44+
constVIEW_SIZE=4096;
4145
letcurrentView=null;
4246
letwrittenBytes=0;
4347

@@ -147,14 +151,7 @@ export function typedArrayToBinaryChunk(
147151
// If we passed through this straight to enqueue we wouldn't have to convert it but since
148152
// we need to copy the buffer in that case, we need to convert it to copy it.
149153
// When we copy it into another array using set() it needs to be a Uint8Array.
150-
constbuffer=newUint8Array(
151-
content.buffer,
152-
content.byteOffset,
153-
content.byteLength,
154-
);
155-
// We clone large chunks so that we can transfer them when we write them.
156-
// Others get copied into the target buffer.
157-
returncontent.byteLength>VIEW_SIZE ? buffer.slice() : buffer;
154+
returnnewUint8Array(content.buffer,content.byteOffset,content.byteLength);
158155
}
159156

160157
exportfunctionbyteLengthOfChunk(chunk: Chunk|PrecomputedChunk): number{

‎packages/react-server/src/ReactServerStreamConfigNode.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ export function flushBuffered(destination: Destination) {
3838
}
3939
}
4040

41-
constVIEW_SIZE=2048;
41+
// Chunks larger than VIEW_SIZE are written directly, without copying into the
42+
// internal view buffer. This must be at least half of Node's internal Buffer
43+
// pool size (8192) to avoid corrupting the pool when using
44+
// renderToReadableStream, which uses a byte stream that detaches ArrayBuffers.
45+
constVIEW_SIZE=4096;
4246
letcurrentView=null;
4347
letwrittenBytes=0;
4448
letdestinationHasCapacity=true;

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [Flight] Fix detached `ArrayBuffer` error when streaming typed arrays… · react/react@dc485c7 · GitHub
Skip to content

Commit dc485c7

Browse files
authored
[Flight] Fix detached ArrayBuffer error when streaming typed arrays (#34849)
Using `renderToReadableStream` in Node.js with binary data from `fs.readFileSync` (or `Buffer.allocUnsafe`) could cause downstream consumers (like compression middleware) to fail with "Cannot perform Construct on a detached ArrayBuffer". The issue occurs because Node.js uses an 8192-byte Buffer pool for small allocations (< 4KB). When React's `VIEW_SIZE` was 2KB, files between ~2KB and 4KB would be passed through as views of pooled buffers rather than copied into `currentView`. ByteStreams (`type: 'bytes'`) detach ArrayBuffers during transfer, which corrupts the shared Buffer pool and causes subsequent Buffer operations to fail. Increasing `VIEW_SIZE` from 2KB to 4KB ensures all chunks smaller than 4KB are copied into `currentView` (which uses a dedicated 4KB buffer outside the pool), while chunks 4KB or larger don't use the pool anyway. Thus no pooled buffers are ever exposed to ByteStream detachment. This adds 2KB memory per active stream, copies chunks in the 2-4KB range instead of passing them as views (small CPU cost), and buffers up to 2KB more data before flushing. However, it avoids duplicating large binary data (which copying everything would require, like the Edge entry point currently does in `typedArrayToBinaryChunk`). Related issues: - vercel/next.js#84753 - vercel/next.js#84858
1 parent c35f6a3 commit dc485c7

3 files changed

Lines changed: 47 additions & 13 deletions

File tree

‎packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010

1111
'use strict';
1212

13+
importfsfrom'fs';
14+
importosfrom'os';
15+
importpathfrom'path';
1316
import{patchSetImmediate}from'../../../../scripts/jest/patchSetImmediate';
1417

15-
global.ReadableStream=
16-
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
17-
1818
letclientExports;
1919
letwebpackMap;
2020
letwebpackModules;
@@ -1136,4 +1136,37 @@ describe('ReactFlightDOMNode', () => {
11361136
'Switched to client rendering because the server rendering errored:\n\nssr-throw',
11371137
);
11381138
});
1139+
1140+
// This is a regression test for a specific issue where byte Web Streams are
1141+
// detaching ArrayBuffers, which caused downstream issues (e.g. "Cannot
1142+
// perform Construct on a detached ArrayBuffer") for chunks that are using
1143+
// Node's internal Buffer pool.
1144+
it('should not corrupt the Node.js Buffer pool by detaching ArrayBuffers when using Web Streams',async()=>{
1145+
// Create a temp file smaller than 4KB to ensure it uses the Buffer pool.
1146+
constfile=path.join(os.tmpdir(),'test.bin');
1147+
fs.writeFileSync(file,Buffer.alloc(4095));
1148+
constfileChunk=fs.readFileSync(file);
1149+
fs.unlinkSync(file);
1150+
1151+
// Verify this chunk uses the Buffer pool (8192 bytes for files < 4KB).
1152+
expect(fileChunk.buffer.byteLength).toBe(8192);
1153+
1154+
constreadable=awaitserverAct(()=>
1155+
ReactServerDOMServer.renderToReadableStream(fileChunk,webpackMap),
1156+
);
1157+
1158+
// Create a Web Streams WritableStream that tries to use Buffer operations.
1159+
constwritable=newWritableStream({
1160+
write(chunk){
1161+
// Only write one byte to ensure Node.js is not creating a new Buffer
1162+
// pool. Typically, library code (e.g. a compression middleware) would
1163+
// call Buffer.from(chunk) or similar, instead of allocating a new
1164+
// Buffer directly. With that, the test file could only be ~2600 bytes.
1165+
Buffer.allocUnsafe(1);
1166+
},
1167+
});
1168+
1169+
// Must not throw an error.
1170+
awaitreadable.pipeTo(writable);
1171+
});
11391172
});

‎packages/react-server/src/ReactServerStreamConfigEdge.js‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ export function flushBuffered(destination: Destination) {
3737
// transform streams. https://github.com/whatwg/streams/issues/960
3838
}
3939

40-
constVIEW_SIZE=2048;
40+
// Chunks larger than VIEW_SIZE are written directly, without copying into the
41+
// internal view buffer. This must be at least half of Node's internal Buffer
42+
// pool size (8192) to avoid corrupting the pool when using
43+
// renderToReadableStream, which uses a byte stream that detaches ArrayBuffers.
44+
constVIEW_SIZE=4096;
4145
letcurrentView=null;
4246
letwrittenBytes=0;
4347

@@ -147,14 +151,7 @@ export function typedArrayToBinaryChunk(
147151
// If we passed through this straight to enqueue we wouldn't have to convert it but since
148152
// we need to copy the buffer in that case, we need to convert it to copy it.
149153
// When we copy it into another array using set() it needs to be a Uint8Array.
150-
constbuffer=newUint8Array(
151-
content.buffer,
152-
content.byteOffset,
153-
content.byteLength,
154-
);
155-
// We clone large chunks so that we can transfer them when we write them.
156-
// Others get copied into the target buffer.
157-
returncontent.byteLength>VIEW_SIZE ? buffer.slice() : buffer;
154+
returnnewUint8Array(content.buffer,content.byteOffset,content.byteLength);
158155
}
159156

160157
exportfunctionbyteLengthOfChunk(chunk: Chunk|PrecomputedChunk): number{

‎packages/react-server/src/ReactServerStreamConfigNode.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ export function flushBuffered(destination: Destination) {
3838
}
3939
}
4040

41-
constVIEW_SIZE=2048;
41+
// Chunks larger than VIEW_SIZE are written directly, without copying into the
42+
// internal view buffer. This must be at least half of Node's internal Buffer
43+
// pool size (8192) to avoid corrupting the pool when using
44+
// renderToReadableStream, which uses a byte stream that detaches ArrayBuffers.
45+
constVIEW_SIZE=4096;
4246
letcurrentView=null;
4347
letwrittenBytes=0;
4448
letdestinationHasCapacity=true;

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [Flight] Fix detached `ArrayBuffer` error when streaming typed arrays… · react/react@dc485c7 · GitHub
Skip to content

Commit dc485c7

Browse files
authored
[Flight] Fix detached ArrayBuffer error when streaming typed arrays (#34849)
Using `renderToReadableStream` in Node.js with binary data from `fs.readFileSync` (or `Buffer.allocUnsafe`) could cause downstream consumers (like compression middleware) to fail with "Cannot perform Construct on a detached ArrayBuffer". The issue occurs because Node.js uses an 8192-byte Buffer pool for small allocations (< 4KB). When React's `VIEW_SIZE` was 2KB, files between ~2KB and 4KB would be passed through as views of pooled buffers rather than copied into `currentView`. ByteStreams (`type: 'bytes'`) detach ArrayBuffers during transfer, which corrupts the shared Buffer pool and causes subsequent Buffer operations to fail. Increasing `VIEW_SIZE` from 2KB to 4KB ensures all chunks smaller than 4KB are copied into `currentView` (which uses a dedicated 4KB buffer outside the pool), while chunks 4KB or larger don't use the pool anyway. Thus no pooled buffers are ever exposed to ByteStream detachment. This adds 2KB memory per active stream, copies chunks in the 2-4KB range instead of passing them as views (small CPU cost), and buffers up to 2KB more data before flushing. However, it avoids duplicating large binary data (which copying everything would require, like the Edge entry point currently does in `typedArrayToBinaryChunk`). Related issues: - vercel/next.js#84753 - vercel/next.js#84858
1 parent c35f6a3 commit dc485c7

3 files changed

Lines changed: 47 additions & 13 deletions

File tree

‎packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010

1111
'use strict';
1212

13+
importfsfrom'fs';
14+
importosfrom'os';
15+
importpathfrom'path';
1316
import{patchSetImmediate}from'../../../../scripts/jest/patchSetImmediate';
1417

15-
global.ReadableStream=
16-
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
17-
1818
letclientExports;
1919
letwebpackMap;
2020
letwebpackModules;
@@ -1136,4 +1136,37 @@ describe('ReactFlightDOMNode', () => {
11361136
'Switched to client rendering because the server rendering errored:\n\nssr-throw',
11371137
);
11381138
});
1139+
1140+
// This is a regression test for a specific issue where byte Web Streams are
1141+
// detaching ArrayBuffers, which caused downstream issues (e.g. "Cannot
1142+
// perform Construct on a detached ArrayBuffer") for chunks that are using
1143+
// Node's internal Buffer pool.
1144+
it('should not corrupt the Node.js Buffer pool by detaching ArrayBuffers when using Web Streams',async()=>{
1145+
// Create a temp file smaller than 4KB to ensure it uses the Buffer pool.
1146+
constfile=path.join(os.tmpdir(),'test.bin');
1147+
fs.writeFileSync(file,Buffer.alloc(4095));
1148+
constfileChunk=fs.readFileSync(file);
1149+
fs.unlinkSync(file);
1150+
1151+
// Verify this chunk uses the Buffer pool (8192 bytes for files < 4KB).
1152+
expect(fileChunk.buffer.byteLength).toBe(8192);
1153+
1154+
constreadable=awaitserverAct(()=>
1155+
ReactServerDOMServer.renderToReadableStream(fileChunk,webpackMap),
1156+
);
1157+
1158+
// Create a Web Streams WritableStream that tries to use Buffer operations.
1159+
constwritable=newWritableStream({
1160+
write(chunk){
1161+
// Only write one byte to ensure Node.js is not creating a new Buffer
1162+
// pool. Typically, library code (e.g. a compression middleware) would
1163+
// call Buffer.from(chunk) or similar, instead of allocating a new
1164+
// Buffer directly. With that, the test file could only be ~2600 bytes.
1165+
Buffer.allocUnsafe(1);
1166+
},
1167+
});
1168+
1169+
// Must not throw an error.
1170+
awaitreadable.pipeTo(writable);
1171+
});
11391172
});

‎packages/react-server/src/ReactServerStreamConfigEdge.js‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ export function flushBuffered(destination: Destination) {
3737
// transform streams. https://github.com/whatwg/streams/issues/960
3838
}
3939

40-
constVIEW_SIZE=2048;
40+
// Chunks larger than VIEW_SIZE are written directly, without copying into the
41+
// internal view buffer. This must be at least half of Node's internal Buffer
42+
// pool size (8192) to avoid corrupting the pool when using
43+
// renderToReadableStream, which uses a byte stream that detaches ArrayBuffers.
44+
constVIEW_SIZE=4096;
4145
letcurrentView=null;
4246
letwrittenBytes=0;
4347

@@ -147,14 +151,7 @@ export function typedArrayToBinaryChunk(
147151
// If we passed through this straight to enqueue we wouldn't have to convert it but since
148152
// we need to copy the buffer in that case, we need to convert it to copy it.
149153
// When we copy it into another array using set() it needs to be a Uint8Array.
150-
constbuffer=newUint8Array(
151-
content.buffer,
152-
content.byteOffset,
153-
content.byteLength,
154-
);
155-
// We clone large chunks so that we can transfer them when we write them.
156-
// Others get copied into the target buffer.
157-
returncontent.byteLength>VIEW_SIZE ? buffer.slice() : buffer;
154+
returnnewUint8Array(content.buffer,content.byteOffset,content.byteLength);
158155
}
159156

160157
exportfunctionbyteLengthOfChunk(chunk: Chunk|PrecomputedChunk): number{

‎packages/react-server/src/ReactServerStreamConfigNode.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ export function flushBuffered(destination: Destination) {
3838
}
3939
}
4040

41-
constVIEW_SIZE=2048;
41+
// Chunks larger than VIEW_SIZE are written directly, without copying into the
42+
// internal view buffer. This must be at least half of Node's internal Buffer
43+
// pool size (8192) to avoid corrupting the pool when using
44+
// renderToReadableStream, which uses a byte stream that detaches ArrayBuffers.
45+
constVIEW_SIZE=4096;
4246
letcurrentView=null;
4347
letwrittenBytes=0;
4448
letdestinationHasCapacity=true;

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); [Flight] Fix detached `ArrayBuffer` error when streaming typed arrays… · react/react@dc485c7 · GitHub
Skip to content

Commit dc485c7

Browse files
authored
[Flight] Fix detached ArrayBuffer error when streaming typed arrays (#34849)
Using `renderToReadableStream` in Node.js with binary data from `fs.readFileSync` (or `Buffer.allocUnsafe`) could cause downstream consumers (like compression middleware) to fail with "Cannot perform Construct on a detached ArrayBuffer". The issue occurs because Node.js uses an 8192-byte Buffer pool for small allocations (< 4KB). When React's `VIEW_SIZE` was 2KB, files between ~2KB and 4KB would be passed through as views of pooled buffers rather than copied into `currentView`. ByteStreams (`type: 'bytes'`) detach ArrayBuffers during transfer, which corrupts the shared Buffer pool and causes subsequent Buffer operations to fail. Increasing `VIEW_SIZE` from 2KB to 4KB ensures all chunks smaller than 4KB are copied into `currentView` (which uses a dedicated 4KB buffer outside the pool), while chunks 4KB or larger don't use the pool anyway. Thus no pooled buffers are ever exposed to ByteStream detachment. This adds 2KB memory per active stream, copies chunks in the 2-4KB range instead of passing them as views (small CPU cost), and buffers up to 2KB more data before flushing. However, it avoids duplicating large binary data (which copying everything would require, like the Edge entry point currently does in `typedArrayToBinaryChunk`). Related issues: - vercel/next.js#84753 - vercel/next.js#84858
1 parent c35f6a3 commit dc485c7

3 files changed

Lines changed: 47 additions & 13 deletions

File tree

‎packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010

1111
'use strict';
1212

13+
importfsfrom'fs';
14+
importosfrom'os';
15+
importpathfrom'path';
1316
import{patchSetImmediate}from'../../../../scripts/jest/patchSetImmediate';
1417

15-
global.ReadableStream=
16-
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
17-
1818
letclientExports;
1919
letwebpackMap;
2020
letwebpackModules;
@@ -1136,4 +1136,37 @@ describe('ReactFlightDOMNode', () => {
11361136
'Switched to client rendering because the server rendering errored:\n\nssr-throw',
11371137
);
11381138
});
1139+
1140+
// This is a regression test for a specific issue where byte Web Streams are
1141+
// detaching ArrayBuffers, which caused downstream issues (e.g. "Cannot
1142+
// perform Construct on a detached ArrayBuffer") for chunks that are using
1143+
// Node's internal Buffer pool.
1144+
it('should not corrupt the Node.js Buffer pool by detaching ArrayBuffers when using Web Streams',async()=>{
1145+
// Create a temp file smaller than 4KB to ensure it uses the Buffer pool.
1146+
constfile=path.join(os.tmpdir(),'test.bin');
1147+
fs.writeFileSync(file,Buffer.alloc(4095));
1148+
constfileChunk=fs.readFileSync(file);
1149+
fs.unlinkSync(file);
1150+
1151+
// Verify this chunk uses the Buffer pool (8192 bytes for files < 4KB).
1152+
expect(fileChunk.buffer.byteLength).toBe(8192);
1153+
1154+
constreadable=awaitserverAct(()=>
1155+
ReactServerDOMServer.renderToReadableStream(fileChunk,webpackMap),
1156+
);
1157+
1158+
// Create a Web Streams WritableStream that tries to use Buffer operations.
1159+
constwritable=newWritableStream({
1160+
write(chunk){
1161+
// Only write one byte to ensure Node.js is not creating a new Buffer
1162+
// pool. Typically, library code (e.g. a compression middleware) would
1163+
// call Buffer.from(chunk) or similar, instead of allocating a new
1164+
// Buffer directly. With that, the test file could only be ~2600 bytes.
1165+
Buffer.allocUnsafe(1);
1166+
},
1167+
});
1168+
1169+
// Must not throw an error.
1170+
awaitreadable.pipeTo(writable);
1171+
});
11391172
});

‎packages/react-server/src/ReactServerStreamConfigEdge.js‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ export function flushBuffered(destination: Destination) {
3737
// transform streams. https://github.com/whatwg/streams/issues/960
3838
}
3939

40-
constVIEW_SIZE=2048;
40+
// Chunks larger than VIEW_SIZE are written directly, without copying into the
41+
// internal view buffer. This must be at least half of Node's internal Buffer
42+
// pool size (8192) to avoid corrupting the pool when using
43+
// renderToReadableStream, which uses a byte stream that detaches ArrayBuffers.
44+
constVIEW_SIZE=4096;
4145
letcurrentView=null;
4246
letwrittenBytes=0;
4347

@@ -147,14 +151,7 @@ export function typedArrayToBinaryChunk(
147151
// If we passed through this straight to enqueue we wouldn't have to convert it but since
148152
// we need to copy the buffer in that case, we need to convert it to copy it.
149153
// When we copy it into another array using set() it needs to be a Uint8Array.
150-
constbuffer=newUint8Array(
151-
content.buffer,
152-
content.byteOffset,
153-
content.byteLength,
154-
);
155-
// We clone large chunks so that we can transfer them when we write them.
156-
// Others get copied into the target buffer.
157-
returncontent.byteLength>VIEW_SIZE ? buffer.slice() : buffer;
154+
returnnewUint8Array(content.buffer,content.byteOffset,content.byteLength);
158155
}
159156

160157
exportfunctionbyteLengthOfChunk(chunk: Chunk|PrecomputedChunk): number{

‎packages/react-server/src/ReactServerStreamConfigNode.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ export function flushBuffered(destination: Destination) {
3838
}
3939
}
4040

41-
constVIEW_SIZE=2048;
41+
// Chunks larger than VIEW_SIZE are written directly, without copying into the
42+
// internal view buffer. This must be at least half of Node's internal Buffer
43+
// pool size (8192) to avoid corrupting the pool when using
44+
// renderToReadableStream, which uses a byte stream that detaches ArrayBuffers.
45+
constVIEW_SIZE=4096;
4246
letcurrentView=null;
4347
letwrittenBytes=0;
4448
letdestinationHasCapacity=true;

0 commit comments

Comments
 (0)