Skip to content

Commit ae9be91

Browse files
committed
fix(dev): handle a rejected reusePort option and a failed cold restart
1 parent 169f917 commit ae9be91

3 files changed

Lines changed: 37 additions & 16 deletions

File tree

packages/nuxt-cli/src/commands/dev.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,6 @@ const command = defineCommand({
197197
})
198198

199199
// On hard restart, use a fork from the pool
200-
let cleanupCurrentFork: (() => Promise<void>) | undefined
201200
// Whatever is serving the app right now: this process, then each fork in turn.
202201
let closeCurrent = close
203202
let currentPid = process.pid
@@ -255,8 +254,9 @@ const command = defineCommand({
255254
: undefined,
256255
onMessage: (message) => {
257256
// Handle IPC messages from the fork
258-
if (message.type === 'nuxt:internal:dev:ready') {
259-
if (startTime) {
257+
if (message.type === 'nuxt:internal:dev:ready' || message.type === 'nuxt:internal:dev:loading:error') {
258+
serving = true
259+
if (message.type === 'nuxt:internal:dev:ready' && startTime) {
260260
debug(`Dev server ready for connections in ${Date.now() - startTime}ms`)
261261
}
262262
}
@@ -278,19 +278,20 @@ const command = defineCommand({
278278
catch (error) {
279279
await fork?.close()
280280
const detail = error instanceof Error ? error.message : String(error)
281-
logger.error(handover
282-
? `Could not restart the dev server, keeping the current one: ${detail}`
283-
: `Could not restart the dev server: ${detail}`)
284-
if (handover) {
285-
onRestart(restart)
281+
if (!handover) {
282+
// The outgoing server is already closed, so there is nothing left to
283+
// serve the app and no watcher left to trigger another attempt.
284+
logger.error(`Could not restart the dev server: ${detail}`)
285+
process.exit(1)
286286
}
287+
logger.error(`Could not restart the dev server, keeping the current one: ${detail}`)
288+
onRestart(restart)
287289
return
288290
}
289291

290292
serving = true
291293
fork.promote()
292294
const closePrevious = handover ? closeCurrent : undefined
293-
cleanupCurrentFork = fork.close
294295
closeCurrent = fork.close
295296
currentPid = fork.pid ?? currentPid
296297
await closePrevious?.()
@@ -303,7 +304,9 @@ const command = defineCommand({
303304
onRestart(restart)
304305

305306
async function closeAll() {
306-
await cleanupCurrentFork?.()
307+
if (closeCurrent !== close) {
308+
await closeCurrent()
309+
}
307310
await close()
308311
}
309312

packages/nuxt-cli/src/dev/listen.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -310,10 +310,18 @@ function bindServer(server: HttpServer, port: number, hostname: string, reusePor
310310
reject(error)
311311
}
312312
server.once('error', onError)
313-
server.listen({ port, host: hostname || undefined, reusePort, exclusive: !reusePort }, () => {
313+
try {
314+
server.listen({ port, host: hostname || undefined, reusePort, exclusive: !reusePort }, () => {
315+
server.removeListener('error', onError)
316+
resolve()
317+
})
318+
}
319+
catch (error) {
320+
// Runtimes that do not know the option reject it before the bind, throwing
321+
// rather than emitting `error`.
314322
server.removeListener('error', onError)
315-
resolve()
316-
})
323+
onError(error as NodeJS.ErrnoException)
324+
}
317325
})
318326
}
319327

@@ -331,7 +339,9 @@ let reusePortSupport: Promise<boolean> | undefined
331339
/**
332340
* Whether two sockets can bind the same port at once via `SO_REUSEPORT`, probed
333341
* on an ephemeral port. A single successful bind is not enough: some platforms
334-
* accept the option and still reject the second socket. Cached per process.
342+
* accept the option and still reject the second socket. Support is a property of
343+
* the platform rather than of an address, so the loopback probe stands in for
344+
* whichever hostname the dev server ends up binding. Cached per process.
335345
*/
336346
export function isReusePortSupported(): Promise<boolean> {
337347
reusePortSupport ??= (async () => {

packages/nuxt-cli/src/dev/pool.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ interface PooledFork {
2424

2525
export interface ActiveFork {
2626
pid?: number
27-
/** Resolves once the fork reports it is serving requests, rejects if it dies first. */
27+
/**
28+
* Resolves once the fork holds the listener, whether the app loaded or the
29+
* error page is being served, and rejects if it dies before that.
30+
*/
2831
serving: Promise<void>
2932
/** Promote the fork so that a later crash takes the dev session down. */
3033
promote: () => void
@@ -119,7 +122,8 @@ export class ForkPool {
119122

120123
/**
121124
* Resolves when the fork has bound its listener and is answering requests, so
122-
* the caller can keep the outgoing server up until then.
125+
* the caller can keep the outgoing server up until then. A load failure counts:
126+
* the fork is serving an error page and owns the port either way.
123127
*/
124128
private trackServing(fork: PooledFork): Promise<void> {
125129
return new Promise<void>((resolve, reject) => {
@@ -170,6 +174,10 @@ export class ForkPool {
170174
this.pool.push(fork)
171175
}
172176

177+
/**
178+
* `ready` always settles, rejecting if the fork exits at any point, so every
179+
* caller has to keep a rejection handler attached to it.
180+
*/
173181
private createFork(): PooledFork {
174182
const childProc = fork(globalThis.__nuxt_cli__.devEntry!, this.rawArgs, {
175183
// The inspector is opened by the fork that actually serves the app (see

0 commit comments

Comments
 (0)