Repository: vercel/next.js
Pull request: [devtools] Add an endpoint to poll for server status

After the server restarts, the frontend needs some way to determine when the server is back up. This provides an endpoint that can be polled.

## Test Plan

Tested by running

```
curl -v http://localhost:3000/__nextjs_server_status
```

a few times and noticing the `executionId` is stable.

Restart the server with

```
curl -v --request POST --header "Content-Type: application/json" --data '{}' http://localhost:3000/__nextjs_restart_dev
```

and notice that the `executionId` change next time I poll `/__nextjs_server_status`.

## Explanation for the `executionId`

**Note:** The feedback was that we don't really need to handle this yet, but most of the complexity is in handling this on the frontend. It's only 3 lines of code and some comments on the backend to return a random value. So this implements it, but it's up to frontend if it wants to use the `executionId` or not.

> I'm trying to work out the polling endpoint to see when the server is back up. Maybe I'm overthinking this, but I'm a bit concerned about a race condition:
>
> * You send the request to shut down the server. You can't reliably wait for it to complete because the exiting server might cause the request to hang (though typically on a sane machine it should close the TCP connection).
> * The shut down request is actually still processing but it's super slow for some reason (e.g. it's sending telemetry).
> * Assuming it's hanging, after a second you start sending requests to the polling endpoint to see when the server is back up.
> * The server hasn't actually shut down yet, but you think it's back up, so you refresh the page too early.
>
> So my thought is we need some identifier of the current executing dev server returned by the poll endpoint that changes upon restart, like a nonce. This could be a timestamp of the server startup or just a large random number that's cached in-memory by the server.
>
> So then how do we pass that to the client? We probably don't want to do that using the bundler define plugin, as it would change on every startup and might unnecessarily invalidate caches (assuming you're not deleting the persistent cache anyways). So I'm thinking the restart flow could be:
>
> 1. We call the poll endpoint to get the nonce / execution id. We await this fetch.
> 2. Once we have the nonce, we call the restart endpoint without awaiting it (under the assumption it could hang).
> 3. We poll the endpoint and only restart once we get a response with a different nonce / execution id value.
>
> That's a little ugly because now it's two requests to initiate the restart, but the poll endpoint should be really quick and it's to localhost, so there's not much added latency.

Closes PACK-4744

Changed files:
filename: packages/next/src/client/components/react-dev-overlay/server/restart-dev-server-middleware.ts
patch: @@ -15,14 +15,23 @@ export function getRestartDevServerMiddleware({
   telemetry,
   turbopackProject,
 }: RestartDevServerMiddlewareConfig) {
-  return async function (
+  /**
+   * Some random value between 1 and Number.MAX_SAFE_INTEGER (inclusive). The same value is returned
+   * on every call to `__nextjs_server_status` until the server is restarted.
+   *
+   * Can be used to determine if two server status responses are from the same process or a
+   * different (restarted) process.
+   */
+  const executionId: number =
+    Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) + 1
+
+  async function handleRestartRequest(
     req: IncomingMessage,
     res: ServerResponse,
-    next: () => void
-  ): Promise<void> {
-    const { pathname, searchParams } = new URL(`http://n${req.url}`)
-    if (pathname !== '/__nextjs_restart_dev' || req.method !== 'POST') {
-      return next()
+    searchParams: URLSearchParams
+  ) {
+    if (req.method !== 'POST') {
+      return middlewareResponse.methodNotAllowed(res)
     }
 
     const invalidatePersistentCache = searchParams.has(
@@ -48,4 +57,31 @@ export function getRestartDevServerMiddleware({
 
     return middlewareResponse.noContent(res)
   }
+
+  async function handleServerStatus(req: IncomingMessage, res: ServerResponse) {
+    if (req.method !== 'GET') {
+      return middlewareResponse.methodNotAllowed(res)
+    }
+
+    return middlewareResponse.json(res, {
+      executionId,
+    })
+  }
+
+  return async function (
+    req: IncomingMessage,
+    res: ServerResponse,
+    next: () => void
+  ): Promise<void> {
+    const { pathname, searchParams } = new URL(`http://n${req.url}`)
+
+    switch (pathname) {
+      case '/__nextjs_restart_dev':
+        return await handleRestartRequest(req, res, searchParams)
+      case '/__nextjs_server_status':
+        return await handleServerStatus(req, res)
+      default:
+        return next()
+    }
+  }
 }