Repository: vercel/next.js
Pull request: Avoid duplicate WebSocket connection for global error pages

When we're rendering a global error page (something with `<html id="__next_error__">`), we are wrapping the root element in a component that renders a top-level dev overlay to show the global error. The function that's wrapping this element was previously creating an unnecessary second WebSocket connection to handle the case where the error is removed by editing a server component, and the page is subsequently reloaded. However, this can be handled by the first WebSocket connection that's created in the root element just fine, we just need to ensure that the reload is also triggered when `document.documentElement.id` is `'__next_error__'`, which is the same [condition we use for rendering the wrapper](https://github.com/vercel/next.js/blob/a0b7a725a1b424473940fe3111842ae92d918a25/packages/next/src/client/app-index.tsx#L275).

This change is covered by existing tests, e.g. `test/development/app-dir/missing-required-html-tags/index.test.ts`.

Changed files:
filename: packages/next/src/client/app-index.tsx
patch: @@ -276,11 +276,13 @@ export function hydrate(
     let element = reactEl
     // Server rendering failed, fall back to client-side rendering
     if (process.env.NODE_ENV !== 'production') {
-      const { createRootLevelDevOverlayElement } =
+      const { RootLevelDevOverlayElement } =
         require('../next-devtools/userspace/app/client-entry') as typeof import('../next-devtools/userspace/app/client-entry')
 
       // Note this won't cause hydration mismatch because we are doing CSR w/o hydration
-      element = createRootLevelDevOverlayElement(element)
+      element = (
+        <RootLevelDevOverlayElement>{element}</RootLevelDevOverlayElement>
+      )
     }
 
     ReactDOMClient.createRoot(appElement, reactRootOptions).render(element)

filename: packages/next/src/client/dev/hot-reloader/app/hot-reloader-app.tsx
patch: @@ -391,7 +391,10 @@ function processMessage(
       // server with any subsequent requests.
       document.cookie = `${NEXT_HMR_REFRESH_HASH_COOKIE}=${obj.hash};path=/`
 
-      if (RuntimeErrorHandler.hadRuntimeError) {
+      if (
+        RuntimeErrorHandler.hadRuntimeError ||
+        document.documentElement.id === '__next_error__'
+      ) {
         if (reloading) return
         reloading = true
         return window.location.reload()

filename: packages/next/src/next-devtools/userspace/app/client-entry.tsx
patch: @@ -1,36 +1,17 @@
 import React from 'react'
-import { getSocketUrl } from '../../../client/dev/hot-reloader/get-socket-url'
-import { HMR_ACTIONS_SENT_TO_BROWSER } from '../../../server/dev/hot-reloader-types'
 import DefaultGlobalError from '../../../client/components/builtin/global-error'
 import { AppDevOverlayErrorBoundary } from './app-dev-overlay-error-boundary'
 
-// if an error is thrown while rendering an RSC stream, this will catch it in dev
-// and show the error overlay
-export function createRootLevelDevOverlayElement(reactEl: React.ReactElement) {
-  const socketUrl = getSocketUrl(process.env.__NEXT_ASSET_PREFIX || '')
-  const socket = new window.WebSocket(`${socketUrl}/_next/webpack-hmr`)
-
-  // add minimal "hot reload" support for RSC errors
-  const handler = (event: MessageEvent) => {
-    let obj
-    try {
-      obj = JSON.parse(event.data)
-    } catch {}
-
-    if (!obj || !('action' in obj)) {
-      return
-    }
-
-    if (obj.action === HMR_ACTIONS_SENT_TO_BROWSER.SERVER_COMPONENT_CHANGES) {
-      window.location.reload()
-    }
-  }
-
-  socket.addEventListener('message', handler)
-
+// If an error is thrown while rendering an RSC stream, this will catch it in
+// dev and show the error overlay.
+export function RootLevelDevOverlayElement({
+  children,
+}: {
+  children: React.ReactNode
+}) {
   return (
     <AppDevOverlayErrorBoundary globalError={[DefaultGlobalError, null]}>
-      {reactEl}
+      {children}
     </AppDevOverlayErrorBoundary>
   )
 }

filename: packages/next/src/server/stream-utils/node-web-streams-helper.ts
patch: @@ -636,7 +636,7 @@ export function createRootLayoutValidatorStream(): TransformStream<
                 .map((c) => `<${c}>`)
                 .join(
                   missingTags.length > 1 ? ' and ' : ''
-                )} tags in the root layout.\nRead more at https://nextjs.org/docs/messages/missing-root-layout-tags""
+                )} tags in the root layout.\nRead more at https://nextjs.org/docs/messages/missing-root-layout-tags"
               data-next-error-digest="${MISSING_ROOT_TAGS_ERROR}"
               data-next-error-stack=""
             ></template>