Skip to content

Commit 37fa36c

Browse files
authored
[Fizz] Fix crash when capturing the callsite of a stalled use() of a Flight chunk that was rejected in the meantime (#36544)
`ensureSuspendableThenableStateDEV` patches `then` in fulfilled thenables to avoid triggering a custom thenable's `then` in an unexpected state. However, we weren't doing the same for rejected thenables. This affected `ReactPromise`, the type used for thenables passed from server to client. if a `ReactPromise` passed to `use` was pending during the render but became rejected between the abort and `pushSuspendedCallSiteOnComponentStack`, then `ReactPromise#then` would crash. (see the added test for a reprouction) This is because we were putting the ReactPromise into an invalid state: a `PendingChunk` expects to have a `value: null | Array<...>`, but we were deleting `value` altogether, and tgus hitting `TypeError: can't access property "push" of undefined` here: https://github.com/facebook/react/blob/75b0945b18f4a60c80c931fd8067d9c715957879/packages/react-client/src/ReactFlightClient.js#L309 Bypassing the suspended thenable's `then` avoids this crash.
1 parent 75b0945 commit 37fa36c

2 files changed

Lines changed: 196 additions & 2 deletions

File tree

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

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,6 +1583,189 @@ describe('ReactFlightDOMNode', () => {
15831583
}
15841584
});
15851585

