Summary
A logged-in user loading an authenticated App Router page in a real browser (full HTML SSR) crashes under Perry with Next.js E394 — Internal Error: do not use legacy react-dom/server APIs — repeated as unhandledRejection: InvariantError, and the response stream never cleanly closes (browser spins "loading forever"; curl reports 500). Node renders the identical request 200.
E394 is a Next.js poison-pill stub: function a(){ throw Error("Internal Error: do not use legacy react-dom/server APIs...") } assigned to the legacy react-dom/server exports (renderToString, renderToStaticMarkup, renderToNodeStream, …). App Router must only call the streaming APIs. So under Perry, a react-dom/server module export is resolving to the throwing legacy stub instead of the real streaming function during the browser SSR of an authenticated page.
Reproduction (gscmaster, Next 16.1.1 standalone)
| request |
Perry |
Node |
GET /de/dashboard with Accept: text/html + browser UA, authenticated |
500 + E394/InvariantError flood |
200 |
GET /de/dashboard plain curl (no Accept/UA), authenticated |
200 (byte-identical to node) |
200 |
GET /de/dashboard with RSC: 1 (client nav), authenticated |
200 |
200 |
| public pages with browser UA |
200 |
200 |
So it is specific to the full-HTML SSR path of an authenticated, dynamic = "force-dynamic" page — exactly what a browser does on the post-login redirect. Plain curl and the RSC-only path take a different render route and succeed, which is why a 220-request curl soak passed while a real browser login hangs.
Likely in the react-dom/server (Fizz) HTML-shell render, possibly interacting with the PPR/postpone-resume path (the bundle also carries Invariant: isDynamicPostpone misidentified a postpone reason), cf. the module-export "value form ≠ static form" resolution family (#6667/#6668) and PPR resume #5437.
Next step
Wrap the bundle's E394 stub a() to log a stack when called → identify exactly which react-dom/server export is mis-resolved to the stub, then fix Perry's resolution of that export (probable: the streaming API being read as a value/namespace member and getting the legacy alias).
Observed on main @ 3fd379f, macOS arm64. Workaround unknown; the RSC/plain-fetch path is unaffected.
Summary
A logged-in user loading an authenticated App Router page in a real browser (full HTML SSR) crashes under Perry with Next.js E394 —
Internal Error: do not use legacy react-dom/server APIs— repeated asunhandledRejection: InvariantError, and the response stream never cleanly closes (browser spins "loading forever"; curl reports 500). Node renders the identical request 200.E394 is a Next.js poison-pill stub:
function a(){ throw Error("Internal Error: do not use legacy react-dom/server APIs...") }assigned to the legacyreact-dom/serverexports (renderToString,renderToStaticMarkup,renderToNodeStream, …). App Router must only call the streaming APIs. So under Perry, areact-dom/servermodule export is resolving to the throwing legacy stub instead of the real streaming function during the browser SSR of an authenticated page.Reproduction (gscmaster, Next 16.1.1 standalone)
GET /de/dashboardwithAccept: text/html+ browser UA, authenticatedGET /de/dashboardplain curl (no Accept/UA), authenticatedGET /de/dashboardwithRSC: 1(client nav), authenticatedSo it is specific to the full-HTML SSR path of an authenticated,
dynamic = "force-dynamic"page — exactly what a browser does on the post-login redirect. Plain curl and the RSC-only path take a different render route and succeed, which is why a 220-request curl soak passed while a real browser login hangs.Likely in the react-dom/server (Fizz) HTML-shell render, possibly interacting with the PPR/postpone-resume path (the bundle also carries
Invariant: isDynamicPostpone misidentified a postpone reason), cf. the module-export "value form ≠ static form" resolution family (#6667/#6668) and PPR resume #5437.Next step
Wrap the bundle's E394 stub
a()to log a stack when called → identify exactly whichreact-dom/serverexport is mis-resolved to the stub, then fix Perry's resolution of that export (probable: the streaming API being read as a value/namespace member and getting the legacy alias).Observed on main @ 3fd379f, macOS arm64. Workaround unknown; the RSC/plain-fetch path is unaffected.