Skip to content

Commit a1723e1

Browse files
authored
[Flight] Only skip past the end boundary if there is a newline character (#26945)
Follow up to #26932 For regular rows, we're increasing the index by one to skip past the last trailing newline character which acts a boundary. For length encoded rows we shouldn't skip an extra byte because it'll leave us missing one. This only accidentally worked because this was also the end of the current chunk which tests don't account for since we're just passing through the chunks. So I added some noise by splitting and joining the chunks so that this gets tested.
1 parent a7bf5ba commit a1723e1

File tree

2 files changed

+45
-7
lines changed

2 files changed

+45
-7
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -950,29 +950,33 @@ export function processBinaryChunk(
950950
}
951951
case ROW_CHUNK_BY_LENGTH: {
952952
// We're looking for the remaining byte length
953-
if (i + rowLength <= chunk.length) {
954-
lastIdx = i + rowLength;
953+
lastIdx = i + rowLength;
954+
if (lastIdx > chunk.length) {
955+
lastIdx = -1;
955956
}
956957
break;
957958
}
958959
}
960+
const offset = chunk.byteOffset + i;
959961
if (lastIdx > -1) {
960962
// We found the last chunk of the row
961-
const offset = chunk.byteOffset + i;
962963
const length = lastIdx - i;
963964
const lastChunk = new Uint8Array(chunk.buffer, offset, length);
964965
processFullRow(response, rowID, rowTag, buffer, lastChunk);
965966
// Reset state machine for a new row
967+
i = lastIdx;
968+
if (rowState === ROW_CHUNK_BY_NEWLINE) {
969+
// If we're trailing by a newline we need to skip it.
970+
i++;
971+
}
966972
rowState = ROW_ID;
967973
rowTag = 0;
968974
rowID = 0;
969975
rowLength = 0;
970976
buffer.length = 0;
971-
i = lastIdx + 1;
972977
} else {
973978
// The rest of this row is in a future chunk. We stash the rest of the
974979
// current chunk until we can process the full row.
975-
const offset = chunk.byteOffset + i;
976980
const length = chunk.byteLength - i;
977981
const remainingSlice = new Uint8Array(chunk.buffer, offset, length);
978982
buffer.push(remainingSlice);

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

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,37 @@ describe('ReactFlightDOMEdge', () => {
4242
use = React.use;
4343
});
4444

45+
function passThrough(stream) {
46+
// Simulate more realistic network by splitting up and rejoining some chunks.
47+
// This lets us test that we don't accidentally rely on particular bounds of the chunks.
48+
return new ReadableStream({
49+
async start(controller) {
50+
const reader = stream.getReader();
51+
let prevChunk = new Uint8Array(0);
52+
function push() {
53+
reader.read().then(({done, value}) => {
54+
if (done) {
55+
controller.enqueue(prevChunk);
56+
controller.close();
57+
return;
58+
}
59+
const chunk = new Uint8Array(prevChunk.length + value.length);
60+
chunk.set(prevChunk, 0);
61+
chunk.set(value, prevChunk.length);
62+
if (chunk.length > 50) {
63+
controller.enqueue(chunk.subarray(0, chunk.length - 50));
64+
prevChunk = chunk.subarray(chunk.length - 50);
65+
} else {
66+
prevChunk = chunk;
67+
}
68+
push();
69+
});
70+
}
71+
push();
72+
},
73+
});
74+
}
75+
4576
async function readResult(stream) {
4677
const reader = stream.getReader();
4778
let result = '';
@@ -101,15 +132,17 @@ describe('ReactFlightDOMEdge', () => {
101132

102133
it('should encode long string in a compact format', async () => {
103134
const testString = '"\n\t'.repeat(500) + '🙃';
135+
const testString2 = 'hello'.repeat(400);
104136

105137
const stream = ReactServerDOMServer.renderToReadableStream({
106138
text: testString,
139+
text2: testString2,
107140
});
108-
const [stream1, stream2] = stream.tee();
141+
const [stream1, stream2] = passThrough(stream).tee();
109142

110143
const serializedContent = await readResult(stream1);
111144
// The content should be compact an unescaped
112-
expect(serializedContent.length).toBeLessThan(2000);
145+
expect(serializedContent.length).toBeLessThan(4000);
113146
expect(serializedContent).not.toContain('\\n');
114147
expect(serializedContent).not.toContain('\\t');
115148
expect(serializedContent).not.toContain('\\"');
@@ -118,5 +151,6 @@ describe('ReactFlightDOMEdge', () => {
118151
const result = await ReactServerDOMClient.createFromReadableStream(stream2);
119152
// Should still match the result when parsed
120153
expect(result.text).toBe(testString);
154+
expect(result.text2).toBe(testString2);
121155
});
122156
});

0 commit comments

Comments
 (0)