Skip to content

Commit 8d7eaae

Browse files
authored
[devtools] Add an endpoint to poll for server status (#80005)
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.
1 parent f9084e3 commit 8d7eaae

File tree

1 file changed

+42
-6
lines changed

1 file changed

+42
-6
lines changed

packages/next/src/client/components/react-dev-overlay/server/restart-dev-server-middleware.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,23 @@ export function getRestartDevServerMiddleware({
1515
telemetry,
1616
turbopackProject,
1717
}: RestartDevServerMiddlewareConfig) {
18-
return async function (
18+
/**
19+
* Some random value between 1 and Number.MAX_SAFE_INTEGER (inclusive). The same value is returned
20+
* on every call to `__nextjs_server_status` until the server is restarted.
21+
*
22+
* Can be used to determine if two server status responses are from the same process or a
23+
* different (restarted) process.
24+
*/
25+
const executionId: number =
26+
Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) + 1
27+
28+
async function handleRestartRequest(
1929
req: IncomingMessage,
2030
res: ServerResponse,
21-
next: () => void
22-
): Promise<void> {
23-
const { pathname, searchParams } = new URL(`http://n${req.url}`)
24-
if (pathname !== '/__nextjs_restart_dev' || req.method !== 'POST') {
25-
return next()
31+
searchParams: URLSearchParams
32+
) {
33+
if (req.method !== 'POST') {
34+
return middlewareResponse.methodNotAllowed(res)
2635
}
2736

2837
const invalidatePersistentCache = searchParams.has(
@@ -48,4 +57,31 @@ export function getRestartDevServerMiddleware({
4857

4958
return middlewareResponse.noContent(res)
5059
}
60+
61+
async function handleServerStatus(req: IncomingMessage, res: ServerResponse) {
62+
if (req.method !== 'GET') {
63+
return middlewareResponse.methodNotAllowed(res)
64+
}
65+
66+
return middlewareResponse.json(res, {
67+
executionId,
68+
})
69+
}
70+
71+
return async function (
72+
req: IncomingMessage,
73+
res: ServerResponse,
74+
next: () => void
75+
): Promise<void> {
76+
const { pathname, searchParams } = new URL(`http://n${req.url}`)
77+
78+
switch (pathname) {
79+
case '/__nextjs_restart_dev':
80+
return await handleRestartRequest(req, res, searchParams)
81+
case '/__nextjs_server_status':
82+
return await handleServerStatus(req, res)
83+
default:
84+
return next()
85+
}
86+
}
5187
}

0 commit comments

Comments
 (0)