Created
December 14, 2024 13:15
-
-
Save huksley/7529d6ea78a4d9feaaefa9fc3ba6c83a to your computer and use it in GitHub Desktop.
Custom NextJS 14 server, works with development mode and standalone builds.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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