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-
letcleanupCurrentFork: (()=>Promise<void>)|undefined
201200
// Whatever is serving the app right now: this process, then each fork in turn.
202201
letcloseCurrent=close
203202
letcurrentPid=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
awaitfork?.close()
280280
constdetail=errorinstanceofError ? 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
constclosePrevious=handover ? closeCurrent : undefined
293-
cleanupCurrentFork=fork.close
294295
closeCurrent=fork.close
295296
currentPid=fork.pid??currentPid
296297
awaitclosePrevious?.()
@@ -303,7 +304,9 @@ const command = defineCommand({
303304
onRestart(restart)
304305

305306
asyncfunctioncloseAll(){
306-
awaitcleanupCurrentFork?.()
307+
if(closeCurrent!==close){
308+
awaitcloseCurrent()
309+
}
307310
awaitclose()
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(errorasNodeJS.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
exportfunctionisReusePortSupported(): 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
exportinterfaceActiveFork{
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
privatetrackServing(fork: PooledFork): Promise<void>{
125129
returnnewPromise<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
privatecreateFork(): PooledFork{
174182
constchildProc=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)