Next.js のサーバ起動とリクエストを処理する流れを追いかけてみる
からの引っ越し
Next.js のサーバ起動とリクエストを処理する流れを追いかけてみる
#snaka_oss
- Next.js v13.4.19
サーバ起動
commands.ts
packages/next/src/lib/commands.ts#L7
start: () => Promise.resolve(require('../cli/next-start').nextStart),
next-start.ts
packages/next/src/cli/next-start.ts#L69C1-L76C5
await startServer({
dir,
isDev: false,
isExperimentalTestProxy,
hostname: host,
port,
keepAliveTimeout,
})
start-server.ts
packages/next/src/server/lib/start-server.ts#L105-L119
// setup server listener as fast as possible
const server = http.createServer(async (req, res) => {
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)
}
})
リクエストの処理
packages/next/src/server/lib/start-server.ts#L112
await requestHandler(req, res)
リクエストハンドラーを呼び出す
requestHandler
packages/next/src/server/lib/router-server.ts#L308
const requestHandler: WorkerRequestHandler = async (req, res) => {
handleRequest
packages/next/src/server/lib/router-server.ts#L442C2-L442C59
const handleRequest = async (handleIndex: number) => {
packages/next/src/server/lib/router-server.ts#L555-L560C13
await invokeRender(parsedUrl, 'pages', handleIndex, '/_error', {
'x-invoke-status': '500',
'x-invoke-error': JSON.stringify({
message: `A conflicting public file and page file was found for path ${matchedOutput.itemPath} https://nextjs.org/docs/messages/conflicting-public-file-page`,
}),
})
invokeRender
packages/next/src/server/lib/router-server.ts#L322-L328
async function invokeRender(
parsedUrl: NextUrlWithParsedQuery,
type: keyof typeof renderWorkers,
handleIndex: number,
invokePath: string,
additionalInvokeHeaders: Record<string, string> = {}
) {
packages/next/src/server/lib/router-server.ts#L374-L382C10
invokeRes = await invokeRequest(
renderUrl,
{
headers: invokeHeaders,
method: req.method,
signal: signalFromNodeResponse(res),
},
getRequestMeta(req, '__NEXT_CLONABLE_BODY')?.cloneBodyStream()
)
invokeRequest
packages/next/src/server/lib/server-ipc/invoke-request.ts#L5-L13
export const invokeRequest = async (
targetUrl: string,
requestInit: {
headers: IncomingMessage['headers']
method: IncomingMessage['method']
signal?: AbortSignal
},
readableBody?: Readable | ReadableStream
) => {
fetch で targetUrl
にリクエストを投げている
return await fetch(targetUrl, {
headers: invokeHeaders as any as Headers,
method: requestInit.method,
redirect: 'manual',
signal: requestInit.signal,
...(requestInit.method !== 'GET' &&
requestInit.method !== 'HEAD' &&
readableBody
? {
body: readableBody as BodyInit,
duplex: 'half',
}
: {}),
next: {
// @ts-ignore
internal: true,
},
})
targetUrl
とは何か?
packages/next/src/server/lib/router-server.ts#L360
const renderUrl = `http://${workerResult.hostname}:${workerResult.port}${req.url}`
renderUrl
が targetUrl
になる
workerResult.hsotname
とは?
packages/next/src/server/lib/router-server.ts#L354
const workerResult = initialized[type]
type
は renderWorkers の key ( pages
| app
) を指す
initialized
とは?
packages/next/src/server/lib/router-server.ts#L223-L226
const initialized = {
app: await renderWorkers.app?.initialize(renderWorkerOpts),
pages: await renderWorkers.pages?.initialize(renderWorkerOpts),
}
renderWorkers.pages
は?
packages/next/src/server/lib/router-server.ts#L213-L220
renderWorkers.pages = await createWorker(
ipcPort,
ipcValidationKey,
opts.isNodeDebugging,
'pages',
config,
initialEnv
)
createWorker
packages/next/src/lib/worker.ts#L36-L51
const createWorker = () => {
this._worker = new JestWorker(workerPath, {
...farmOptions,
forkOptions: {
...farmOptions.forkOptions,
env: {
...((farmOptions.forkOptions?.env || {}) as any),
...process.env,
// we don't pass down NODE_OPTIONS as it can
// extra memory usage
NODE_OPTIONS: getNodeOptionsWithoutInspect()
.replace(/--max-old-space-size=[\d]{1,}/, '')
.trim(),
} as any,
},
}) as JestWorker
jest-worker
って使ってるんだ
Jest という名前からテストをイメージするけど、それをプロダクションのサービスで利用しているの以外 ( 名前がミスリード )
サーバ起動処理のつづき
getRequestHandlers - http.server が listening
イベント発火したら呼び出される
packages/next/src/server/lib/start-server.ts#L31-L61
export async function getRequestHandlers({
dir,
port,
isDev,
hostname,
minimalMode,
isNodeDebugging,
keepAliveTimeout,
experimentalTestProxy,
}: {
dir: string
port: number
isDev: boolean
hostname: string
minimalMode?: boolean
isNodeDebugging?: boolean
keepAliveTimeout?: number
experimentalTestProxy?: boolean
}): ReturnType<typeof initialize> {
return initialize({
dir,
port,
hostname,
dev: isDev,
minimalMode,
workerType: 'router',
isNodeDebugging: isNodeDebugging || false,
keepAliveTimeout,
experimentalTestProxy,
})
}
router-server ( next-router-worker )
packages/next/src/server/lib/router-server.ts#L58-L69
export async function initialize(opts: {
dir: string
port: number
dev: boolean
minimalMode?: boolean
hostname?: string
workerType: 'router' | 'render'
isNodeDebugging: boolean
keepAliveTimeout?: number
customServer?: boolean
experimentalTestProxy?: boolean
}): Promise<[WorkerRequestHandler, WorkerUpgradeHandler]> {
createIpcServer - 内部的な http サーバを立ち上げている
packages/next/src/server/lib/router-server.ts#L142C1-L150C7
const { ipcPort, ipcValidationKey } = await createIpcServer({
async ensurePage(
match: Parameters<
InstanceType<typeof import('../dev/hot-reloader').default>['ensurePage']
>[0]
) {
// TODO: remove after ensure is pulled out of server
return await devInstance?.hotReloader.ensurePage(match)
},
renderWorkers.pages = await createWorker(
ipcPort,
ipcValidationKey,
opts.isNodeDebugging,
'pages',
config,
initialEnv
)
server-ipc
createIpcServer
packages/next/src/server/lib/server-ipc/index.ts#L13C1-L21
// we can't use process.send as jest-worker relies on
// it already and can cause unexpected message errors
// so we create an IPC server for communicating
export async function createIpcServer(
server: InstanceType<typeof NextServer>
): Promise<{
ipcPort: number
ipcServer: import('http').Server
ipcValidationKey: string
}> {