@samitouri / QOS-React-1 / commits / 79e4f23808

[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 = {};