1586+
it('should use late-arriving I/O debug info from rejected server promises to enhance component and owner stacks when aborting a prerender',async()=>{
1587+
letrejectHangingPromise;
1588+
1589+
asyncfunctionmakeHangingPromise(){
1590+
returnnewPromise((resolve,reject)=>{
1591+
rejectHangingPromise=reject;
1592+
});
1593+
}
1594+
1595+
asyncfunctiongetRoot(){
1596+
return{promise: makeHangingPromise()};
1597+
}
1598+
1599+
letstaticEndTime=-1;
1600+
conststaticChunks=[];
1601+
constdynamicChunks=[];
1602+
1603+
constserverAbortController=newAbortController();
1604+
awaitnewPromise(resolve=>{
1605+
setTimeout(async()=>{
1606+
conststream=ReactServerDOMServer.renderToPipeableStream(
1607+
getRoot(),
1608+
webpackMap,
1609+
{
1610+
filterStackFrame,
1611+
onError(err){
1612+
if(serverAbortController.signal.aborted){
1613+
return;
1614+
}
1615+
console.error(err);
1616+
},
1617+
},
1618+
);
1619+
serverAbortController.signal.addEventListener(
1620+
'abort',
1621+
()=>{
1622+
stream.abort(serverAbortController.signal.reason);
1623+
1624+
// Only reject the promise after the render is aborted
1625+
// so that it's no longer observable
1626+
rejectHangingPromise(
1627+
newError(
1628+
'Hanging promise was rejected after the prerender finished',
1629+
),
1630+
);
1631+
},
1632+
{once: true},
1633+
);
1634+
1635+
constpassThrough=newStream.PassThrough(streamOptions);
1636+
stream.pipe(passThrough);
1637+
1638+
passThrough.on('data',chunk=>{
1639+
if(staticEndTime<0){
1640+
staticChunks.push(chunk);
1641+
}else{
1642+
dynamicChunks.push(chunk);
1643+
}
1644+
});
1645+
1646+
passThrough.on('end',resolve);
1647+
});
1648+
setTimeout(()=>{
1649+
staticEndTime=performance.now()+performance.timeOrigin;
1650+
serverAbortController.abort();
1651+
});
1652+
});
1653+
1654+
constclientAbortController=newAbortController();
1655+
1656+
constserverStream=createReadableWithLateRelease(
1657+
staticChunks,
1658+
dynamicChunks,
1659+
clientAbortController.signal,
1660+
);
1661+
1662+
constresponse=awaitReactServerDOMClient.createFromNodeStream(
1663+
serverStream,
1664+
{
1665+
serverConsumerManifest: {
1666+
moduleMap: null,
1667+
moduleLoading: null,
1668+
},
1669+
},
1670+
{
1671+
// Debug info arriving after this end time will be ignored, e.g. the
1672+
// I/O info for the second dynamic data.
1673+
endTime: staticEndTime,
1674+
},
1675+
);
1676+
1677+
constresolvedPromise=Promise.resolve('hello');
1678+
functionClientDynamic(){
1679+
use(resolvedPromise);
1680+
use(response.promise);// unresolved ReactPromise (becomes rejected when we abort)
1681+
}
1682+
1683+
functionClientRoot(){
1684+
returnReact.createElement(
1685+
'html',
1686+
null,
1687+
React.createElement(
1688+
'body',
1689+
null,
1690+
React.createElement(
1691+
React.Suspense,
1692+
{fallback: 'Loading...'},
1693+
React.createElement(ClientDynamic),
1694+
),
1695+
),
1696+
);
1697+
}
1698+
1699+
letownerStack;
1700+
letcomponentStack;
1701+
1702+
const{prelude}=awaitnewPromise(resolve=>{
1703+
letresult;
1704+
1705+
setTimeout(()=>{
1706+
result=ReactDOMFizzStatic.prerenderToNodeStream(
1707+
React.createElement(ClientRoot),
1708+
{
1709+
signal: clientAbortController.signal,
1710+
onError(error,errorInfo){
1711+
componentStack=errorInfo.componentStack;
1712+
ownerStack=React.captureOwnerStack
1713+
? React.captureOwnerStack()
1714+
: null;
1715+
},
1716+
},
1717+
);
1718+
});
1719+
1720+
setTimeout(()=>{
1721+
clientAbortController.abort();
1722+
resolve(result);
1723+
});
1724+
});
1725+
1726+
constprerenderHTML=awaitreadResult(prelude);
1727+
1728+
expect(prerenderHTML).toContain('Loading...');
1729+
1730+
if(__DEV__){
1731+
expect(
1732+
normalizeCodeLocInfo(componentStack,{preserveLocation: true}),
1733+
).toBe(
1734+
'\n'+
1735+
' in ClientDynamic (ReactFlightDOMNode-test.js:1679:9)\n'+
1736+
' in Suspense\n'+
1737+
' in body\n'+
1738+
' in html\n'+
1739+
' in ClientRoot',
1740+
);
1741+
}else{
1742+
expect(
1743+
normalizeCodeLocInfo(componentStack,{preserveLocation: true}),
1744+
).toBe(
1745+
'\n'+
1746+
' in ClientDynamic (ReactFlightDOMNode-test.js:1679:9)\n'+
1747+
' in Suspense\n'+
1748+
' in body\n'+
1749+
' in html\n'+
1750+
' in ClientRoot',
1751+
);
1752+
}
1753+
1754+
if(__DEV__){
1755+
expect(ignoreListStack(ownerStack)).toBe(
1756+
'\n'+
1757+
gate(flags=>
1758+
flags.enableAsyncDebugInfo
1759+
? ' at ClientDynamic (./ReactFlightDOMNode-test.js:1680:9)\n'
1760+
: '',
1761+
)+
1762+
' at ClientRoot (./ReactFlightDOMNode-test.js:1693:21)',
1763+
);
1764+
}else{
1765+
expect(ownerStack).toBeNull();
1766+
}
1767+
});
1768+
15861769
functioncreateReadableWithLateRelease(initialChunks,lateChunks,signal){
15871770
// Create a new Readable and push all initial chunks immediately.
15881771
constreadable=newStream.Readable({...streamOptions,read(){}});

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ export function ensureSuspendableThenableStateDEV(
257257
constlastThenable=thenableState[thenableState.length-1];
258258
// Reset the last thenable back to pending.
259259
switch(lastThenable.status){
260-
case'fulfilled':
260+
case'fulfilled':{
261261
const previousThenableValue =lastThenable.value;
262262
// $FlowIgnore[method-unbinding] We rebind .then immediately.
263263
constpreviousThenableThen=lastThenable.then.bind(lastThenable);
@@ -274,14 +274,25 @@ export function ensureSuspendableThenableStateDEV(
274274
lastThenable.value=previousThenableValue;
275275
lastThenable.status='fulfilled';
276276
};
277-
case 'rejected':
277+
}
278+
case 'rejected': {
278279
constpreviousThenableReason=lastThenable.reason;
280+
// $FlowIgnore[method-unbinding] We rebind .then immediately.
281+
constpreviousThenableThen=lastThenable.then.bind(lastThenable);
279282
deletelastThenable.reason;
280283
delete(lastThenable: any).status;
284+
// We'll call .then again if we resuspend. Since we potentially corrupted
285+
// the internal state of unknown classes, we need to diffuse the potential
286+
// crash by replacing the .then method with a noop.
287+
// $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
288+
lastThenable.then=noop;
281289
return()=>{
290+
// $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
291+
lastThenable.then=previousThenableThen;
282292
lastThenable.reason=previousThenableReason;
283293
lastThenable.status='rejected';
284294
};
295+
}
285296
}
286297
returnnoop;
287298
}else{

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" + '
[Fizz] Fix crash when capturing the callsite of a stalled use() of a … · react/react@37fa36c · GitHub
Skip to content

Commit 37fa36c

Browse files
authored
[Fizz] Fix crash when capturing the callsite of a stalled use() of a Flight chunk that was rejected in the meantime (#36544)
`ensureSuspendableThenableStateDEV` patches `then` in fulfilled thenables to avoid triggering a custom thenable's `then` in an unexpected state. However, we weren't doing the same for rejected thenables. This affected `ReactPromise`, the type used for thenables passed from server to client. if a `ReactPromise` passed to `use` was pending during the render but became rejected between the abort and `pushSuspendedCallSiteOnComponentStack`, then `ReactPromise#then` would crash. (see the added test for a reprouction) This is because we were putting the ReactPromise into an invalid state: a `PendingChunk` expects to have a `value: null | Array<...>`, but we were deleting `value` altogether, and tgus hitting `TypeError: can't access property "push" of undefined` here: https://github.com/facebook/react/blob/75b0945b18f4a60c80c931fd8067d9c715957879/packages/react-client/src/ReactFlightClient.js#L309 Bypassing the suspended thenable's `then` avoids this crash.
1 parent 75b0945 commit 37fa36c

2 files changed

Lines changed: 196 additions & 2 deletions

File tree

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

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,6 +1583,189 @@ describe('ReactFlightDOMNode', () => {
15831583
}
15841584
});
15851585

1586+
it('should use late-arriving I/O debug info from rejected server promises to enhance component and owner stacks when aborting a prerender',async()=>{
1587+
letrejectHangingPromise;
1588+
1589+
asyncfunctionmakeHangingPromise(){
1590+
returnnewPromise((resolve,reject)=>{
1591+
rejectHangingPromise=reject;
1592+
});
1593+
}
1594+
1595+
asyncfunctiongetRoot(){
1596+
return{promise: makeHangingPromise()};
1597+
}
1598+
1599+
letstaticEndTime=-1;
1600+
conststaticChunks=[];
1601+
constdynamicChunks=[];
1602+
1603+
constserverAbortController=newAbortController();
1604+
awaitnewPromise(resolve=>{
1605+
setTimeout(async()=>{
1606+
conststream=ReactServerDOMServer.renderToPipeableStream(
1607+
getRoot(),
1608+
webpackMap,
1609+
{
1610+
filterStackFrame,
1611+
onError(err){
1612+
if(serverAbortController.signal.aborted){
1613+
return;
1614+
}
1615+
console.error(err);
1616+
},
1617+
},
1618+
);
1619+
serverAbortController.signal.addEventListener(
1620+
'abort',
1621+
()=>{
1622+
stream.abort(serverAbortController.signal.reason);
1623+
1624+
// Only reject the promise after the render is aborted
1625+
// so that it's no longer observable
1626+
rejectHangingPromise(
1627+
newError(
1628+
'Hanging promise was rejected after the prerender finished',
1629+
),
1630+
);
1631+
},
1632+
{once: true},
1633+
);
1634+
1635+
constpassThrough=newStream.PassThrough(streamOptions);
1636+
stream.pipe(passThrough);
1637+
1638+
passThrough.on('data',chunk=>{
1639+
if(staticEndTime<0){
1640+
staticChunks.push(chunk);
1641+
}else{
1642+
dynamicChunks.push(chunk);
1643+
}
1644+
});
1645+
1646+
passThrough.on('end',resolve);
1647+
});
1648+
setTimeout(()=>{
1649+
staticEndTime=performance.now()+performance.timeOrigin;
1650+
serverAbortController.abort();
1651+
});
1652+
});
1653+
1654+
constclientAbortController=newAbortController();
1655+
1656+
constserverStream=createReadableWithLateRelease(
1657+
staticChunks,
1658+
dynamicChunks,
1659+
clientAbortController.signal,
1660+
);
1661+
1662+
constresponse=awaitReactServerDOMClient.createFromNodeStream(
1663+
serverStream,
1664+
{
1665+
serverConsumerManifest: {
1666+
moduleMap: null,
1667+
moduleLoading: null,
1668+
},
1669+
},
1670+
{
1671+
// Debug info arriving after this end time will be ignored, e.g. the
1672+
// I/O info for the second dynamic data.
1673+
endTime: staticEndTime,
1674+
},
1675+
);
1676+
1677+
constresolvedPromise=Promise.resolve('hello');
1678+
functionClientDynamic(){
1679+
use(resolvedPromise);
1680+
use(response.promise);// unresolved ReactPromise (becomes rejected when we abort)
1681+
}
1682+
1683+
functionClientRoot(){
1684+
returnReact.createElement(
1685+
'html',
1686+
null,
1687+
React.createElement(
1688+
'body',
1689+
null,
1690+
React.createElement(
1691+
React.Suspense,
1692+
{fallback: 'Loading...'},
1693+
React.createElement(ClientDynamic),
1694+
),
1695+
),
1696+
);
1697+
}
1698+
1699+
letownerStack;
1700+
letcomponentStack;
1701+
1702+
const{prelude}=awaitnewPromise(resolve=>{
1703+
letresult;
1704+
1705+
setTimeout(()=>{
1706+
result=ReactDOMFizzStatic.prerenderToNodeStream(
1707+
React.createElement(ClientRoot),
1708+
{
1709+
signal: clientAbortController.signal,
1710+
onError(error,errorInfo){
1711+
componentStack=errorInfo.componentStack;
1712+
ownerStack=React.captureOwnerStack
1713+
? React.captureOwnerStack()
1714+
: null;
1715+
},
1716+
},
1717+
);
1718+
});
1719+
1720+
setTimeout(()=>{
1721+
clientAbortController.abort();
1722+
resolve(result);
1723+
});
1724+
});
1725+
1726+
constprerenderHTML=awaitreadResult(prelude);
1727+
1728+
expect(prerenderHTML).toContain('Loading...');
1729+
1730+
if(__DEV__){
1731+
expect(
1732+
normalizeCodeLocInfo(componentStack,{preserveLocation: true}),
1733+
).toBe(
1734+
'\n'+
1735+
' in ClientDynamic (ReactFlightDOMNode-test.js:1679:9)\n'+
1736+
' in Suspense\n'+
1737+
' in body\n'+
1738+
' in html\n'+
1739+
' in ClientRoot',
1740+
);
1741+
}else{
1742+
expect(
1743+
normalizeCodeLocInfo(componentStack,{preserveLocation: true}),
1744+
).toBe(
1745+
'\n'+
1746+
' in ClientDynamic (ReactFlightDOMNode-test.js:1679:9)\n'+
1747+
' in Suspense\n'+
1748+
' in body\n'+
1749+
' in html\n'+
1750+
' in ClientRoot',
1751+
);
1752+
}
1753+
1754+
if(__DEV__){
1755+
expect(ignoreListStack(ownerStack)).toBe(
1756+
'\n'+
1757+
gate(flags=>
1758+
flags.enableAsyncDebugInfo
1759+
? ' at ClientDynamic (./ReactFlightDOMNode-test.js:1680:9)\n'
1760+
: '',
1761+
)+
1762+
' at ClientRoot (./ReactFlightDOMNode-test.js:1693:21)',
1763+
);
1764+
}else{
1765+
expect(ownerStack).toBeNull();
1766+
}
1767+
});
1768+
15861769
functioncreateReadableWithLateRelease(initialChunks,lateChunks,signal){
15871770
// Create a new Readable and push all initial chunks immediately.
15881771
constreadable=newStream.Readable({...streamOptions,read(){}});

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ export function ensureSuspendableThenableStateDEV(
257257
constlastThenable=thenableState[thenableState.length-1];
258258
// Reset the last thenable back to pending.
259259
switch(lastThenable.status){
260-
case'fulfilled':
260+
case'fulfilled':{
261261
const previousThenableValue =lastThenable.value;
262262
// $FlowIgnore[method-unbinding] We rebind .then immediately.
263263
constpreviousThenableThen=lastThenable.then.bind(lastThenable);
@@ -274,14 +274,25 @@ export function ensureSuspendableThenableStateDEV(
274274
lastThenable.value=previousThenableValue;
275275
lastThenable.status='fulfilled';
276276
};
277-
case 'rejected':
277+
}
278+
case 'rejected': {
278279
constpreviousThenableReason=lastThenable.reason;
280+
// $FlowIgnore[method-unbinding] We rebind .then immediately.
281+
constpreviousThenableThen=lastThenable.then.bind(lastThenable);
279282
deletelastThenable.reason;
280283
delete(lastThenable: any).status;
284+
// We'll call .then again if we resuspend. Since we potentially corrupted
285+
// the internal state of unknown classes, we need to diffuse the potential
286+
// crash by replacing the .then method with a noop.
287+
// $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
288+
lastThenable.then=noop;
281289
return()=>{
290+
// $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
291+
lastThenable.then=previousThenableThen;
282292
lastThenable.reason=previousThenableReason;
283293
lastThenable.status='rejected';
284294
};
295+
}
285296
}
286297
returnnoop;
287298
}else{

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('^' + ".*" + ' [Fizz] Fix crash when capturing the callsite of a stalled use() of a … · react/react@37fa36c · GitHub
Skip to content

Commit 37fa36c

Browse files
authored
[Fizz] Fix crash when capturing the callsite of a stalled use() of a Flight chunk that was rejected in the meantime (#36544)
`ensureSuspendableThenableStateDEV` patches `then` in fulfilled thenables to avoid triggering a custom thenable's `then` in an unexpected state. However, we weren't doing the same for rejected thenables. This affected `ReactPromise`, the type used for thenables passed from server to client. if a `ReactPromise` passed to `use` was pending during the render but became rejected between the abort and `pushSuspendedCallSiteOnComponentStack`, then `ReactPromise#then` would crash. (see the added test for a reprouction) This is because we were putting the ReactPromise into an invalid state: a `PendingChunk` expects to have a `value: null | Array<...>`, but we were deleting `value` altogether, and tgus hitting `TypeError: can't access property "push" of undefined` here: https://github.com/facebook/react/blob/75b0945b18f4a60c80c931fd8067d9c715957879/packages/react-client/src/ReactFlightClient.js#L309 Bypassing the suspended thenable's `then` avoids this crash.
1 parent 75b0945 commit 37fa36c

2 files changed

Lines changed: 196 additions & 2 deletions

File tree

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

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,6 +1583,189 @@ describe('ReactFlightDOMNode', () => {
15831583
}
15841584
});
15851585

1586+
it('should use late-arriving I/O debug info from rejected server promises to enhance component and owner stacks when aborting a prerender',async()=>{
1587+
letrejectHangingPromise;
1588+
1589+
asyncfunctionmakeHangingPromise(){
1590+
returnnewPromise((resolve,reject)=>{
1591+
rejectHangingPromise=reject;
1592+
});
1593+
}
1594+
1595+
asyncfunctiongetRoot(){
1596+
return{promise: makeHangingPromise()};
1597+
}
1598+
1599+
letstaticEndTime=-1;
1600+
conststaticChunks=[];
1601+
constdynamicChunks=[];
1602+
1603+
constserverAbortController=newAbortController();
1604+
awaitnewPromise(resolve=>{
1605+
setTimeout(async()=>{
1606+
conststream=ReactServerDOMServer.renderToPipeableStream(
1607+
getRoot(),
1608+
webpackMap,
1609+
{
1610+
filterStackFrame,
1611+
onError(err){
1612+
if(serverAbortController.signal.aborted){
1613+
return;
1614+
}
1615+
console.error(err);
1616+
},
1617+
},
1618+
);
1619+
serverAbortController.signal.addEventListener(
1620+
'abort',
1621+
()=>{
1622+
stream.abort(serverAbortController.signal.reason);
1623+
1624+
// Only reject the promise after the render is aborted
1625+
// so that it's no longer observable
1626+
rejectHangingPromise(
1627+
newError(
1628+
'Hanging promise was rejected after the prerender finished',
1629+
),
1630+
);
1631+
},
1632+
{once: true},
1633+
);
1634+
1635+
constpassThrough=newStream.PassThrough(streamOptions);
1636+
stream.pipe(passThrough);
1637+
1638+
passThrough.on('data',chunk=>{
1639+
if(staticEndTime<0){
1640+
staticChunks.push(chunk);
1641+
}else{
1642+
dynamicChunks.push(chunk);
1643+
}
1644+
});
1645+
1646+
passThrough.on('end',resolve);
1647+
});
1648+
setTimeout(()=>{
1649+
staticEndTime=performance.now()+performance.timeOrigin;
1650+
serverAbortController.abort();
1651+
});
1652+
});
1653+
1654+
constclientAbortController=newAbortController();
1655+
1656+
constserverStream=createReadableWithLateRelease(
1657+
staticChunks,
1658+
dynamicChunks,
1659+
clientAbortController.signal,
1660+
);
1661+
1662+
constresponse=awaitReactServerDOMClient.createFromNodeStream(
1663+
serverStream,
1664+
{
1665+
serverConsumerManifest: {
1666+
moduleMap: null,
1667+
moduleLoading: null,
1668+
},
1669+
},
1670+
{
1671+
// Debug info arriving after this end time will be ignored, e.g. the
1672+
// I/O info for the second dynamic data.
1673+
endTime: staticEndTime,
1674+
},
1675+
);
1676+
1677+
constresolvedPromise=Promise.resolve('hello');
1678+
functionClientDynamic(){
1679+
use(resolvedPromise);
1680+
use(response.promise);// unresolved ReactPromise (becomes rejected when we abort)
1681+
}
1682+
1683+
functionClientRoot(){
1684+
returnReact.createElement(
1685+
'html',
1686+
null,
1687+
React.createElement(
1688+
'body',
1689+
null,
1690+
React.createElement(
1691+
React.Suspense,
1692+
{fallback: 'Loading...'},
1693+
React.createElement(ClientDynamic),
1694+
),
1695+
),
1696+
);
1697+
}
1698+
1699+
letownerStack;
1700+
letcomponentStack;
1701+
1702+
const{prelude}=awaitnewPromise(resolve=>{
1703+
letresult;
1704+
1705+
setTimeout(()=>{
1706+
result=ReactDOMFizzStatic.prerenderToNodeStream(
1707+
React.createElement(ClientRoot),
1708+
{
1709+
signal: clientAbortController.signal,
1710+
onError(error,errorInfo){
1711+
componentStack=errorInfo.componentStack;
1712+
ownerStack=React.captureOwnerStack
1713+
? React.captureOwnerStack()
1714+
: null;
1715+
},
1716+
},
1717+
);
1718+
});
1719+
1720+
setTimeout(()=>{
1721+
clientAbortController.abort();
1722+
resolve(result);
1723+
});
1724+
});
1725+
1726+
constprerenderHTML=awaitreadResult(prelude);
1727+
1728+
expect(prerenderHTML).toContain('Loading...');
1729+
1730+
if(__DEV__){
1731+
expect(
1732+
normalizeCodeLocInfo(componentStack,{preserveLocation: true}),
1733+
).toBe(
1734+
'\n'+
1735+
' in ClientDynamic (ReactFlightDOMNode-test.js:1679:9)\n'+
1736+
' in Suspense\n'+
1737+
' in body\n'+
1738+
' in html\n'+
1739+
' in ClientRoot',
1740+
);
1741+
}else{
1742+
expect(
1743+
normalizeCodeLocInfo(componentStack,{preserveLocation: true}),
1744+
).toBe(
1745+
'\n'+
1746+
' in ClientDynamic (ReactFlightDOMNode-test.js:1679:9)\n'+
1747+
' in Suspense\n'+
1748+
' in body\n'+
1749+
' in html\n'+
1750+
' in ClientRoot',
1751+
);
1752+
}
1753+
1754+
if(__DEV__){
1755+
expect(ignoreListStack(ownerStack)).toBe(
1756+
'\n'+
1757+
gate(flags=>
1758+
flags.enableAsyncDebugInfo
1759+
? ' at ClientDynamic (./ReactFlightDOMNode-test.js:1680:9)\n'
1760+
: '',
1761+
)+
1762+
' at ClientRoot (./ReactFlightDOMNode-test.js:1693:21)',
1763+
);
1764+
}else{
1765+
expect(ownerStack).toBeNull();
1766+
}
1767+
});
1768+
15861769
functioncreateReadableWithLateRelease(initialChunks,lateChunks,signal){
15871770
// Create a new Readable and push all initial chunks immediately.
15881771
constreadable=newStream.Readable({...streamOptions,read(){}});

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ export function ensureSuspendableThenableStateDEV(
257257
constlastThenable=thenableState[thenableState.length-1];
258258
// Reset the last thenable back to pending.
259259
switch(lastThenable.status){
260-
case'fulfilled':
260+
case'fulfilled':{
261261
const previousThenableValue =lastThenable.value;
262262
// $FlowIgnore[method-unbinding] We rebind .then immediately.
263263
constpreviousThenableThen=lastThenable.then.bind(lastThenable);
@@ -274,14 +274,25 @@ export function ensureSuspendableThenableStateDEV(
274274
lastThenable.value=previousThenableValue;
275275
lastThenable.status='fulfilled';
276276
};
277-
case 'rejected':
277+
}
278+
case 'rejected': {
278279
constpreviousThenableReason=lastThenable.reason;
280+
// $FlowIgnore[method-unbinding] We rebind .then immediately.
281+
constpreviousThenableThen=lastThenable.then.bind(lastThenable);
279282
deletelastThenable.reason;
280283
delete(lastThenable: any).status;
284+
// We'll call .then again if we resuspend. Since we potentially corrupted
285+
// the internal state of unknown classes, we need to diffuse the potential
286+
// crash by replacing the .then method with a noop.
287+
// $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
288+
lastThenable.then=noop;
281289
return()=>{
290+
// $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
291+
lastThenable.then=previousThenableThen;
282292
lastThenable.reason=previousThenableReason;
283293
lastThenable.status='rejected';
284294
};
295+
}
285296
}
286297
returnnoop;
287298
}else{

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('^' + ".*" + ' [Fizz] Fix crash when capturing the callsite of a stalled use() of a … · react/react@37fa36c · GitHub
Skip to content

Commit 37fa36c

Browse files
authored
[Fizz] Fix crash when capturing the callsite of a stalled use() of a Flight chunk that was rejected in the meantime (#36544)
`ensureSuspendableThenableStateDEV` patches `then` in fulfilled thenables to avoid triggering a custom thenable's `then` in an unexpected state. However, we weren't doing the same for rejected thenables. This affected `ReactPromise`, the type used for thenables passed from server to client. if a `ReactPromise` passed to `use` was pending during the render but became rejected between the abort and `pushSuspendedCallSiteOnComponentStack`, then `ReactPromise#then` would crash. (see the added test for a reprouction) This is because we were putting the ReactPromise into an invalid state: a `PendingChunk` expects to have a `value: null | Array<...>`, but we were deleting `value` altogether, and tgus hitting `TypeError: can't access property "push" of undefined` here: https://github.com/facebook/react/blob/75b0945b18f4a60c80c931fd8067d9c715957879/packages/react-client/src/ReactFlightClient.js#L309 Bypassing the suspended thenable's `then` avoids this crash.
1 parent 75b0945 commit 37fa36c

2 files changed

Lines changed: 196 additions & 2 deletions

File tree

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

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,6 +1583,189 @@ describe('ReactFlightDOMNode', () => {
15831583
}
15841584
});
15851585

1586+
it('should use late-arriving I/O debug info from rejected server promises to enhance component and owner stacks when aborting a prerender',async()=>{
1587+
letrejectHangingPromise;
1588+
1589+
asyncfunctionmakeHangingPromise(){
1590+
returnnewPromise((resolve,reject)=>{
1591+
rejectHangingPromise=reject;
1592+
});
1593+
}
1594+
1595+
asyncfunctiongetRoot(){
1596+
return{promise: makeHangingPromise()};
1597+
}
1598+
1599+
letstaticEndTime=-1;
1600+
conststaticChunks=[];
1601+
constdynamicChunks=[];
1602+
1603+
constserverAbortController=newAbortController();
1604+
awaitnewPromise(resolve=>{
1605+
setTimeout(async()=>{
1606+
conststream=ReactServerDOMServer.renderToPipeableStream(
1607+
getRoot(),
1608+
webpackMap,
1609+
{
1610+
filterStackFrame,
1611+
onError(err){
1612+
if(serverAbortController.signal.aborted){
1613+
return;
1614+
}
1615+
console.error(err);
1616+
},
1617+
},
1618+
);
1619+
serverAbortController.signal.addEventListener(
1620+
'abort',
1621+
()=>{
1622+
stream.abort(serverAbortController.signal.reason);
1623+
1624+
// Only reject the promise after the render is aborted
1625+
// so that it's no longer observable
1626+
rejectHangingPromise(
1627+
newError(
1628+
'Hanging promise was rejected after the prerender finished',
1629+
),
1630+
);
1631+
},
1632+
{once: true},
1633+
);
1634+
1635+
constpassThrough=newStream.PassThrough(streamOptions);
1636+
stream.pipe(passThrough);
1637+
1638+
passThrough.on('data',chunk=>{
1639+
if(staticEndTime<0){
1640+
staticChunks.push(chunk);
1641+
}else{
1642+
dynamicChunks.push(chunk);
1643+
}
1644+
});
1645+
1646+
passThrough.on('end',resolve);
1647+
});
1648+
setTimeout(()=>{
1649+
staticEndTime=performance.now()+performance.timeOrigin;
1650+
serverAbortController.abort();
1651+
});
1652+
});
1653+
1654+
constclientAbortController=newAbortController();
1655+
1656+
constserverStream=createReadableWithLateRelease(
1657+
staticChunks,
1658+
dynamicChunks,
1659+
clientAbortController.signal,
1660+
);
1661+
1662+
constresponse=awaitReactServerDOMClient.createFromNodeStream(
1663+
serverStream,
1664+
{
1665+
serverConsumerManifest: {
1666+
moduleMap: null,
1667+
moduleLoading: null,
1668+
},
1669+
},
1670+
{
1671+
// Debug info arriving after this end time will be ignored, e.g. the
1672+
// I/O info for the second dynamic data.
1673+
endTime: staticEndTime,
1674+
},
1675+
);
1676+
1677+
constresolvedPromise=Promise.resolve('hello');
1678+
functionClientDynamic(){
1679+
use(resolvedPromise);
1680+
use(response.promise);// unresolved ReactPromise (becomes rejected when we abort)
1681+
}
1682+
1683+
functionClientRoot(){
1684+
returnReact.createElement(
1685+
'html',
1686+
null,
1687+
React.createElement(
1688+
'body',
1689+
null,
1690+
React.createElement(
1691+
React.Suspense,
1692+
{fallback: 'Loading...'},
1693+
React.createElement(ClientDynamic),
1694+
),
1695+
),
1696+
);
1697+
}
1698+
1699+
letownerStack;
1700+
letcomponentStack;
1701+
1702+
const{prelude}=awaitnewPromise(resolve=>{
1703+
letresult;
1704+
1705+
setTimeout(()=>{
1706+
result=ReactDOMFizzStatic.prerenderToNodeStream(
1707+
React.createElement(ClientRoot),
1708+
{
1709+
signal: clientAbortController.signal,
1710+
onError(error,errorInfo){
1711+
componentStack=errorInfo.componentStack;
1712+
ownerStack=React.captureOwnerStack
1713+
? React.captureOwnerStack()
1714+
: null;
1715+
},
1716+
},
1717+
);
1718+
});
1719+
1720+
setTimeout(()=>{
1721+
clientAbortController.abort();
1722+
resolve(result);
1723+
});
1724+
});
1725+
1726+
constprerenderHTML=awaitreadResult(prelude);
1727+
1728+
expect(prerenderHTML).toContain('Loading...');
1729+
1730+
if(__DEV__){
1731+
expect(
1732+
normalizeCodeLocInfo(componentStack,{preserveLocation: true}),
1733+
).toBe(
1734+
'\n'+
1735+
' in ClientDynamic (ReactFlightDOMNode-test.js:1679:9)\n'+
1736+
' in Suspense\n'+
1737+
' in body\n'+
1738+
' in html\n'+
1739+
' in ClientRoot',
1740+
);
1741+
}else{
1742+
expect(
1743+
normalizeCodeLocInfo(componentStack,{preserveLocation: true}),
1744+
).toBe(
1745+
'\n'+
1746+
' in ClientDynamic (ReactFlightDOMNode-test.js:1679:9)\n'+
1747+
' in Suspense\n'+
1748+
' in body\n'+
1749+
' in html\n'+
1750+
' in ClientRoot',
1751+
);
1752+
}
1753+
1754+
if(__DEV__){
1755+
expect(ignoreListStack(ownerStack)).toBe(
1756+
'\n'+
1757+
gate(flags=>
1758+
flags.enableAsyncDebugInfo
1759+
? ' at ClientDynamic (./ReactFlightDOMNode-test.js:1680:9)\n'
1760+
: '',
1761+
)+
1762+
' at ClientRoot (./ReactFlightDOMNode-test.js:1693:21)',
1763+
);
1764+
}else{
1765+
expect(ownerStack).toBeNull();
1766+
}
1767+
});
1768+
15861769
functioncreateReadableWithLateRelease(initialChunks,lateChunks,signal){
15871770
// Create a new Readable and push all initial chunks immediately.
15881771
constreadable=newStream.Readable({...streamOptions,read(){}});

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ export function ensureSuspendableThenableStateDEV(
257257
constlastThenable=thenableState[thenableState.length-1];
258258
// Reset the last thenable back to pending.
259259
switch(lastThenable.status){
260-
case'fulfilled':
260+
case'fulfilled':{
261261
const previousThenableValue =lastThenable.value;
262262
// $FlowIgnore[method-unbinding] We rebind .then immediately.
263263
constpreviousThenableThen=lastThenable.then.bind(lastThenable);
@@ -274,14 +274,25 @@ export function ensureSuspendableThenableStateDEV(
274274
lastThenable.value=previousThenableValue;
275275
lastThenable.status='fulfilled';
276276
};
277-
case 'rejected':
277+
}
278+
case 'rejected': {
278279
constpreviousThenableReason=lastThenable.reason;
280+
// $FlowIgnore[method-unbinding] We rebind .then immediately.
281+
constpreviousThenableThen=lastThenable.then.bind(lastThenable);
279282
deletelastThenable.reason;
280283
delete(lastThenable: any).status;
284+
// We'll call .then again if we resuspend. Since we potentially corrupted
285+
// the internal state of unknown classes, we need to diffuse the potential
286+
// crash by replacing the .then method with a noop.
287+
// $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
288+
lastThenable.then=noop;
281289
return()=>{
290+
// $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
291+
lastThenable.then=previousThenableThen;
282292
lastThenable.reason=previousThenableReason;
283293
lastThenable.status='rejected';
284294
};
295+
}
285296
}
286297
returnnoop;
287298
}else{

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" + ' [Fizz] Fix crash when capturing the callsite of a stalled use() of a … · react/react@37fa36c · GitHub
Skip to content

Commit 37fa36c

Browse files
authored
[Fizz] Fix crash when capturing the callsite of a stalled use() of a Flight chunk that was rejected in the meantime (#36544)
`ensureSuspendableThenableStateDEV` patches `then` in fulfilled thenables to avoid triggering a custom thenable's `then` in an unexpected state. However, we weren't doing the same for rejected thenables. This affected `ReactPromise`, the type used for thenables passed from server to client. if a `ReactPromise` passed to `use` was pending during the render but became rejected between the abort and `pushSuspendedCallSiteOnComponentStack`, then `ReactPromise#then` would crash. (see the added test for a reprouction) This is because we were putting the ReactPromise into an invalid state: a `PendingChunk` expects to have a `value: null | Array<...>`, but we were deleting `value` altogether, and tgus hitting `TypeError: can't access property "push" of undefined` here: https://github.com/facebook/react/blob/75b0945b18f4a60c80c931fd8067d9c715957879/packages/react-client/src/ReactFlightClient.js#L309 Bypassing the suspended thenable's `then` avoids this crash.
1 parent 75b0945 commit 37fa36c

2 files changed

Lines changed: 196 additions & 2 deletions

File tree

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

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,6 +1583,189 @@ describe('ReactFlightDOMNode', () => {
15831583
}
15841584
});
15851585

1586+
it('should use late-arriving I/O debug info from rejected server promises to enhance component and owner stacks when aborting a prerender',async()=>{
1587+
letrejectHangingPromise;
1588+
1589+
asyncfunctionmakeHangingPromise(){
1590+
returnnewPromise((resolve,reject)=>{
1591+
rejectHangingPromise=reject;
1592+
});
1593+
}
1594+
1595+
asyncfunctiongetRoot(){
1596+
return{promise: makeHangingPromise()};
1597+
}
1598+
1599+
letstaticEndTime=-1;
1600+
conststaticChunks=[];
1601+
constdynamicChunks=[];
1602+
1603+
constserverAbortController=newAbortController();
1604+
awaitnewPromise(resolve=>{
1605+
setTimeout(async()=>{
1606+
conststream=ReactServerDOMServer.renderToPipeableStream(
1607+
getRoot(),
1608+
webpackMap,
1609+
{
1610+
filterStackFrame,
1611+
onError(err){
1612+
if(serverAbortController.signal.aborted){
1613+
return;
1614+
}
1615+
console.error(err);
1616+
},
1617+
},
1618+
);
1619+
serverAbortController.signal.addEventListener(
1620+
'abort',
1621+
()=>{
1622+
stream.abort(serverAbortController.signal.reason);
1623+
1624+
// Only reject the promise after the render is aborted
1625+
// so that it's no longer observable
1626+
rejectHangingPromise(
1627+
newError(
1628+
'Hanging promise was rejected after the prerender finished',
1629+
),
1630+
);
1631+
},
1632+
{once: true},
1633+
);
1634+
1635+
constpassThrough=newStream.PassThrough(streamOptions);
1636+
stream.pipe(passThrough);
1637+
1638+
passThrough.on('data',chunk=>{
1639+
if(staticEndTime<0){
1640+
staticChunks.push(chunk);
1641+
}else{
1642+
dynamicChunks.push(chunk);
1643+
}
1644+
});
1645+
1646+
passThrough.on('end',resolve);
1647+
});
1648+
setTimeout(()=>{
1649+
staticEndTime=performance.now()+performance.timeOrigin;
1650+
serverAbortController.abort();
1651+
});
1652+
});
1653+
1654+
constclientAbortController=newAbortController();
1655+
1656+
constserverStream=createReadableWithLateRelease(
1657+
staticChunks,
1658+
dynamicChunks,
1659+
clientAbortController.signal,
1660+
);
1661+
1662+
constresponse=awaitReactServerDOMClient.createFromNodeStream(
1663+
serverStream,
1664+
{
1665+
serverConsumerManifest: {
1666+
moduleMap: null,
1667+
moduleLoading: null,
1668+
},
1669+
},
1670+
{
1671+
// Debug info arriving after this end time will be ignored, e.g. the
1672+
// I/O info for the second dynamic data.
1673+
endTime: staticEndTime,
1674+
},
1675+
);
1676+
1677+
constresolvedPromise=Promise.resolve('hello');
1678+
functionClientDynamic(){
1679+
use(resolvedPromise);
1680+
use(response.promise);// unresolved ReactPromise (becomes rejected when we abort)
1681+
}
1682+
1683+
functionClientRoot(){
1684+
returnReact.createElement(
1685+
'html',
1686+
null,
1687+
React.createElement(
1688+
'body',
1689+
null,
1690+
React.createElement(
1691+
React.Suspense,
1692+
{fallback: 'Loading...'},
1693+
React.createElement(ClientDynamic),
1694+
),
1695+
),
1696+
);
1697+
}
1698+
1699+
letownerStack;
1700+
letcomponentStack;
1701+
1702+
const{prelude}=awaitnewPromise(resolve=>{
1703+
letresult;
1704+
1705+
setTimeout(()=>{
1706+
result=ReactDOMFizzStatic.prerenderToNodeStream(
1707+
React.createElement(ClientRoot),
1708+
{
1709+
signal: clientAbortController.signal,
1710+
onError(error,errorInfo){
1711+
componentStack=errorInfo.componentStack;
1712+
ownerStack=React.captureOwnerStack
1713+
? React.captureOwnerStack()
1714+
: null;
1715+
},
1716+
},
1717+
);
1718+
});
1719+
1720+
setTimeout(()=>{
1721+
clientAbortController.abort();
1722+
resolve(result);
1723+
});
1724+
});
1725+
1726+
constprerenderHTML=awaitreadResult(prelude);
1727+
1728+
expect(prerenderHTML).toContain('Loading...');
1729+
1730+
if(__DEV__){
1731+
expect(
1732+
normalizeCodeLocInfo(componentStack,{preserveLocation: true}),
1733+
).toBe(
1734+
'\n'+
1735+
' in ClientDynamic (ReactFlightDOMNode-test.js:1679:9)\n'+
1736+
' in Suspense\n'+
1737+
' in body\n'+
1738+
' in html\n'+
1739+
' in ClientRoot',
1740+
);
1741+
}else{
1742+
expect(
1743+
normalizeCodeLocInfo(componentStack,{preserveLocation: true}),
1744+
).toBe(
1745+
'\n'+
1746+
' in ClientDynamic (ReactFlightDOMNode-test.js:1679:9)\n'+
1747+
' in Suspense\n'+
1748+
' in body\n'+
1749+
' in html\n'+
1750+
' in ClientRoot',
1751+
);
1752+
}
1753+
1754+
if(__DEV__){
1755+
expect(ignoreListStack(ownerStack)).toBe(
1756+
'\n'+
1757+
gate(flags=>
1758+
flags.enableAsyncDebugInfo
1759+
? ' at ClientDynamic (./ReactFlightDOMNode-test.js:1680:9)\n'
1760+
: '',
1761+
)+
1762+
' at ClientRoot (./ReactFlightDOMNode-test.js:1693:21)',
1763+
);
1764+
}else{
1765+
expect(ownerStack).toBeNull();
1766+
}
1767+
});
1768+
15861769
functioncreateReadableWithLateRelease(initialChunks,lateChunks,signal){
15871770
// Create a new Readable and push all initial chunks immediately.
15881771
constreadable=newStream.Readable({...streamOptions,read(){}});

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ export function ensureSuspendableThenableStateDEV(
257257
constlastThenable=thenableState[thenableState.length-1];
258258
// Reset the last thenable back to pending.
259259
switch(lastThenable.status){
260-
case'fulfilled':
260+
case'fulfilled':{
261261
const previousThenableValue =lastThenable.value;
262262
// $FlowIgnore[method-unbinding] We rebind .then immediately.
263263
constpreviousThenableThen=lastThenable.then.bind(lastThenable);
@@ -274,14 +274,25 @@ export function ensureSuspendableThenableStateDEV(
274274
lastThenable.value=previousThenableValue;
275275
lastThenable.status='fulfilled';
276276
};
277-
case 'rejected':
277+
}
278+
case 'rejected': {
278279
constpreviousThenableReason=lastThenable.reason;
280+
// $FlowIgnore[method-unbinding] We rebind .then immediately.
281+
constpreviousThenableThen=lastThenable.then.bind(lastThenable);
279282
deletelastThenable.reason;
280283
delete(lastThenable: any).status;
284+
// We'll call .then again if we resuspend. Since we potentially corrupted
285+
// the internal state of unknown classes, we need to diffuse the potential
286+
// crash by replacing the .then method with a noop.
287+
// $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
288+
lastThenable.then=noop;
281289
return()=>{
290+
// $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
291+
lastThenable.then=previousThenableThen;
282292
lastThenable.reason=previousThenableReason;
283293
lastThenable.status='rejected';
284294
};
295+
}
285296
}
286297
returnnoop;
287298
}else{

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('^' + ".*" + ' [Fizz] Fix crash when capturing the callsite of a stalled use() of a … · react/react@37fa36c · GitHub
Skip to content

Commit 37fa36c

Browse files
authored
[Fizz] Fix crash when capturing the callsite of a stalled use() of a Flight chunk that was rejected in the meantime (#36544)
`ensureSuspendableThenableStateDEV` patches `then` in fulfilled thenables to avoid triggering a custom thenable's `then` in an unexpected state. However, we weren't doing the same for rejected thenables. This affected `ReactPromise`, the type used for thenables passed from server to client. if a `ReactPromise` passed to `use` was pending during the render but became rejected between the abort and `pushSuspendedCallSiteOnComponentStack`, then `ReactPromise#then` would crash. (see the added test for a reprouction) This is because we were putting the ReactPromise into an invalid state: a `PendingChunk` expects to have a `value: null | Array<...>`, but we were deleting `value` altogether, and tgus hitting `TypeError: can't access property "push" of undefined` here: https://github.com/facebook/react/blob/75b0945b18f4a60c80c931fd8067d9c715957879/packages/react-client/src/ReactFlightClient.js#L309 Bypassing the suspended thenable's `then` avoids this crash.
1 parent 75b0945 commit 37fa36c

2 files changed

Lines changed: 196 additions & 2 deletions

File tree

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

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,6 +1583,189 @@ describe('ReactFlightDOMNode', () => {
15831583
}
15841584
});
15851585

1586+
it('should use late-arriving I/O debug info from rejected server promises to enhance component and owner stacks when aborting a prerender',async()=>{
1587+
letrejectHangingPromise;
1588+
1589+
asyncfunctionmakeHangingPromise(){
1590+
returnnewPromise((resolve,reject)=>{
1591+
rejectHangingPromise=reject;
1592+
});
1593+
}
1594+
1595+
asyncfunctiongetRoot(){
1596+
return{promise: makeHangingPromise()};
1597+
}
1598+
1599+
letstaticEndTime=-1;
1600+
conststaticChunks=[];
1601+
constdynamicChunks=[];
1602+
1603+
constserverAbortController=newAbortController();
1604+
awaitnewPromise(resolve=>{
1605+
setTimeout(async()=>{
1606+
conststream=ReactServerDOMServer.renderToPipeableStream(
1607+
getRoot(),
1608+
webpackMap,
1609+
{
1610+
filterStackFrame,
1611+
onError(err){
1612+
if(serverAbortController.signal.aborted){
1613+
return;
1614+
}
1615+
console.error(err);
1616+
},
1617+
},
1618+
);
1619+
serverAbortController.signal.addEventListener(
1620+
'abort',
1621+
()=>{
1622+
stream.abort(serverAbortController.signal.reason);
1623+
1624+
// Only reject the promise after the render is aborted
1625+
// so that it's no longer observable
1626+
rejectHangingPromise(
1627+
newError(
1628+
'Hanging promise was rejected after the prerender finished',
1629+
),
1630+
);
1631+
},
1632+
{once: true},
1633+
);
1634+
1635+
constpassThrough=newStream.PassThrough(streamOptions);
1636+
stream.pipe(passThrough);
1637+
1638+
passThrough.on('data',chunk=>{
1639+
if(staticEndTime<0){
1640+
staticChunks.push(chunk);
1641+
}else{
1642+
dynamicChunks.push(chunk);
1643+
}
1644+
});
1645+
1646+
passThrough.on('end',resolve);
1647+
});
1648+
setTimeout(()=>{
1649+
staticEndTime=performance.now()+performance.timeOrigin;
1650+
serverAbortController.abort();
1651+
});
1652+
});
1653+
1654+
constclientAbortController=newAbortController();
1655+
1656+
constserverStream=createReadableWithLateRelease(
1657+
staticChunks,
1658+
dynamicChunks,
1659+
clientAbortController.signal,
1660+
);
1661+
1662+
constresponse=awaitReactServerDOMClient.createFromNodeStream(
1663+
serverStream,
1664+
{
1665+
serverConsumerManifest: {
1666+
moduleMap: null,
1667+
moduleLoading: null,
1668+
},
1669+
},
1670+
{
1671+
// Debug info arriving after this end time will be ignored, e.g. the
1672+
// I/O info for the second dynamic data.
1673+
endTime: staticEndTime,
1674+
},
1675+
);
1676+
1677+
constresolvedPromise=Promise.resolve('hello');
1678+
functionClientDynamic(){
1679+
use(resolvedPromise);
1680+
use(response.promise);// unresolved ReactPromise (becomes rejected when we abort)
1681+
}
1682+
1683+
functionClientRoot(){
1684+
returnReact.createElement(
1685+
'html',
1686+
null,
1687+
React.createElement(
1688+
'body',
1689+
null,
1690+
React.createElement(
1691+
React.Suspense,
1692+
{fallback: 'Loading...'},
1693+
React.createElement(ClientDynamic),
1694+
),
1695+
),
1696+
);
1697+
}
1698+
1699+
letownerStack;
1700+
letcomponentStack;
1701+
1702+
const{prelude}=awaitnewPromise(resolve=>{
1703+
letresult;
1704+
1705+
setTimeout(()=>{
1706+
result=ReactDOMFizzStatic.prerenderToNodeStream(
1707+
React.createElement(ClientRoot),
1708+
{
1709+
signal: clientAbortController.signal,
1710+
onError(error,errorInfo){
1711+
componentStack=errorInfo.componentStack;
1712+
ownerStack=React.captureOwnerStack
1713+
? React.captureOwnerStack()
1714+
: null;
1715+
},
1716+
},
1717+
);
1718+
});
1719+
1720+
setTimeout(()=>{
1721+
clientAbortController.abort();
1722+
resolve(result);
1723+
});
1724+
});
1725+
1726+
constprerenderHTML=awaitreadResult(prelude);
1727+
1728+
expect(prerenderHTML).toContain('Loading...');
1729+
1730+
if(__DEV__){
1731+
expect(
1732+
normalizeCodeLocInfo(componentStack,{preserveLocation: true}),
1733+
).toBe(
1734+
'\n'+
1735+
' in ClientDynamic (ReactFlightDOMNode-test.js:1679:9)\n'+
1736+
' in Suspense\n'+
1737+
' in body\n'+
1738+
' in html\n'+
1739+
' in ClientRoot',
1740+
);
1741+
}else{
1742+
expect(
1743+
normalizeCodeLocInfo(componentStack,{preserveLocation: true}),
1744+
).toBe(
1745+
'\n'+
1746+
' in ClientDynamic (ReactFlightDOMNode-test.js:1679:9)\n'+
1747+
' in Suspense\n'+
1748+
' in body\n'+
1749+
' in html\n'+
1750+
' in ClientRoot',
1751+
);
1752+
}
1753+
1754+
if(__DEV__){
1755+
expect(ignoreListStack(ownerStack)).toBe(
1756+
'\n'+
1757+
gate(flags=>
1758+
flags.enableAsyncDebugInfo
1759+
? ' at ClientDynamic (./ReactFlightDOMNode-test.js:1680:9)\n'
1760+
: '',
1761+
)+
1762+
' at ClientRoot (./ReactFlightDOMNode-test.js:1693:21)',
1763+
);
1764+
}else{
1765+
expect(ownerStack).toBeNull();
1766+
}
1767+
});
1768+
15861769
functioncreateReadableWithLateRelease(initialChunks,lateChunks,signal){
15871770
// Create a new Readable and push all initial chunks immediately.
15881771
constreadable=newStream.Readable({...streamOptions,read(){}});

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ export function ensureSuspendableThenableStateDEV(
257257
constlastThenable=thenableState[thenableState.length-1];
258258
// Reset the last thenable back to pending.
259259
switch(lastThenable.status){
260-
case'fulfilled':
260+
case'fulfilled':{
261261
const previousThenableValue =lastThenable.value;
262262
// $FlowIgnore[method-unbinding] We rebind .then immediately.
263263
constpreviousThenableThen=lastThenable.then.bind(lastThenable);
@@ -274,14 +274,25 @@ export function ensureSuspendableThenableStateDEV(
274274
lastThenable.value=previousThenableValue;
275275
lastThenable.status='fulfilled';
276276
};
277-
case 'rejected':
277+
}
278+
case 'rejected': {
278279
constpreviousThenableReason=lastThenable.reason;
280+
// $FlowIgnore[method-unbinding] We rebind .then immediately.
281+
constpreviousThenableThen=lastThenable.then.bind(lastThenable);
279282
deletelastThenable.reason;
280283
delete(lastThenable: any).status;
284+
// We'll call .then again if we resuspend. Since we potentially corrupted
285+
// the internal state of unknown classes, we need to diffuse the potential
286+
// crash by replacing the .then method with a noop.
287+
// $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
288+
lastThenable.then=noop;
281289
return()=>{
290+
// $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
291+
lastThenable.then=previousThenableThen;
282292
lastThenable.reason=previousThenableReason;
283293
lastThenable.status='rejected';
284294
};
295+
}
285296
}
286297
returnnoop;
287298
}else{

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('^' + ".*" + ' [Fizz] Fix crash when capturing the callsite of a stalled use() of a … · react/react@37fa36c · GitHub
Skip to content

Commit 37fa36c

Browse files
authored
[Fizz] Fix crash when capturing the callsite of a stalled use() of a Flight chunk that was rejected in the meantime (#36544)
`ensureSuspendableThenableStateDEV` patches `then` in fulfilled thenables to avoid triggering a custom thenable's `then` in an unexpected state. However, we weren't doing the same for rejected thenables. This affected `ReactPromise`, the type used for thenables passed from server to client. if a `ReactPromise` passed to `use` was pending during the render but became rejected between the abort and `pushSuspendedCallSiteOnComponentStack`, then `ReactPromise#then` would crash. (see the added test for a reprouction) This is because we were putting the ReactPromise into an invalid state: a `PendingChunk` expects to have a `value: null | Array<...>`, but we were deleting `value` altogether, and tgus hitting `TypeError: can't access property "push" of undefined` here: https://github.com/facebook/react/blob/75b0945b18f4a60c80c931fd8067d9c715957879/packages/react-client/src/ReactFlightClient.js#L309 Bypassing the suspended thenable's `then` avoids this crash.
1 parent 75b0945 commit 37fa36c

2 files changed

Lines changed: 196 additions & 2 deletions

File tree

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

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,6 +1583,189 @@ describe('ReactFlightDOMNode', () => {
15831583
}
15841584
});
15851585

1586+
it('should use late-arriving I/O debug info from rejected server promises to enhance component and owner stacks when aborting a prerender',async()=>{
1587+
letrejectHangingPromise;
1588+
1589+
asyncfunctionmakeHangingPromise(){
1590+
returnnewPromise((resolve,reject)=>{
1591+
rejectHangingPromise=reject;
1592+
});
1593+
}
1594+
1595+
asyncfunctiongetRoot(){
1596+
return{promise: makeHangingPromise()};
1597+
}
1598+
1599+
letstaticEndTime=-1;
1600+
conststaticChunks=[];
1601+
constdynamicChunks=[];
1602+
1603+
constserverAbortController=newAbortController();
1604+
awaitnewPromise(resolve=>{
1605+
setTimeout(async()=>{
1606+
conststream=ReactServerDOMServer.renderToPipeableStream(
1607+
getRoot(),
1608+
webpackMap,
1609+
{
1610+
filterStackFrame,
1611+
onError(err){
1612+
if(serverAbortController.signal.aborted){
1613+
return;
1614+
}
1615+
console.error(err);
1616+
},
1617+
},
1618+
);
1619+
serverAbortController.signal.addEventListener(
1620+
'abort',
1621+
()=>{
1622+
stream.abort(serverAbortController.signal.reason);
1623+
1624+
// Only reject the promise after the render is aborted
1625+
// so that it's no longer observable
1626+
rejectHangingPromise(
1627+
newError(
1628+
'Hanging promise was rejected after the prerender finished',
1629+
),
1630+
);
1631+
},
1632+
{once: true},
1633+
);
1634+
1635+
constpassThrough=newStream.PassThrough(streamOptions);
1636+
stream.pipe(passThrough);
1637+
1638+
passThrough.on('data',chunk=>{
1639+
if(staticEndTime<0){
1640+
staticChunks.push(chunk);
1641+
}else{
1642+
dynamicChunks.push(chunk);
1643+
}
1644+
});
1645+
1646+
passThrough.on('end',resolve);
1647+
});
1648+
setTimeout(()=>{
1649+
staticEndTime=performance.now()+performance.timeOrigin;
1650+
serverAbortController.abort();
1651+
});
1652+
});
1653+
1654+
constclientAbortController=newAbortController();
1655+
1656+
constserverStream=createReadableWithLateRelease(
1657+
staticChunks,
1658+
dynamicChunks,
1659+
clientAbortController.signal,
1660+
);
1661+
1662+
constresponse=awaitReactServerDOMClient.createFromNodeStream(
1663+
serverStream,
1664+
{
1665+
serverConsumerManifest: {
1666+
moduleMap: null,
1667+
moduleLoading: null,
1668+
},
1669+
},
1670+
{
1671+
// Debug info arriving after this end time will be ignored, e.g. the
1672+
// I/O info for the second dynamic data.
1673+
endTime: staticEndTime,
1674+
},
1675+
);
1676+
1677+
constresolvedPromise=Promise.resolve('hello');
1678+
functionClientDynamic(){
1679+
use(resolvedPromise);
1680+
use(response.promise);// unresolved ReactPromise (becomes rejected when we abort)
1681+
}
1682+
1683+
functionClientRoot(){
1684+
returnReact.createElement(
1685+
'html',
1686+
null,
1687+
React.createElement(
1688+
'body',
1689+
null,
1690+
React.createElement(
1691+
React.Suspense,
1692+
{fallback: 'Loading...'},
1693+
React.createElement(ClientDynamic),
1694+
),
1695+
),
1696+
);
1697+
}
1698+
1699+
letownerStack;
1700+
letcomponentStack;
1701+
1702+
const{prelude}=awaitnewPromise(resolve=>{
1703+
letresult;
1704+
1705+
setTimeout(()=>{
1706+
result=ReactDOMFizzStatic.prerenderToNodeStream(
1707+
React.createElement(ClientRoot),
1708+
{
1709+
signal: clientAbortController.signal,
1710+
onError(error,errorInfo){
1711+
componentStack=errorInfo.componentStack;
1712+
ownerStack=React.captureOwnerStack
1713+
? React.captureOwnerStack()
1714+
: null;
1715+
},
1716+
},
1717+
);
1718+
});
1719+
1720+
setTimeout(()=>{
1721+
clientAbortController.abort();
1722+
resolve(result);
1723+
});
1724+
});
1725+
1726+
constprerenderHTML=awaitreadResult(prelude);
1727+
1728+
expect(prerenderHTML).toContain('Loading...');
1729+
1730+
if(__DEV__){
1731+
expect(
1732+
normalizeCodeLocInfo(componentStack,{preserveLocation: true}),
1733+
).toBe(
1734+
'\n'+
1735+
' in ClientDynamic (ReactFlightDOMNode-test.js:1679:9)\n'+
1736+
' in Suspense\n'+
1737+
' in body\n'+
1738+
' in html\n'+
1739+
' in ClientRoot',
1740+
);
1741+
}else{
1742+
expect(
1743+
normalizeCodeLocInfo(componentStack,{preserveLocation: true}),
1744+
).toBe(
1745+
'\n'+
1746+
' in ClientDynamic (ReactFlightDOMNode-test.js:1679:9)\n'+
1747+
' in Suspense\n'+
1748+
' in body\n'+
1749+
' in html\n'+
1750+
' in ClientRoot',
1751+
);
1752+
}
1753+
1754+
if(__DEV__){
1755+
expect(ignoreListStack(ownerStack)).toBe(
1756+
'\n'+
1757+
gate(flags=>
1758+
flags.enableAsyncDebugInfo
1759+
? ' at ClientDynamic (./ReactFlightDOMNode-test.js:1680:9)\n'
1760+
: '',
1761+
)+
1762+
' at ClientRoot (./ReactFlightDOMNode-test.js:1693:21)',
1763+
);
1764+
}else{
1765+
expect(ownerStack).toBeNull();
1766+
}
1767+
});
1768+
15861769
functioncreateReadableWithLateRelease(initialChunks,lateChunks,signal){
15871770
// Create a new Readable and push all initial chunks immediately.
15881771
constreadable=newStream.Readable({...streamOptions,read(){}});

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ export function ensureSuspendableThenableStateDEV(
257257
constlastThenable=thenableState[thenableState.length-1];
258258
// Reset the last thenable back to pending.
259259
switch(lastThenable.status){
260-
case'fulfilled':
260+
case'fulfilled':{
261261
const previousThenableValue =lastThenable.value;
262262
// $FlowIgnore[method-unbinding] We rebind .then immediately.
263263
constpreviousThenableThen=lastThenable.then.bind(lastThenable);
@@ -274,14 +274,25 @@ export function ensureSuspendableThenableStateDEV(
274274
lastThenable.value=previousThenableValue;
275275
lastThenable.status='fulfilled';
276276
};
277-
case 'rejected':
277+
}
278+
case 'rejected': {
278279
constpreviousThenableReason=lastThenable.reason;
280+
// $FlowIgnore[method-unbinding] We rebind .then immediately.
281+
constpreviousThenableThen=lastThenable.then.bind(lastThenable);
279282
deletelastThenable.reason;
280283
delete(lastThenable: any).status;
284+
// We'll call .then again if we resuspend. Since we potentially corrupted
285+
// the internal state of unknown classes, we need to diffuse the potential
286+
// crash by replacing the .then method with a noop.
287+
// $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
288+
lastThenable.then=noop;
281289
return()=>{
290+
// $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
291+
lastThenable.then=previousThenableThen;
282292
lastThenable.reason=previousThenableReason;
283293
lastThenable.status='rejected';
284294
};
295+
}
285296
}
286297
returnnoop;
287298
}else{

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); } })(); })(); [Fizz] Fix crash when capturing the callsite of a stalled use() of a … · react/react@37fa36c · GitHub
Skip to content

Commit 37fa36c

Browse files
authored
[Fizz] Fix crash when capturing the callsite of a stalled use() of a Flight chunk that was rejected in the meantime (#36544)
`ensureSuspendableThenableStateDEV` patches `then` in fulfilled thenables to avoid triggering a custom thenable's `then` in an unexpected state. However, we weren't doing the same for rejected thenables. This affected `ReactPromise`, the type used for thenables passed from server to client. if a `ReactPromise` passed to `use` was pending during the render but became rejected between the abort and `pushSuspendedCallSiteOnComponentStack`, then `ReactPromise#then` would crash. (see the added test for a reprouction) This is because we were putting the ReactPromise into an invalid state: a `PendingChunk` expects to have a `value: null | Array<...>`, but we were deleting `value` altogether, and tgus hitting `TypeError: can't access property "push" of undefined` here: https://github.com/facebook/react/blob/75b0945b18f4a60c80c931fd8067d9c715957879/packages/react-client/src/ReactFlightClient.js#L309 Bypassing the suspended thenable's `then` avoids this crash.
1 parent 75b0945 commit 37fa36c

2 files changed

Lines changed: 196 additions & 2 deletions

File tree

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

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,6 +1583,189 @@ describe('ReactFlightDOMNode', () => {
15831583
}
15841584
});
15851585

1586+
it('should use late-arriving I/O debug info from rejected server promises to enhance component and owner stacks when aborting a prerender',async()=>{
1587+
letrejectHangingPromise;
1588+
1589+
asyncfunctionmakeHangingPromise(){
1590+
returnnewPromise((resolve,reject)=>{
1591+
rejectHangingPromise=reject;
1592+
});
1593+
}
1594+
1595+
asyncfunctiongetRoot(){
1596+
return{promise: makeHangingPromise()};
1597+
}
1598+
1599+
letstaticEndTime=-1;
1600+
conststaticChunks=[];
1601+
constdynamicChunks=[];
1602+
1603+
constserverAbortController=newAbortController();
1604+
awaitnewPromise(resolve=>{
1605+
setTimeout(async()=>{
1606+
conststream=ReactServerDOMServer.renderToPipeableStream(
1607+
getRoot(),
1608+
webpackMap,
1609+
{
1610+
filterStackFrame,
1611+
onError(err){
1612+
if(serverAbortController.signal.aborted){
1613+
return;
1614+
}
1615+
console.error(err);
1616+
},
1617+
},
1618+
);
1619+
serverAbortController.signal.addEventListener(
1620+
'abort',
1621+
()=>{
1622+
stream.abort(serverAbortController.signal.reason);
1623+
1624+
// Only reject the promise after the render is aborted
1625+
// so that it's no longer observable
1626+
rejectHangingPromise(
1627+
newError(
1628+
'Hanging promise was rejected after the prerender finished',
1629+
),
1630+
);
1631+
},
1632+
{once: true},
1633+
);
1634+
1635+
constpassThrough=newStream.PassThrough(streamOptions);
1636+
stream.pipe(passThrough);
1637+
1638+
passThrough.on('data',chunk=>{
1639+
if(staticEndTime<0){
1640+
staticChunks.push(chunk);
1641+
}else{
1642+
dynamicChunks.push(chunk);
1643+
}
1644+
});
1645+
1646+
passThrough.on('end',resolve);
1647+
});
1648+
setTimeout(()=>{
1649+
staticEndTime=performance.now()+performance.timeOrigin;
1650+
serverAbortController.abort();
1651+
});
1652+
});
1653+
1654+
constclientAbortController=newAbortController();
1655+
1656+
constserverStream=createReadableWithLateRelease(
1657+
staticChunks,
1658+
dynamicChunks,
1659+
clientAbortController.signal,
1660+
);
1661+
1662+
constresponse=awaitReactServerDOMClient.createFromNodeStream(
1663+
serverStream,
1664+
{
1665+
serverConsumerManifest: {
1666+
moduleMap: null,
1667+
moduleLoading: null,
1668+
},
1669+
},
1670+
{
1671+
// Debug info arriving after this end time will be ignored, e.g. the
1672+
// I/O info for the second dynamic data.
1673+
endTime: staticEndTime,
1674+
},
1675+
);
1676+
1677+
constresolvedPromise=Promise.resolve('hello');
1678+
functionClientDynamic(){
1679+
use(resolvedPromise);
1680+
use(response.promise);// unresolved ReactPromise (becomes rejected when we abort)
1681+
}
1682+
1683+
functionClientRoot(){
1684+
returnReact.createElement(
1685+
'html',
1686+
null,
1687+
React.createElement(
1688+
'body',
1689+
null,
1690+
React.createElement(
1691+
React.Suspense,
1692+
{fallback: 'Loading...'},
1693+
React.createElement(ClientDynamic),
1694+
),
1695+
),
1696+
);
1697+
}
1698+
1699+
letownerStack;
1700+
letcomponentStack;
1701+
1702+
const{prelude}=awaitnewPromise(resolve=>{
1703+
letresult;
1704+
1705+
setTimeout(()=>{
1706+
result=ReactDOMFizzStatic.prerenderToNodeStream(
1707+
React.createElement(ClientRoot),
1708+
{
1709+
signal: clientAbortController.signal,
1710+
onError(error,errorInfo){
1711+
componentStack=errorInfo.componentStack;
1712+
ownerStack=React.captureOwnerStack
1713+
? React.captureOwnerStack()
1714+
: null;
1715+
},
1716+
},
1717+
);
1718+
});
1719+
1720+
setTimeout(()=>{
1721+
clientAbortController.abort();
1722+
resolve(result);
1723+
});
1724+
});
1725+
1726+
constprerenderHTML=awaitreadResult(prelude);
1727+
1728+
expect(prerenderHTML).toContain('Loading...');
1729+
1730+
if(__DEV__){
1731+
expect(
1732+
normalizeCodeLocInfo(componentStack,{preserveLocation: true}),
1733+
).toBe(
1734+
'\n'+
1735+
' in ClientDynamic (ReactFlightDOMNode-test.js:1679:9)\n'+
1736+
' in Suspense\n'+
1737+
' in body\n'+
1738+
' in html\n'+
1739+
' in ClientRoot',
1740+
);
1741+
}else{
1742+
expect(
1743+
normalizeCodeLocInfo(componentStack,{preserveLocation: true}),
1744+
).toBe(
1745+
'\n'+
1746+
' in ClientDynamic (ReactFlightDOMNode-test.js:1679:9)\n'+
1747+
' in Suspense\n'+
1748+
' in body\n'+
1749+
' in html\n'+
1750+
' in ClientRoot',
1751+
);
1752+
}
1753+
1754+
if(__DEV__){
1755+
expect(ignoreListStack(ownerStack)).toBe(
1756+
'\n'+
1757+
gate(flags=>
1758+
flags.enableAsyncDebugInfo
1759+
? ' at ClientDynamic (./ReactFlightDOMNode-test.js:1680:9)\n'
1760+
: '',
1761+
)+
1762+
' at ClientRoot (./ReactFlightDOMNode-test.js:1693:21)',
1763+
);
1764+
}else{
1765+
expect(ownerStack).toBeNull();
1766+
}
1767+
});
1768+
15861769
functioncreateReadableWithLateRelease(initialChunks,lateChunks,signal){
15871770
// Create a new Readable and push all initial chunks immediately.
15881771
constreadable=newStream.Readable({...streamOptions,read(){}});

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ export function ensureSuspendableThenableStateDEV(
257257
constlastThenable=thenableState[thenableState.length-1];
258258
// Reset the last thenable back to pending.
259259
switch(lastThenable.status){
260-
case'fulfilled':
260+
case'fulfilled':{
261261
const previousThenableValue =lastThenable.value;
262262
// $FlowIgnore[method-unbinding] We rebind .then immediately.
263263
constpreviousThenableThen=lastThenable.then.bind(lastThenable);
@@ -274,14 +274,25 @@ export function ensureSuspendableThenableStateDEV(
274274
lastThenable.value=previousThenableValue;
275275
lastThenable.status='fulfilled';
276276
};
277-
case 'rejected':
277+
}
278+
case 'rejected': {
278279
constpreviousThenableReason=lastThenable.reason;
280+
// $FlowIgnore[method-unbinding] We rebind .then immediately.
281+
constpreviousThenableThen=lastThenable.then.bind(lastThenable);
279282
deletelastThenable.reason;
280283
delete(lastThenable: any).status;
284+
// We'll call .then again if we resuspend. Since we potentially corrupted
285+
// the internal state of unknown classes, we need to diffuse the potential
286+
// crash by replacing the .then method with a noop.
287+
// $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
288+
lastThenable.then=noop;
281289
return()=>{
290+
// $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
291+
lastThenable.then=previousThenableThen;
282292
lastThenable.reason=previousThenableReason;
283293
lastThenable.status='rejected';
284294
};
295+
}
285296
}
286297
returnnoop;
287298
}else{

0 commit comments

Comments
 (0)