next
Version:
The React Framework
364 lines (363 loc) • 16.5 kB
JavaScript
import "../node-polyfill-fetch";
import http from "http";
import { isIPv6 } from "net";
import { initialEnv } from "@next/env";
import * as Log from "../../build/output/log";
import setupDebug from "next/dist/compiled/debug";
import { splitCookiesString, toNodeOutgoingHttpHeaders } from "../web/utils";
import { getCloneableBody } from "../body-streams";
import { filterReqHeaders, ipcForbiddenHeaders } from "./server-ipc/utils";
import setupCompression from "next/dist/compiled/compression";
import { normalizeRepeatedSlashes } from "../../shared/lib/utils";
import { invokeRequest } from "./server-ipc/invoke-request";
import { isAbortError, pipeReadable } from "../pipe-readable";
import { genRouterWorkerExecArgv, getDebugPort, getNodeOptionsWithoutInspect } from "./utils";
import { signalFromNodeResponse } from "../web/spec-extension/adapters/next-request";
const debug = setupDebug("next:start-server");
export const checkIsNodeDebugging = ()=>{
var _process_env_NODE_OPTIONS, _process_env_NODE_OPTIONS1;
let isNodeDebugging = !!(process.execArgv.some((localArg)=>localArg.startsWith("--inspect")) || ((_process_env_NODE_OPTIONS = process.env.NODE_OPTIONS) == null ? void 0 : _process_env_NODE_OPTIONS.match == null ? void 0 : _process_env_NODE_OPTIONS.match(/--inspect(=\S+)?( |$)/)));
if (process.execArgv.some((localArg)=>localArg.startsWith("--inspect-brk")) || ((_process_env_NODE_OPTIONS1 = process.env.NODE_OPTIONS) == null ? void 0 : _process_env_NODE_OPTIONS1.match == null ? void 0 : _process_env_NODE_OPTIONS1.match(/--inspect-brk(=\S+)?( |$)/))) {
isNodeDebugging = "brk";
}
return isNodeDebugging;
};
export const createRouterWorker = async (routerServerPath, isNodeDebugging, jestWorkerPath = require.resolve("next/dist/compiled/jest-worker"))=>{
const { Worker } = require(jestWorkerPath);
return new Worker(routerServerPath, {
numWorkers: 1,
// TODO: do we want to allow more than 8 OOM restarts?
maxRetries: 8,
forkOptions: {
execArgv: await genRouterWorkerExecArgv(isNodeDebugging === undefined ? false : isNodeDebugging),
env: {
FORCE_COLOR: "1",
...initialEnv || process.env,
NODE_OPTIONS: getNodeOptionsWithoutInspect(),
...process.env.NEXT_CPU_PROF ? {
__NEXT_PRIVATE_CPU_PROFILE: `CPU.router`
} : {},
WATCHPACK_WATCHER_LIMIT: "20",
EXPERIMENTAL_TURBOPACK: process.env.EXPERIMENTAL_TURBOPACK
}
},
exposedMethods: [
"initialize"
]
});
};
export async function startServer({ dir , nextConfig , prevDir , port , isDev , hostname , useWorkers , minimalMode , allowRetry , keepAliveTimeout , onStdout , onStderr , logReady =true }) {
const sockets = new Set();
let worker;
let routerPort;
let handlersReady = ()=>{};
let handlersError = ()=>{};
let handlersPromise = new Promise((resolve, reject)=>{
handlersReady = resolve;
handlersError = reject;
});
let requestHandler = async (_req, _res)=>{
if (handlersPromise) {
await handlersPromise;
return requestHandler(_req, _res);
}
throw new Error("Invariant request handler was not setup");
};
let upgradeHandler = async (_req, _socket, _head)=>{
if (handlersPromise) {
await handlersPromise;
return upgradeHandler(_req, _socket, _head);
}
throw new Error("Invariant upgrade handler was not setup");
};
// setup server listener as fast as possible
const server = http.createServer(async (req, res)=>{
try {
if (handlersPromise) {
await handlersPromise;
handlersPromise = undefined;
}
sockets.add(res);
res.on("close", ()=>sockets.delete(res));
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);
}
});
if (keepAliveTimeout) {
server.keepAliveTimeout = keepAliveTimeout;
}
server.on("upgrade", async (req, socket, head)=>{
try {
sockets.add(socket);
socket.on("close", ()=>sockets.delete(socket));
await upgradeHandler(req, socket, head);
} catch (err) {
socket.destroy();
Log.error(`Failed to handle request for ${req.url}`);
console.error(err);
}
});
let portRetryCount = 0;
server.on("error", (err)=>{
if (allowRetry && port && isDev && err.code === "EADDRINUSE" && portRetryCount < 10) {
Log.warn(`Port ${port} is in use, trying ${port + 1} instead.`);
port += 1;
portRetryCount += 1;
server.listen(port, hostname);
} else {
Log.error(`Failed to start server`);
console.error(err);
process.exit(1);
}
});
let targetHost = hostname;
const isNodeDebugging = checkIsNodeDebugging();
await new Promise((resolve)=>{
server.on("listening", ()=>{
const addr = server.address();
port = typeof addr === "object" ? (addr == null ? void 0 : addr.port) || port : port;
let host = !hostname || hostname === "0.0.0.0" ? "localhost" : hostname;
let normalizedHostname = hostname || "0.0.0.0";
if (isIPv6(hostname)) {
host = host === "::" ? "[::1]" : `[${host}]`;
normalizedHostname = `[${hostname}]`;
}
targetHost = host;
const appUrl = `http://${host}:${port}`;
if (isNodeDebugging) {
const debugPort = getDebugPort();
Log.info(`the --inspect${isNodeDebugging === "brk" ? "-brk" : ""} option was detected, the Next.js proxy server should be inspected at port ${debugPort}.`);
}
if (logReady) {
Log.ready(`started server on ${normalizedHostname}${(port + "").startsWith(":") ? "" : ":"}${port}, url: ${appUrl}`);
// expose the main port to render workers
process.env.PORT = port + "";
}
resolve();
});
server.listen(port, hostname === "localhost" ? "0.0.0.0" : hostname);
});
try {
if (useWorkers) {
var _routerWorker__workerPool;
const httpProxy = require("next/dist/compiled/http-proxy");
let routerServerPath = require.resolve("./router-server");
let jestWorkerPath = require.resolve("next/dist/compiled/jest-worker");
if (prevDir) {
jestWorkerPath = jestWorkerPath.replace(prevDir, dir);
routerServerPath = routerServerPath.replace(prevDir, dir);
}
const routerWorker = await createRouterWorker(routerServerPath, isNodeDebugging, jestWorkerPath);
const cleanup = ()=>{
var _routerWorker__workerPool;
debug("start-server process cleanup");
for (const curWorker of ((_routerWorker__workerPool = routerWorker._workerPool) == null ? void 0 : _routerWorker__workerPool._workers) || []){
var _curWorker__child;
(_curWorker__child = curWorker._child) == null ? void 0 : _curWorker__child.kill("SIGINT");
}
process.exit(0);
};
process.on("exit", cleanup);
process.on("SIGINT", cleanup);
process.on("SIGTERM", cleanup);
process.on("uncaughtException", cleanup);
process.on("unhandledRejection", cleanup);
let didInitialize = false;
for (const _worker of ((_routerWorker__workerPool = routerWorker._workerPool) == null ? void 0 : _routerWorker__workerPool._workers) || []){
// eslint-disable-next-line no-loop-func
_worker._child.on("exit", (code, signal)=>{
// catch failed initializing without retry
if ((code || signal) && !didInitialize) {
routerWorker == null ? void 0 : routerWorker.end();
process.exit(1);
}
});
}
const workerStdout = routerWorker.getStdout();
const workerStderr = routerWorker.getStderr();
workerStdout.on("data", (data)=>{
if (typeof onStdout === "function") {
onStdout(data);
} else {
process.stdout.write(data);
}
});
workerStderr.on("data", (data)=>{
if (typeof onStderr === "function") {
onStderr(data);
} else {
process.stderr.write(data);
}
});
const initializeResult = await routerWorker.initialize({
dir,
port,
hostname,
dev: !!isDev,
minimalMode,
workerType: "router",
isNodeDebugging: !!isNodeDebugging,
keepAliveTimeout
});
routerPort = initializeResult.port;
didInitialize = true;
let compress;
if ((nextConfig == null ? void 0 : nextConfig.compress) !== false) {
compress = setupCompression();
}
const getProxyServer = (pathname)=>{
const targetUrl = `http://${targetHost === "localhost" ? "127.0.0.1" : targetHost}:${routerPort}${pathname}`;
const proxyServer = httpProxy.createProxy({
target: targetUrl,
changeOrigin: false,
ignorePath: true,
xfwd: true,
ws: true,
followRedirects: false
});
// add error listener to prevent uncaught exceptions
proxyServer.on("error", (_err)=>{
// TODO?: enable verbose error logs with --debug flag?
});
proxyServer.on("proxyRes", (proxyRes, innerReq, innerRes)=>{
const cleanupProxy = (err)=>{
// cleanup event listeners to allow clean garbage collection
proxyRes.removeListener("error", cleanupProxy);
proxyRes.removeListener("close", cleanupProxy);
innerRes.removeListener("error", cleanupProxy);
innerRes.removeListener("close", cleanupProxy);
// destroy all source streams to propagate the caught event backward
innerReq.destroy(err);
proxyRes.destroy(err);
};
proxyRes.once("error", cleanupProxy);
proxyRes.once("close", cleanupProxy);
innerRes.once("error", cleanupProxy);
innerRes.once("close", cleanupProxy);
});
return proxyServer;
};
// proxy to router worker
requestHandler = async (req, res)=>{
const urlParts = (req.url || "").split("?");
const urlNoQuery = urlParts[0];
// this normalizes repeated slashes in the path e.g. hello//world ->
// hello/world or backslashes to forward slashes, this does not
// handle trailing slash as that is handled the same as a next.config.js
// redirect
if (urlNoQuery == null ? void 0 : urlNoQuery.match(/(\\|\/\/)/)) {
const cleanUrl = normalizeRepeatedSlashes(req.url);
res.statusCode = 308;
res.setHeader("Location", cleanUrl);
res.end(cleanUrl);
return;
}
if (typeof compress === "function") {
// @ts-expect-error not express req/res
compress(req, res, ()=>{});
}
const targetUrl = `http://${targetHost === "localhost" ? "127.0.0.1" : targetHost}:${routerPort}${req.url || "/"}`;
let invokeRes;
try {
invokeRes = await invokeRequest(targetUrl, {
headers: req.headers,
method: req.method,
signal: signalFromNodeResponse(res)
}, getCloneableBody(req).cloneBodyStream());
} catch (e) {
// If the client aborts before we can receive a response object (when
// the headers are flushed), then we can early exit without further
// processing.
if (isAbortError(e)) {
return;
}
throw e;
}
res.statusCode = invokeRes.status;
res.statusMessage = invokeRes.statusText;
for (const [key, value] of Object.entries(filterReqHeaders(toNodeOutgoingHttpHeaders(invokeRes.headers), ipcForbiddenHeaders))){
if (value !== undefined) {
if (key === "set-cookie") {
const curValue = res.getHeader(key);
const newValue = [];
for (const cookie of Array.isArray(curValue) ? curValue : splitCookiesString(curValue || "")){
newValue.push(cookie);
}
for (const val of Array.isArray(value) ? value : value ? [
value
] : []){
newValue.push(val);
}
res.setHeader(key, newValue);
} else {
res.setHeader(key, value);
}
}
}
if (invokeRes.body) {
await pipeReadable(invokeRes.body, res);
} else {
res.end();
}
};
upgradeHandler = async (req, socket, head)=>{
// add error listeners to prevent uncaught exceptions on socket errors
req.on("error", (_err)=>{
// TODO: log socket errors?
// console.log(_err)
});
socket.on("error", (_err)=>{
// TODO: log socket errors?
// console.log(_err)
});
const proxyServer = getProxyServer(req.url || "/");
proxyServer.on("proxyReqWs", (proxyReq)=>{
socket.on("close", ()=>proxyReq.destroy());
});
proxyServer.ws(req, socket, head);
};
handlersReady();
} else {
// when not using a worker start next in main process
const next = require("../next");
const addr = server.address();
const app = next({
dir,
hostname,
dev: isDev,
isNodeDebugging,
httpServer: server,
customServer: false,
port: addr && typeof addr === "object" ? addr.port : port
});
// handle in process
requestHandler = app.getRequestHandler();
upgradeHandler = app.getUpgradeHandler();
await app.prepare();
handlersReady();
}
} catch (err) {
// fatal error if we can't setup
handlersError();
console.error(err);
process.exit(1);
}
// return teardown function for destroying the server
async function teardown() {
server.close();
sockets.forEach((socket)=>{
sockets.delete(socket);
socket.destroy();
});
if (worker) {
await worker.end();
}
}
teardown.port = routerPort;
return teardown;
}
//# sourceMappingURL=start-server.js.map