[Fizz] correctly track header length (#30327)
The interstital characters in our link header tracking are not contributing to the remaining capacity calculation so when a lot of inditidual links are present in the link header it can allow an overflowing link header to be included. This change corrects the math so it properly prevents overflow.
Josh Story committed
Jul 13, 2024 at 07:24 UTC
79e4f23808435d74eaa4a199ae0093cfc176f583
2 files changed
+65
-8
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+12
-8
@@ -431,9 +431,13 @@ export function createRenderState(
431
fontPreloads: '',
432
highImagePreloads: '',
433
remainingCapacity:
434
- typeof maxHeadersLength === 'number'
434
+ // We seed the remainingCapacity with 2 extra bytes because when we decrement the capacity
435
+ // we always assume we are inserting an interstitial ", " however the first header does not actually
436
+ // consume these two extra bytes.
437
+ 2 +
438
+ (typeof maxHeadersLength === 'number'
439
? maxHeadersLength
436
- : DEFAULT_HEADERS_CAPACITY_IN_UTF16_CODE_UNITS,
440
+ : DEFAULT_HEADERS_CAPACITY_IN_UTF16_CODE_UNITS),
441
}
442
: null;
443
const renderState: RenderState = {
@@ -2953,7 +2957,7 @@ function pushImg(
2957
// make this behavior different between render and prerender since in the latter case
2958
// we are less sensitive to the current requests runtime per and more sensitive to maximizing
2959
// headers.
2956
- (headers.remainingCapacity -= header.length) >= 2)
2960
+ (headers.remainingCapacity -= header.length + 2) >= 0)
2961
) {
2962
// If we postpone in the shell we will still emit this preload so we track
2963
// it to make sure we don't reset it.
@@ -5393,7 +5397,7 @@ function prefetchDNS(href: string) {
5397
// make this behavior different between render and prerender since in the latter case
5398
// we are less sensitive to the current requests runtime per and more sensitive to maximizing
5399
// headers.
5396
- (headers.remainingCapacity -= header.length) >= 2)
5400
+ (headers.remainingCapacity -= header.length + 2) >= 0)
5401
) {
5402
// Store this as resettable in case we are prerendering and postpone in the Shell
5403
renderState.resets.dns[key] = EXISTS;
@@ -5452,7 +5456,7 @@ function preconnect(href: string, crossOrigin: ?CrossOriginEnum) {
5456
// make this behavior different between render and prerender since in the latter case
5457
// we are less sensitive to the current requests runtime per and more sensitive to maximizing
5458
// headers.
5455
- (headers.remainingCapacity -= header.length) >= 2)
5459
+ (headers.remainingCapacity -= header.length + 2) >= 0)
5460
) {
5461
// Store this in resettableState in case we are prerending and postpone in the Shell
5462
renderState.resets.connect[bucket][key] = EXISTS;
@@ -5518,7 +5522,7 @@ function preload(href: string, as: string, options?: ?PreloadImplOptions) {
5522
// make this behavior different between render and prerender since in the latter case
5523
// we are less sensitive to the current requests runtime per and more sensitive to maximizing
5524
// headers.
5521
- (headers.remainingCapacity -= header.length) >= 2)
5525
+ (headers.remainingCapacity -= header.length + 2) >= 0)
5526
) {
5527
// If we postpone in the shell we will still emit a preload as a header so we
5528
// track this to make sure we don't reset it.
@@ -5633,7 +5637,7 @@ function preload(href: string, as: string, options?: ?PreloadImplOptions) {
5637
// make this behavior different between render and prerender since in the latter case
5638
// we are less sensitive to the current requests runtime per and more sensitive to maximizing
5639
// headers.
5636
- (headers.remainingCapacity -= header.length) >= 2)
5640
+ (headers.remainingCapacity -= header.length + 2) >= 0)
5641
) {
5642
// If we postpone in the shell we will still emit this preload so we
5643
// track it here to prevent it from being reset.
@@ -6260,7 +6264,7 @@ export function emitEarlyPreloads(
6264
// This means that a particularly long header might close out the header queue where later
6265
// headers could still fit. We could in the future alter the behavior here based on prerender vs render
6266
// since during prerender we aren't as concerned with pure runtime performance.
6263
- if ((headers.remainingCapacity -= header.length) >= 2) {
6267
+ if ((headers.remainingCapacity -= header.length + 2) >= 0) {
6268
renderState.resets.style[key] = PRELOAD_NO_CREDS;
6269
if (linkHeader) {
6270
linkHeader += ', ';
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+53
@@ -3940,6 +3940,59 @@ describe('ReactDOMFizzServer', () => {
3940
expect(getVisibleChildren(container)).toEqual(<div>hello</div>);
3941
});
3942
3943
+ it('accounts for the length of the interstitial between links when computing the headers length', async () => {
3944
+ let headers = null;
3945
+ function onHeaders(x) {
3946
+ headers = x;
3947
+ }
3948
+
3949
+ function App() {
3950
+ // 20 bytes
3951
+ ReactDOM.preconnect('01');
3952
+ // 42 bytes
3953
+ ReactDOM.preconnect('02');
3954
+ // 64 bytes
3955
+ ReactDOM.preconnect('03');
3956
+ // 86 bytes
3957
+ ReactDOM.preconnect('04');
3958
+ // 108 bytes
3959
+ ReactDOM.preconnect('05');
3960
+ // 130 bytes
3961
+ ReactDOM.preconnect('06');
3962
+ // 152 bytes
3963
+ ReactDOM.preconnect('07');
3964
+ // 174 bytes
3965
+ ReactDOM.preconnect('08');
3966
+ // 196 bytes
3967
+ ReactDOM.preconnect('09');
3968
+ // 218 bytes
3969
+ ReactDOM.preconnect('10');
3970
+ // 240 bytes
3971
+ ReactDOM.preconnect('11');
3972
+ // 262 bytes
3973
+ ReactDOM.preconnect('12');
3974
+ // 284 bytes
3975
+ ReactDOM.preconnect('13');
3976
+ // 306 bytes
3977
+ ReactDOM.preconnect('14');
3978
+ return (
3979
+ <html>
3980
+ <body>hello</body>
3981
+ </html>
3982
+ );
3983
+ }
3984
+
3985
+ await act(() => {
3986
+ renderToPipeableStream(<App />, {onHeaders, maxHeadersLength: 305});
3987
+ });
3988
+ expect(headers.Link.length).toBe(284);
3989
+
3990
+ await act(() => {
3991
+ renderToPipeableStream(<App />, {onHeaders, maxHeadersLength: 306});
3992
+ });
3993
+ expect(headers.Link.length).toBe(306);
3994
+ });
3995
+
3996
describe('error escaping', () => {
3997
it('escapes error hash, message, and component stack values in directly flushed errors (html escaping)', async () => {
3998
window.__outlet = {};