Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix standalone mode with appDir running in a single process #49116

Merged
merged 13 commits into from
May 4, 2023
2 changes: 1 addition & 1 deletion docs/advanced-features/custom-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ app.prepare().then(() => {
} catch (err) {
console.error('Error occurred handling', req.url, err)
res.statusCode = 500
res.end('internal server error')
res.end('Internal Server Error')
}
})
.once('error', (err) => {
Expand Down
50 changes: 37 additions & 13 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1899,10 +1899,21 @@ export default async function build(
config.experimental?.turbotrace?.contextDirectory ??
outputFileTracingRoot

// Under standalone mode, we need to trace the extra IPC server and
// worker files.
const isStandalone = config.output === 'standalone'

const nextServerEntry = require.resolve(
'next/dist/server/next-server'
)
const toTrace = [nextServerEntry]
const toTrace = [
nextServerEntry,
isStandalone
? require.resolve(
'next/dist/server/lib/render-server-standalone'
)
: null,
].filter(nonNullable)

// ensure we trace any dependencies needed for custom
// incremental cache handler
Expand All @@ -1927,7 +1938,7 @@ export default async function build(
'**/*.d.ts',
'**/*.map',
'**/next/dist/pages/**/*',
'**/next/dist/compiled/jest-worker/**/*',
isStandalone ? null : '**/next/dist/compiled/jest-worker/**/*',
'**/next/dist/compiled/webpack/(bundle4|bundle5).js',
'**/node_modules/webpack5/**/*',
'**/next/dist/server/lib/squoosh/**/*.wasm',
Expand All @@ -1944,7 +1955,8 @@ export default async function build(
? ['**/next/dist/compiled/@ampproject/toolbox-optimizer/**/*']
: []),
...additionalIgnores,
]
].filter(nonNullable)

const ignoreFn = (pathname: string) => {
if (path.isAbsolute(pathname) && !pathname.startsWith(root)) {
return true
Expand All @@ -1957,6 +1969,26 @@ export default async function build(
}
const traceContext = path.join(nextServerEntry, '..', '..')
const tracedFiles = new Set<string>()

function addToTracedFiles(base: string, file: string) {
tracedFiles.add(
path
.relative(distDir, path.join(base, file))
.replace(/\\/g, '/')
)
}

if (isStandalone) {
addToTracedFiles(
'',
require.resolve('next/dist/compiled/jest-worker/processChild')
)
addToTracedFiles(
'',
require.resolve('next/dist/compiled/jest-worker/threadChild')
)
}

if (config.experimental.turbotrace) {
const files: string[] = await nodeFileTrace(
{
Expand All @@ -1972,11 +2004,7 @@ export default async function build(
)
for (const file of files) {
if (!ignoreFn(path.join(traceContext, file))) {
tracedFiles.add(
path
.relative(distDir, path.join(traceContext, file))
.replace(/\\/g, '/')
)
addToTracedFiles(traceContext, file)
}
}
} else {
Expand All @@ -1987,11 +2015,7 @@ export default async function build(
})

serverResult.fileList.forEach((file) => {
tracedFiles.add(
path
.relative(distDir, path.join(root, file))
.replace(/\\/g, '/')
)
addToTracedFiles(root, file)
})
}
await promises.writeFile(
Expand Down
38 changes: 15 additions & 23 deletions packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1913,17 +1913,20 @@ export async function copyTracedFiles(
serverOutputPath,
`${
moduleType
? `import Server from 'next/dist/server/next-server.js'
import http from 'http'
? `import http from 'http'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const NextServer = Server.default`
import { createServerHandler } from 'next/dist/server/lib/render-server-standalone.js'
`
: `
const NextServer = require('next/dist/server/next-server').default
const http = require('http')
const path = require('path')`
const path = require('path')
const { createServerHandler } = require('next/dist/server/lib/render-server-standalone')`
}

const dir = path.join(__dirname)

process.env.NODE_ENV = 'production'
process.chdir(__dirname)

Expand All @@ -1944,24 +1947,15 @@ const nextConfig = ${JSON.stringify({
distDir: `./${path.relative(dir, distDir)}`,
})}

${
// In standalone mode, we don't have separated render workers so if both
// app and pages are used, we need to resolve to the prebundled React
// to ensure the correctness of the version for app.
`\
if (nextConfig && nextConfig.experimental && nextConfig.experimental.appDir) {
process.env.__NEXT_PRIVATE_PREBUNDLED_REACT = nextConfig.experimental.serverActions ? 'experimental' : 'next'
}
`
}
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)

const server = http.createServer(async (req, res) => {
try {
await handler(req, res)
} catch (err) {
console.error(err);
res.statusCode = 500
res.end('internal server error')
res.end('Internal Server Error')
}
})

Expand All @@ -1972,20 +1966,18 @@ if (
) {
server.keepAliveTimeout = keepAliveTimeout
}
server.listen(currentPort, (err) => {
server.listen(currentPort, async (err) => {
if (err) {
console.error("Failed to start server", err)
process.exit(1)
}
const nextServer = new NextServer({
hostname,

handler = await createServerHandler({
port: currentPort,
dir: path.join(__dirname),
dev: false,
customServer: false,
hostname,
dir,
conf: nextConfig,
})
handler = nextServer.getRequestHandler()

console.log(
'Listening on port',
Expand Down
4 changes: 4 additions & 0 deletions packages/next/src/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,10 @@ export default async function loadConfig(
rawConfig?: boolean,
silent?: boolean
): Promise<NextConfigComplete> {
if (process.env.__NEXT_PRIVATE_STANDALONE_CONFIG) {
return JSON.parse(process.env.__NEXT_PRIVATE_STANDALONE_CONFIG)
}

const curLog = silent
? {
warn: () => {},
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/image-optimizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ export async function optimizeImage({
console.error(
`Error: 'sharp' is required to be installed in standalone mode for the image optimization to function correctly. Read more at: https://nextjs.org/docs/messages/sharp-missing-in-production`
)
throw new ImageError(500, 'internal server error')
throw new ImageError(500, 'Internal Server Error')
}
// Show sharp warning in production once
if (showSharpMissingWarning) {
Expand Down
107 changes: 107 additions & 0 deletions packages/next/src/server/lib/render-server-standalone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { IncomingMessage, ServerResponse } from 'http'
import type { ChildProcess } from 'child_process'

import httpProxy from 'next/dist/compiled/http-proxy'
import { Worker } from 'next/dist/compiled/jest-worker'
import { normalizeRepeatedSlashes } from '../../shared/lib/utils'

const renderServerPath = require.resolve('./render-server')

export const createServerHandler = async ({
port,
hostname,
dir,
minimalMode,
}: {
port: number
hostname: string
dir: string
minimalMode: boolean
}) => {
const routerWorker = new Worker(renderServerPath, {
numWorkers: 1,
maxRetries: 10,
forkOptions: {
env: {
FORCE_COLOR: '1',
...process.env,
},
},
exposedMethods: ['initialize'],
}) as any as InstanceType<typeof Worker> & {
initialize: typeof import('./render-server').initialize
}

let didInitialize = false

for (const _worker of ((routerWorker as any)._workerPool?._workers || []) as {
_child: ChildProcess
}[]) {
// eslint-disable-next-line no-loop-func
_worker._child.on('exit', (code, signal) => {
// catch failed initializing without retry
if ((code || signal) && !didInitialize) {
routerWorker?.end()
process.exit(1)
}
})
}

const workerStdout = routerWorker.getStdout()
const workerStderr = routerWorker.getStderr()

workerStdout.on('data', (data) => {
process.stdout.write(data)
})
workerStderr.on('data', (data) => {
process.stderr.write(data)
})

const { port: routerPort } = await routerWorker.initialize({
dir,
port,
dev: false,
hostname,
minimalMode,
workerType: 'router',
})
didInitialize = true

const getProxyServer = (pathname: string) => {
const targetUrl = `http://${hostname}:${routerPort}${pathname}`
const proxyServer = httpProxy.createProxy({
target: targetUrl,
changeOrigin: false,
ignorePath: true,
xfwd: true,
ws: true,
followRedirects: false,
})
return proxyServer
}

// proxy to router worker
return async (req: IncomingMessage, res: ServerResponse) => {
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?.match(/(\\|\/\/)/)) {
const cleanUrl = normalizeRepeatedSlashes(req.url!)
res.statusCode = 308
res.setHeader('Location', cleanUrl)
res.end(cleanUrl)
return
}
const proxyServer = getProxyServer(req.url || '/')
proxyServer.web(req, res)
proxyServer.on('error', (err) => {
res.statusCode = 500
res.end('Internal Server Error')
console.error(err)
})
}
}
31 changes: 19 additions & 12 deletions packages/next/src/server/lib/render-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export async function initialize(opts: {
dir: string
port: number
dev: boolean
minimalMode?: boolean
hostname?: string
workerType: 'router' | 'render'
keepAliveTimeout?: number
Expand All @@ -54,18 +55,24 @@ export async function initialize(opts: {
let requestHandler: RequestHandler

const server = http.createServer((req, res) => {
return requestHandler(req, res).finally(() => {
if (
process.memoryUsage().heapUsed / 1024 / 1024 >
MAXIMUM_HEAP_SIZE_ALLOWED
) {
warn(
'The server is running out of memory, restarting to free up memory.'
)
server.close()
process.exit(WORKER_SELF_EXIT_CODE)
}
})
return requestHandler(req, res)
.catch((err) => {
res.statusCode = 500
res.end('Internal Server Error')
console.error(err)
})
.finally(() => {
if (
process.memoryUsage().heapUsed / 1024 / 1024 >
MAXIMUM_HEAP_SIZE_ALLOWED
) {
warn(
'The server is running out of memory, restarting to free up memory.'
)
server.close()
process.exit(WORKER_SELF_EXIT_CODE)
}
})
})

if (opts.keepAliveTimeout) {
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/server/lib/server-ipc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ export const createWorker = (
__NEXT_PRIVATE_RENDER_WORKER: type,
__NEXT_PRIVATE_ROUTER_IPC_PORT: ipcPort + '',
__NEXT_PRIVATE_ROUTER_IPC_KEY: ipcValidationKey,
__NEXT_PRIVATE_STANDALONE_CONFIG:
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG,
NODE_ENV: process.env.NODE_ENV,
...(type === 'app'
? {
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ export default class NextNodeServer extends BaseServer {
dir: this.dir,
workerType: 'render',
hostname: this.hostname,
minimalMode: this.minimalMode,
dev: !!options.dev,
}
const { createWorker, createIpcServer } =
Expand Down
8 changes: 5 additions & 3 deletions packages/next/src/server/next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,11 @@ export class NextServer {
this.serverPromise = this.loadConfig().then(async (conf) => {
if (!this.options.dev) {
if (conf.output === 'standalone') {
log.warn(
`"next start" does not work with "output: standalone" configuration. Use "node .next/standalone/server.js" instead.`
)
if (!process.env.__NEXT_PRIVATE_STANDALONE_CONFIG) {
log.warn(
`"next start" does not work with "output: standalone" configuration. Use "node .next/standalone/server.js" instead.`
)
}
} else if (conf.output === 'export') {
throw new Error(
`"next start" does not work with "output: export" configuration. Use "npx serve@latest out" instead.`
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/streaming-ssr/custom-server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const server = http.createServer(async (req, res) => {
} catch (err) {
console.error(err)
res.statusCode = 500
res.end('internal server error')
res.end('Internal Server Error')
}
})
const currentPort = parseInt(process.env.PORT, 10) || 3000
Expand Down
Loading