// Start CPU profile if it wasn't already started. import './cpu-profile' import { getNetworkHost } from '../../lib/get-network-host' if (performance.getEntriesByName('next-start').length === 0) { performance.mark('next-start') } import '../next' import '../require-hook' import type { IncomingMessage, ServerResponse } from 'http' import type { SelfSignedCertificate } from '../../lib/mkcert' import type { WorkerRequestHandler, WorkerUpgradeHandler } from './types' import fs from 'fs' import v8 from 'v8' import path from 'path' import http from 'http' import https from 'https' import os from 'os' import { exec } from 'child_process' import Watchpack from 'next/dist/compiled/watchpack' import * as Log from '../../build/output/log' import setupDebug from 'next/dist/compiled/debug' import { RESTART_EXIT_CODE } from './utils' import { formatHostname } from './format-hostname' import { initialize } from './router-server' import { CONFIG_FILES, PHASE_DEVELOPMENT_SERVER, } from '../../shared/lib/constants' import { getEnvInfo, logExperimentalInfo, logStartInfo } from './app-info-log' import { validateTurboNextConfig } from '../../lib/turbopack-warning' import { type Span, trace, flushAllTraces } from '../../trace' import { isIPv6 } from './is-ipv6' import { AsyncCallbackSet } from './async-callback-set' import type { NextServer } from '../next' import { durationToString } from '../../build/duration-to-string' const debug = setupDebug('next:start-server') let startServerSpan: Span | undefined /** * Get the process ID (PID) of the process using the specified port */ async function getProcessIdUsingPort(port: number): Promise { const timeoutMs = 250 const processLookupController = new AbortController() const pidPromise = new Promise((resolve) => { const handleError = (error: Error) => { debug('Failed to get process ID for port', port, error) resolve(null) } try { // Use lsof on Unix-like systems (macOS, Linux) if (process.platform !== 'win32') { exec( `lsof -ti:${port} -sTCP:LISTEN`, { signal: processLookupController.signal }, (error, stdout) => { if (error) { handleError(error) return } // `-sTCP` will ensure there's only one port, clean up output const pid = stdout.trim() resolve(pid || null) } ) } else { // Use netstat on Windows exec( `netstat -ano | findstr /C:":${port} " | findstr LISTENING`, { signal: processLookupController.signal }, (error, stdout) => { if (error) { handleError(error) return } // Clean up output and extract PID const cleanOutput = stdout.replace(/\s+/g, ' ').trim() if (cleanOutput) { const lines = cleanOutput.split('\n') const firstLine = lines[0].trim() if (firstLine) { const parts = firstLine.split(' ') const pid = parts[parts.length - 1] resolve(pid || null) } else { resolve(null) } } else { resolve(null) } } ) } } catch (cause) { handleError( new Error('Unexpected error during process lookup', { cause }) ) } }) const timeoutId = setTimeout(() => { processLookupController.abort( `PID detection timed out after ${timeoutMs}ms for port ${port}.` ) }, timeoutMs) pidPromise.finally(() => clearTimeout(timeoutId)) return pidPromise } export interface StartServerOptions { dir: string port: number isDev: boolean hostname?: string allowRetry?: boolean customServer?: boolean minimalMode?: boolean keepAliveTimeout?: number // this is dev-server only selfSignedCertificate?: SelfSignedCertificate } export async function getRequestHandlers({ dir, port, isDev, onDevServerCleanup, server, hostname, minimalMode, keepAliveTimeout, experimentalHttpsServer, quiet, }: { dir: string port: number isDev: boolean onDevServerCleanup: ((listener: () => Promise) => void) | undefined server?: import('http').Server hostname?: string minimalMode?: boolean keepAliveTimeout?: number experimentalHttpsServer?: boolean quiet?: boolean }): ReturnType { return initialize({ dir, port, hostname, onDevServerCleanup, dev: isDev, minimalMode, server, keepAliveTimeout, experimentalHttpsServer, startServerSpan, quiet, }) } export type StartServerResult = { distDir: string } export async function startServer( serverOptions: StartServerOptions ): Promise { const { dir, isDev, hostname, minimalMode, allowRetry, keepAliveTimeout, selfSignedCertificate, } = serverOptions let { port } = serverOptions process.title = `next-server (v${process.env.__NEXT_VERSION})` let handlersReady = () => {} let handlersError = () => {} let handlersPromise: Promise | undefined = new Promise( (resolve, reject) => { handlersReady = resolve handlersError = reject } ) let requestHandler: WorkerRequestHandler = async ( req: IncomingMessage, res: ServerResponse ): Promise => { if (handlersPromise) { await handlersPromise return requestHandler(req, res) } throw new Error('Invariant request handler was not setup') } let upgradeHandler: WorkerUpgradeHandler = async ( req, socket, head ): Promise => { if (handlersPromise) { await handlersPromise return upgradeHandler(req, socket, head) } throw new Error('Invariant upgrade handler was not setup') } let nextServer: NextServer | undefined // setup server listener as fast as possible if (selfSignedCertificate && !isDev) { throw new Error( 'Using a self signed certificate is only supported with `next dev`.' ) } async function requestListener(req: IncomingMessage, res: ServerResponse) { try { if (handlersPromise) { await handlersPromise handlersPromise = undefined } await requestHandler(req, res) } catch (err) { res.statusCode = 500 res.end('Internal Server Error') Log.error(`Failed to handle request for ${req.url}`) console.error(err) } finally { if (isDev) { if ( v8.getHeapStatistics().used_heap_size > 0.8 * v8.getHeapStatistics().heap_size_limit ) { Log.warn( `Server is approaching the used memory threshold, restarting...` ) trace('server-restart-close-to-memory-threshold', undefined, { 'memory.heapSizeLimit': String( v8.getHeapStatistics().heap_size_limit ), 'memory.heapUsed': String(v8.getHeapStatistics().used_heap_size), }).stop() await flushAllTraces() process.exit(RESTART_EXIT_CODE) } } } } const server = selfSignedCertificate ? https.createServer( { key: fs.readFileSync(selfSignedCertificate.key), cert: fs.readFileSync(selfSignedCertificate.cert), }, requestListener ) : http.createServer(requestListener) if (keepAliveTimeout) { server.keepAliveTimeout = keepAliveTimeout } server.on('upgrade', async (req, socket, head) => { try { await upgradeHandler(req, socket, head) } catch (err) { socket.destroy() Log.error(`Failed to handle request for ${req.url}`) console.error(err) } }) let portRetryCount = 0 const originalPort = port server.on('error', (err: NodeJS.ErrnoException) => { if ( allowRetry && port && isDev && err.code === 'EADDRINUSE' && portRetryCount < 10 ) { port += 1 portRetryCount += 1 server.listen(port, hostname) } else { Log.error(`Failed to start server`) console.error(err) process.exit(1) } }) let cleanupListeners = isDev ? new AsyncCallbackSet() : undefined const distDir = await new Promise((resolve) => { server.on('listening', async () => { const addr = server.address() const actualHostname = formatHostname( typeof addr === 'object' ? addr?.address || hostname || 'localhost' : addr ) const formattedHostname = !hostname || actualHostname === '0.0.0.0' ? 'localhost' : actualHostname === '[::]' ? '[::1]' : formatHostname(hostname) port = typeof addr === 'object' ? addr?.port || port : port if (portRetryCount) { const pid = await getProcessIdUsingPort(originalPort) if (pid) { Log.warn( `Port ${originalPort} is in use by process ${pid}, using available port ${port} instead.` ) } else { Log.warn( `Port ${originalPort} is in use by an unknown process, using available port ${port} instead.` ) } } const networkHostname = hostname ?? getNetworkHost(isIPv6(actualHostname) ? 'IPv6' : 'IPv4') const protocol = selfSignedCertificate ? 'https' : 'http' const networkUrl = networkHostname ? `${protocol}://${formatHostname(networkHostname)}:${port}` : null const appUrl = `${protocol}://${formattedHostname}:${port}` // Store the selected port to: // - expose it to render workers // - re-use it for automatic dev server restarts with a randomly selected port process.env.PORT = port + '' process.env.__NEXT_PRIVATE_ORIGIN = appUrl // Set experimental HTTPS flag for metadata resolution if (selfSignedCertificate) { process.env.__NEXT_EXPERIMENTAL_HTTPS = '1' } // Get env info first (fast, doesn't require config) const envInfo = isDev ? getEnvInfo(dir) : undefined // Log basic startup info immediately (before loading config) logStartInfo({ networkUrl, appUrl, envInfo, logBundler: isDev, }) // Calculate and log "Ready in X" before loading config // so it reflects actual framework startup time const startTime = parseInt(process.env.NEXT_PRIVATE_START_TIME || '0', 10) const endTime = Date.now() const startServerProcessDurationMs = endTime - startTime const formattedStartDuration = durationToString( startServerProcessDurationMs / 1000 ) Log.event(`Ready in ${formattedStartDuration}`) try { let cleanupStarted = false let closeUpgraded: (() => void) | null = null const cleanup = () => { if (cleanupStarted) { // We can get duplicate signals, e.g. when `ctrl+c` is used in an // interactive shell (i.e. bash, zsh), the shell will recursively // send SIGINT to children. The parent `next-dev` process will also // send us SIGINT. return } cleanupStarted = true ;(async () => { debug('start-server process cleanup') // first, stop accepting new connections and finish pending requests, // because they might affect `nextServer.close()` (e.g. by scheduling an `after`) await new Promise((res) => { server.close((err) => { if (err) console.error(err) res() }) if (isDev) { server.closeAllConnections() closeUpgraded?.() } }) // now that no new requests can come in, clean up the rest await Promise.all([ nextServer?.close().catch(console.error), cleanupListeners?.runAll().catch(console.error), ]) // Flush telemetry if this is a dev server if (isDev) { try { const { traceGlobals } = require('../../trace/shared') as typeof import('../../trace/shared') const telemetry = traceGlobals.get('telemetry') as | InstanceType< typeof import('../../telemetry/storage').Telemetry > | undefined if (telemetry) { // Use flushDetached to avoid blocking process exit // Each process writes to a unique file (_events_${pid}.json) // to avoid race conditions with the parent process telemetry.flushDetached('dev', dir) } } catch (_) { // Ignore telemetry errors during cleanup } } debug('start-server process cleanup finished') process.exit(0) })() } // Make sure commands gracefully respect termination signals (e.g. from Docker) // Allow the graceful termination to be manually configurable if (!process.env.NEXT_MANUAL_SIG_HANDLE) { process.on('SIGINT', cleanup) process.on('SIGTERM', cleanup) } // Now load config via getRequestHandlers (single loadConfig call) const initResult = await getRequestHandlers({ dir, port, isDev, onDevServerCleanup: cleanupListeners ? cleanupListeners.add.bind(cleanupListeners) : undefined, server, hostname, minimalMode, keepAliveTimeout, experimentalHttpsServer: !!selfSignedCertificate, }) requestHandler = initResult.requestHandler upgradeHandler = initResult.upgradeHandler nextServer = initResult.server closeUpgraded = initResult.closeUpgraded // Log experimental features after config is loaded if (isDev) { logExperimentalInfo({ experimentalFeatures: initResult.experimentalFeatures, cacheComponents: initResult.cacheComponents, }) } handlersReady() if (process.env.TURBOPACK && isDev) { await validateTurboNextConfig({ dir: serverOptions.dir, configPhase: PHASE_DEVELOPMENT_SERVER, }) } resolve(initResult.distDir) } catch (err) { // fatal error if we can't setup handlersError() console.error(err) process.exit(1) } }) server.listen(port, hostname) }) if (isDev) { function watchConfigFiles( dirToWatch: string, onChange: (filename: string) => void ) { const wp = new Watchpack() wp.watch({ files: CONFIG_FILES.map((file) => path.join(dirToWatch, file)), }) wp.on('change', onChange) } watchConfigFiles(dir, async (filename) => { if (process.env.__NEXT_DISABLE_MEMORY_WATCHER) { Log.info( `Detected change, manual restart required due to '__NEXT_DISABLE_MEMORY_WATCHER' usage` ) return } Log.warn( `Found a change in ${path.basename( filename )}. Restarting the server to apply the changes...` ) process.exit(RESTART_EXIT_CODE) }) } return { distDir } } if (process.env.NEXT_PRIVATE_WORKER && process.send) { process.addListener('message', async (msg: any) => { if ( msg && typeof msg === 'object' && msg.nextWorkerOptions && process.send ) { startServerSpan = trace('start-dev-server', undefined, { cpus: String(os.cpus().length), platform: os.platform(), 'memory.freeMem': String(os.freemem()), 'memory.totalMem': String(os.totalmem()), 'memory.heapSizeLimit': String(v8.getHeapStatistics().heap_size_limit), }) const result = await startServerSpan.traceAsyncFn(() => startServer(msg.nextWorkerOptions) ) const memoryUsage = process.memoryUsage() startServerSpan.setAttribute('memory.rss', String(memoryUsage.rss)) startServerSpan.setAttribute( 'memory.heapTotal', String(memoryUsage.heapTotal) ) startServerSpan.setAttribute( 'memory.heapUsed', String(memoryUsage.heapUsed) ) process.send({ nextServerReady: true, port: process.env.PORT, distDir: result.distDir, }) } }) process.send({ nextWorkerReady: true }) }