Skip to content

Instantly share code, notes, and snippets.

@huksley
Created December 14, 2024 13:15
Show Gist options
  • Save huksley/7529d6ea78a4d9feaaefa9fc3ba6c83a to your computer and use it in GitHub Desktop.
Save huksley/7529d6ea78a4d9feaaefa9fc3ba6c83a to your computer and use it in GitHub Desktop.
Custom NextJS 14 server, works with development mode and standalone builds.
/**
* Based on https://nextjs.org/docs/pages/building-your-application/configuring/custom-server
*/
/* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-empty-function */
const { createServer } = require("node:http");
const { parse } = require("node:url");
const next = require("next");
const { initialize } = require("next/dist/server/lib/router-server");
const fs = require("node:fs");
/**
* Filter out private IPs (both IPv6 or IPv4) and return possible public IP
* address, analyzing any IP addresses or X-Forwarded-For header
*
* @param {(string | string[] | undefined)[]} sourceIps
* @returns {string | undefined}
*/
const getPublicIp = (...sourceIps) => {
/** @type {string[]} */
// @ts-ignore
const myIps = [...(sourceIps ?? [])]
.flat()
.filter(v => v !== undefined && v !== null && v.trim() !== "");
return (
myIps
// X-Forwarded-For: <client>, <proxy>, …, <proxyN>
.map(ip => (ip?.indexOf(",") > 0 ? ip.split(",")[0] : ip))
.map(ip => ip.trim())
.filter(
ip =>
ip !== "undefined" &&
ip != "::1" &&
!ip.startsWith("::ffff:10.") &&
!ip.startsWith("::ffff:192.168.") &&
!ip.startsWith("::ffff:127.0.")
)
.filter(
ip => !ip.startsWith("127.0") && !ip.startsWith("192.168.") && !ip.startsWith("10.")
)
.map(ip => (ip.startsWith("::ffff:") ? ip.substring(7) : ip))
.map(ip => ip.trim())
.filter(ip => ip != "")
.shift()
);
};
/** @type {Console & { verbose: (...args: any[]) => void }} */
// @ts-ignore
const logger = console;
logger.verbose = process.env.NEXT_PUBLIC_LOG_VERBOSE === "1" ? logger.info : () => {};
const hostname = process.env.HOSTNAME ?? "127.0.0.1";
const port = parseInt(process.env.PORT || "3000", 10);
const dev = process.env.NODE_ENV !== "production";
const keepAliveTimeout = process.env.KEEP_ALIVE_TIMEOUT
? parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
: 60000;
const logIpAddress = process.env.SERVER_LOG_IP_ADDRESS === "1";
/** @type {import("next/dist/server/lib/types").WorkerUpgradeHandler | undefined} */
let upgradeHandler = undefined;
/** @type {import("next/dist/server/next").RequestHandler | undefined} */
let requestHandler = undefined;
const server = createServer(
{
maxHeaderSize: 8192,
insecureHTTPParser: false
},
async (req, res) => {
if (!req.url) {
logger.warn("No URL in request");
res.statusCode = 500;
res.end("Internal server error");
return;
}
const parsedUrl = parse(req.url, true);
const start = Date.now();
const ipAddress = logIpAddress
? (getPublicIp(
req?.connection?.remoteAddress,
req?.socket?.remoteAddress,
req?.headers["x-real-ip"],
req?.headers["x-forwarded-for"]
) ?? "?.?.?.?")
: undefined;
logger.info("-->", req.method, parsedUrl.pathname, ipAddress ?? "");
if (!requestHandler) {
logger.warn("-->", req.method, parsedUrl.pathname, "No NextJS request handler");
res.statusCode = 500;
res.end("Internal server error");
return;
}
if (process.env.NODE_ENV === "development") {
// Omit NextJS writing it's access log https://github.com/vercel/next.js/discussions/65992
const __write = process.stdout.write;
// @ts-ignore
process.stdout.write = (...args) => {
if (
typeof args[0] !== "string" ||
!(
args[0].startsWith(" GET /") ||
args[0].startsWith(" POST /") ||
args[0].startsWith(" DELETE /") ||
args[0].startsWith(" PATCH /")
)
) {
// @ts-ignore
__write.apply(process.stdout, args);
}
};
}
await requestHandler(req, res, parsedUrl).then(() => {
logger.info(
"<--",
res.statusCode,
req.method,
parsedUrl.pathname,
ipAddress ? ipAddress + " Δ" : "Δ",
Date.now() - start,
"ms"
);
});
}
);
server.timeout = 120000;
server.keepAliveTimeout = keepAliveTimeout;
server.headersTimeout = 10000;
server.requestTimeout = 120000;
server.maxHeadersCount = 100;
server.on("error", err => {
logger.warn("Failed to launch server", err);
throw new Error("Failed to launch server: " + new Error(err.message || String(err)));
});
server.on("upgrade", async (req, socket, head) => {
if (!req.url) {
logger.warn("No URL in request");
return;
}
const parsedUrl = parse(req.url, true);
if (!upgradeHandler) {
logger.warn("-->", req.method, parsedUrl.pathname, "No NextJS request handler");
return;
}
try {
logger.warn("-->", req.method, parsedUrl.pathname, "websocket upgrade");
await upgradeHandler(req, socket, head);
} catch (err) {
socket.destroy();
logger.warn(`Failed to handle upgrade request for ${req.url}`, err);
}
});
server.on("listening", async () => {
logger.info(
`Next app listening at http://${hostname}:${port} (${
process.env.NODE_ENV || "development"
})`
);
const nextBuild = fs.existsSync(".next/required-server-files.json")
? JSON.parse(fs.readFileSync("./.next/required-server-files.json", "utf8"))
: undefined;
/** @type {import('next').NextConfig} */
const config = nextBuild?.config;
if (config?.output === "standalone") {
if (dev) {
logger.warn("NextJS standalone server is not supported in development mode");
process.exit(1);
}
// https://github.com/vercel/next.js/issues/64031
//process.env.__NEXT_PRIVATE_RENDER_WORKER = "yes";
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(config);
const options = {
dir: ".",
port,
hostname,
config,
dev: false,
minimalMode: false,
server,
isNodeDebugging: false,
keepAliveTimeout,
experimentalTestProxy: false,
experimentalHttpsServer: false
};
[requestHandler, upgradeHandler] = await initialize(options);
logger.info("NextJS standalone server ready");
} else {
/** @type {import("next/dist/server/next").NextServerOptions} */
const options = {
dev,
customServer: true,
hostname,
port,
httpServer: server
};
/** @type {import("next/dist/server/next").NextServer} */
// @ts-ignore
const app = next(options);
await app.prepare();
requestHandler = app.getRequestHandler();
upgradeHandler = app.getUpgradeHandler();
logger.info("NextJS server ready");
}
// https://pm2.keymetrics.io/docs/usage/signals-clean-restart/
if (process.send) {
process.send("ready");
}
});
process.on("SIGINT", () => {
logger.warn("SIGINT: Shutting down...");
server.close();
process.exit(1);
});
process.on("SIGTERM", () => {
logger.info("SIGTERM: Shutting down...");
server.close();
process.exit(0);
});
server.listen(port, hostname);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment