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: fixed standalone crashing on Node.js >=17, fixed revalidatePath, revalidateTag #51887

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TLSSocket } from 'tls'
import { isIPv6 } from 'net'
import {
addRequestMeta,
NextUrlWithParsedQuery,
Expand All @@ -18,7 +19,13 @@ export function attachRequestMeta(
? 'https'
: 'http'

const initUrl = `${protocol}://${host}${req.url}`
let resolvedHostname = host

if (resolvedHostname && isIPv6(resolvedHostname)) {
resolvedHostname = `[${resolvedHostname}]`
}

const initUrl = `${protocol}://${resolvedHostname}${req.url}`

addRequestMeta(req, '__NEXT_INIT_URL', initUrl)
addRequestMeta(req, '__NEXT_INIT_QUERY', { ...parsedUrl.query })
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/export/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ export default async function exportPage({
writeFile: (f, d) => fs.promises.writeFile(f, d),
mkdir: (dir) => fs.promises.mkdir(dir, { recursive: true }),
stat: (f) => fs.promises.stat(f),
createWriteStream: fs.createWriteStream,
},
serverDistDir: join(distDir, 'server'),
CurCacheHandler: CacheHandler,
Expand Down
34 changes: 20 additions & 14 deletions packages/next/src/server/lib/incremental-cache/file-system-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,24 +110,30 @@ export default class FileSystemCache implements CacheHandler {
// we need to ensure the tagsManifest is refreshed
// since separate workers can be updating it at the same
// time and we can't flush out of sync data
this.loadTagsManifest()
if (!tagsManifest || !this.tagsManifestPath) {
if (!this.tagsManifestPath) {
return
}
const manifestWriteStream = this.fs.createWriteStream(this.tagsManifestPath)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this change needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ijjk it is as I described, we call multiple revalidateTag (staticGenerationStore.pendingRevalidates) with await Promise.all, but it is not safe to write to one file in a parallel manner.


const data = tagsManifest.items[tag] || { keys: [] }
data.revalidatedAt = Date.now()
tagsManifest.items[tag] = data

try {
await this.fs.mkdir(path.dirname(this.tagsManifestPath))
await this.fs.writeFile(
this.tagsManifestPath,
JSON.stringify(tagsManifest || {})
return new Promise<void>((resolve, reject) => {
manifestWriteStream.on('ready', () => {
this.loadTagsManifest()
if (!tagsManifest) {
return
}
const data = tagsManifest.items[tag] || {
keys: [],
}
data.revalidatedAt = Date.now()
tagsManifest.items[tag] = data
manifestWriteStream.write(JSON.stringify(tagsManifest))
manifestWriteStream.end()
})
manifestWriteStream.on('finish', resolve)
manifestWriteStream.on('error', (err) =>
reject(`Failed to update tags manifest. ${err}`)
)
} catch (err: any) {
console.warn('Failed to update tags manifest.', err)
}
})
}

public async get(key: string, fetchCache?: boolean) {
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/lib/node-fs-methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export const nodeFs: CacheFs = {
writeFile: (f, d) => _fs.promises.writeFile(f, d),
mkdir: (dir) => _fs.promises.mkdir(dir, { recursive: true }),
stat: (f) => _fs.promises.stat(f),
createWriteStream: _fs.createWriteStream,
}
24 changes: 13 additions & 11 deletions packages/next/src/server/lib/render-server-standalone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const createServerHandler = async ({
},
},
exposedMethods: ['initialize'],
}) as any as InstanceType<typeof Worker> & {
}) as InstanceType<typeof Worker> & {
initialize: typeof import('./render-server').initialize
}

Expand Down Expand Up @@ -57,19 +57,21 @@ export const createServerHandler = async ({
process.stderr.write(data)
})

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

didInitialize = true

const getProxyServer = (pathname: string) => {
const targetUrl = `http://${hostname}:${routerPort}${pathname}`
const targetUrl = `http://${routerHostname}:${routerPort}${pathname}`
const proxyServer = httpProxy.createProxy({
target: targetUrl,
changeOrigin: false,
Expand Down
21 changes: 11 additions & 10 deletions packages/next/src/server/lib/render-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,28 +102,29 @@ export async function initialize(opts: {
})
}

let hostname = opts.hostname || 'localhost'

server.on('listening', async () => {
try {
const addr = server.address()
const port = addr && typeof addr === 'object' ? addr.port : 0
let resolvedHostname =
addr && typeof addr === 'object' ? addr.address : undefined

if (!port) {
if (!port || !resolvedHostname) {
console.error(`Invariant failed to detect render worker port`, addr)
process.exit(1)
}

let hostname =
!opts.hostname || opts.hostname === '0.0.0.0'
? 'localhost'
: opts.hostname

if (isIPv6(hostname)) {
hostname = hostname === '::' ? '[::1]' : `[${hostname}]`
if (isIPv6(resolvedHostname)) {
resolvedHostname = `[${resolvedHostname}]`
}

result = {
port,
hostname,
hostname: resolvedHostname,
}

const app = next({
...opts,
_routerWorker: opts.workerType === 'router',
Expand All @@ -143,6 +144,6 @@ export async function initialize(opts: {
return reject(err)
}
})
server.listen(await getFreePort(), '0.0.0.0')
server.listen(await getFreePort(), hostname)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing this to hostname seems like it could cause the issue you're describing, 0.0.0.0 should ensure it's available to be invoked via 127.0.0.1.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ijjk no, it won't. I tried it myself. Rather than using the provided hostname for the proxy server, we now use what server.address() returns. So if the provided host is localhost, it will return ::1 if IPv6 is used and 127.0.0.1 if IPv4 is used, ensuring that the hostname for the proxy server is always correct.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a quick way to install this particular change under node_modules in my own app repository? I could test out if this change resolves the issue I'm getting here: #52215

Copy link
Contributor Author

@DuCanhGH DuCanhGH Jul 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michaelangrivera I don't know, maybe try npm install 'https://gitpkg.now.sh/DuCanhGH/next.js/packages/next?00457da5f319d9ce9f49f53ba8c80779c502906a'?

Edit: doesn't seem to be working...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup. it doesn't work for me. Is it possible to publish it?

Copy link
Contributor Author

@DuCanhGH DuCanhGH Jul 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michaelangrivera eh, doesn't seem possible without straight up publishing a fork of Next.js, which seems to be quite troublesome...

})
}
2 changes: 1 addition & 1 deletion packages/next/src/server/lib/server-ipc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export async function createIpcServer(
)

const ipcPort = await new Promise<number>((resolveIpc) => {
ipcServer.listen(0, '0.0.0.0', () => {
ipcServer.listen(0, server.hostname, () => {
const addr = ipcServer.address()

if (addr && typeof addr === 'object') {
Expand Down
7 changes: 1 addition & 6 deletions packages/next/src/server/lib/server-ipc/invoke-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,8 @@ export const invokeRequest = async (
const http = require('http') as typeof import('http')

try {
// force to 127.0.0.1 as IPC always runs on this hostname
// to avoid localhost issues
const parsedTargetUrl = new URL(targetUrl)
parsedTargetUrl.hostname = '127.0.0.1'

const invokeReq = http.request(
parsedTargetUrl.toString(),
targetUrl,
{
headers: invokeHeaders,
method: requestInit.method,
Expand Down
11 changes: 9 additions & 2 deletions packages/next/src/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import type { RenderOpts } from './render'
import fs from 'fs'
import { join, relative, resolve, sep, isAbsolute } from 'path'
import { IncomingMessage, ServerResponse } from 'http'
import { isIPv6 } from 'net'
import { addRequestMeta, getRequestMeta } from './request-meta'
import {
PAGES_MANIFEST,
Expand Down Expand Up @@ -2769,10 +2770,16 @@ export default class NextNodeServer extends BaseServer {
? 'https'
: 'http'

let resolvedHostname = this.hostname

if (resolvedHostname && isIPv6(resolvedHostname)) {
resolvedHostname = `[${resolvedHostname}]`
}

// When there are hostname and port we build an absolute URL
const initUrl =
this.hostname && this.port
? `${protocol}://${this.hostname}:${this.port}${req.url}`
resolvedHostname && this.port
? `${protocol}://${resolvedHostname}:${this.port}${req.url}`
: (this.nextConfig.experimental as any).trustHostHeader
? `https://${req.headers.host || 'localhost'}${req.url}`
: req.url
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/shared/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ComponentType } from 'react'
import type { DomainLocale } from '../../server/config'
import type { Env } from '@next/env'
import type { IncomingMessage, ServerResponse } from 'http'
import type fs from 'fs'
import type { NextRouter } from './router/router'
import type { ParsedUrlQuery } from 'querystring'
import type { PreviewData } from 'next/types'
Expand Down Expand Up @@ -451,6 +452,7 @@ export interface CacheFs {
writeFile(f: string, d: any): Promise<void>
mkdir(dir: string): Promise<void | string>
stat(f: string): Promise<{ mtime: Date }>
createWriteStream: typeof fs.createWriteStream
}

export function stringifyError(error: Error) {
Expand Down