Better IPv6 support for next-server
(#53131)
### What? This PR makes it easier to use Next.js with IPv6 hostnames such as `::1` and `::`. ### How? It does so by removing rewrites from `localhost` to `127.0.0.1` introduced in #52492. It also fixes the issue where Next.js tries to fetch something like `http://::1:3000` when `--hostname` is `::1` as it is not a valid URL (browsers' `URL` class throws an error when constructed with such hosts). It also fixes `NextURL` so that it doesn't accept `http://::1:3000` but refuse `http://[::1]:3000`. It also changes `next/src/server/lib/setup-server-worker.ts` so that it uses the server's `address` method to retrieve the host instead of our provided `opts.hostname`, ensuring that no matter what `opts.hostname` is we will always get the correct one. ### Note I've verified that `next dev`, `next start` and `node .next/standalone/server.js` work with IPv6 hostnames (such as `::` and `::1`), IPv4 hostnames (such as `127.0.0.1`, `0.0.0.0`) and `localhost` - and with any of these hostnames fetching to `localhost` also works. Server Actions and middleware have no problems as well. This also removes `.next/standalone/server.js`'s logging as we now use `start-server`'s logging to avoid duplicates. `start-server`'s logging has also been updated to report the actual hostname. ![image](https://github.com/vercel/next.js/assets/75556609/cefa5f23-ff09-4cef-a055-13eea7c11d89) ![image](https://github.com/vercel/next.js/assets/75556609/619e82ce-45d9-47b7-8644-f4ad083429db) The above pictures also demonstrate using Server Actions with Next.js after this PR. ![image](https://github.com/vercel/next.js/assets/75556609/3d4166e9-f950-4390-bde9-af2547658148) Fixes #53171 Fixes #49578 Closes NEXT-1510 Co-authored-by: Tim Neutkens <6324199+timneutkens@users.noreply.github.com> Co-authored-by: Zack Tanner <1939140+ztanner@users.noreply.github.com>
This commit is contained in:
parent
df6ec96ab2
commit
a4b430e6f1
28 changed files with 168 additions and 127 deletions
|
@ -6,11 +6,12 @@ import {
|
|||
import { NodeNextRequest } from 'next/dist/server/base-http/node'
|
||||
import { BaseNextRequest } from 'next/dist/server/base-http'
|
||||
import { getCloneableBody } from 'next/dist/server/body-streams'
|
||||
import { formatHostname } from 'next/dist/server/lib/format-hostname'
|
||||
|
||||
export function attachRequestMeta(
|
||||
req: BaseNextRequest,
|
||||
parsedUrl: NextUrlWithParsedQuery,
|
||||
host: string
|
||||
hostname: string
|
||||
) {
|
||||
const protocol = (
|
||||
(req as NodeNextRequest).originalRequest?.socket as TLSSocket
|
||||
|
@ -18,7 +19,7 @@ export function attachRequestMeta(
|
|||
? 'https'
|
||||
: 'http'
|
||||
|
||||
const initUrl = `${protocol}://${host}${req.url}`
|
||||
const initUrl = `${protocol}://${formatHostname(hostname)}${req.url}`
|
||||
|
||||
addRequestMeta(req, '__NEXT_INIT_URL', initUrl)
|
||||
addRequestMeta(req, '__NEXT_INIT_QUERY', { ...parsedUrl.query })
|
||||
|
|
|
@ -1972,17 +1972,11 @@ startServer({
|
|||
dir,
|
||||
isDev: false,
|
||||
config: nextConfig,
|
||||
hostname: hostname === 'localhost' ? '0.0.0.0' : hostname,
|
||||
hostname,
|
||||
port: currentPort,
|
||||
allowRetry: false,
|
||||
keepAliveTimeout,
|
||||
useWorkers: !!nextConfig.experimental?.appDir,
|
||||
}).then(() => {
|
||||
console.log(
|
||||
'Listening on port',
|
||||
currentPort,
|
||||
'url: http://' + hostname + ':' + currentPort
|
||||
)
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
|
|
|
@ -466,7 +466,7 @@ async function revalidate(
|
|||
const ipcKey = process.env.__NEXT_PRIVATE_ROUTER_IPC_KEY
|
||||
const res = await invokeRequest(
|
||||
`http://${
|
||||
context.hostname
|
||||
context.hostname || 'localhost'
|
||||
}:${ipcPort}?key=${ipcKey}&method=revalidate&args=${encodeURIComponent(
|
||||
JSON.stringify([{ urlPath, revalidateHeaders, opts }])
|
||||
)}`,
|
||||
|
|
|
@ -121,24 +121,6 @@ function getForwardedHeaders(
|
|||
return new Headers(mergedHeaders)
|
||||
}
|
||||
|
||||
function fetchIPv4v6(
|
||||
url: URL,
|
||||
init: RequestInit,
|
||||
v6 = false
|
||||
): Promise<Response> {
|
||||
const hostname = url.hostname
|
||||
|
||||
if (!v6 && hostname === 'localhost') {
|
||||
url.hostname = '127.0.0.1'
|
||||
}
|
||||
return fetch(url, init).catch((err) => {
|
||||
if (err.code === 'ECONNREFUSED' && !v6) {
|
||||
return fetchIPv4v6(url, init, true)
|
||||
}
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
async function addRevalidationHeader(
|
||||
res: ServerResponse,
|
||||
{
|
||||
|
@ -212,7 +194,7 @@ async function createRedirectRenderResult(
|
|||
// }
|
||||
|
||||
try {
|
||||
const headResponse = await fetchIPv4v6(fetchUrl, {
|
||||
const headResponse = await fetch(fetchUrl, {
|
||||
method: 'HEAD',
|
||||
headers: forwardedHeaders,
|
||||
next: {
|
||||
|
@ -224,7 +206,7 @@ async function createRedirectRenderResult(
|
|||
if (
|
||||
headResponse.headers.get('content-type') === RSC_CONTENT_TYPE_HEADER
|
||||
) {
|
||||
const response = await fetchIPv4v6(fetchUrl, {
|
||||
const response = await fetch(fetchUrl, {
|
||||
method: 'GET',
|
||||
headers: forwardedHeaders,
|
||||
next: {
|
||||
|
|
|
@ -36,6 +36,7 @@ import type {
|
|||
} from './future/route-modules/app-route/module'
|
||||
|
||||
import { format as formatUrl, parse as parseUrl } from 'url'
|
||||
import { formatHostname } from './lib/format-hostname'
|
||||
import { getRedirectStatus } from '../lib/redirect-status'
|
||||
import { isEdgeRuntime } from '../lib/is-edge-runtime'
|
||||
import {
|
||||
|
@ -268,6 +269,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
protected clientReferenceManifest?: ClientReferenceManifest
|
||||
protected nextFontManifest?: NextFontManifest
|
||||
public readonly hostname?: string
|
||||
public readonly fetchHostname?: string
|
||||
public readonly port?: number
|
||||
|
||||
protected abstract getPublicDir(): string
|
||||
|
@ -367,6 +369,10 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
// values from causing issues as this can be user provided
|
||||
this.nextConfig = conf as NextConfigComplete
|
||||
this.hostname = hostname
|
||||
if (this.hostname) {
|
||||
// we format the hostname so that it can be fetched
|
||||
this.fetchHostname = formatHostname(this.hostname)
|
||||
}
|
||||
this.port = port
|
||||
this.distDir =
|
||||
process.env.NEXT_RUNTIME === 'edge'
|
||||
|
|
|
@ -476,7 +476,7 @@ export default class DevServer extends Server {
|
|||
) {
|
||||
if (this.isRenderWorker) {
|
||||
await invokeIpcMethod({
|
||||
hostname: this.hostname,
|
||||
fetchHostname: this.fetchHostname,
|
||||
method: 'logErrorWithOriginalStack',
|
||||
args: [errorToJSON(err as Error), type],
|
||||
ipcPort: process.env.__NEXT_PRIVATE_ROUTER_IPC_PORT,
|
||||
|
@ -732,7 +732,7 @@ export default class DevServer extends Server {
|
|||
}) {
|
||||
if (this.isRenderWorker) {
|
||||
await invokeIpcMethod({
|
||||
hostname: this.hostname,
|
||||
fetchHostname: this.fetchHostname,
|
||||
method: 'ensurePage',
|
||||
args: [opts],
|
||||
ipcPort: process.env.__NEXT_PRIVATE_ROUTER_IPC_PORT,
|
||||
|
@ -797,7 +797,7 @@ export default class DevServer extends Server {
|
|||
protected async getFallbackErrorComponents(): Promise<LoadComponentsReturnType | null> {
|
||||
if (this.isRenderWorker) {
|
||||
await invokeIpcMethod({
|
||||
hostname: this.hostname,
|
||||
fetchHostname: this.fetchHostname,
|
||||
method: 'getFallbackErrorComponents',
|
||||
args: [],
|
||||
ipcPort: process.env.__NEXT_PRIVATE_ROUTER_IPC_PORT,
|
||||
|
@ -813,7 +813,7 @@ export default class DevServer extends Server {
|
|||
async getCompilationError(page: string): Promise<any> {
|
||||
if (this.isRenderWorker) {
|
||||
const err = await invokeIpcMethod({
|
||||
hostname: this.hostname,
|
||||
fetchHostname: this.fetchHostname,
|
||||
method: 'getCompilationError',
|
||||
args: [page],
|
||||
ipcPort: process.env.__NEXT_PRIVATE_ROUTER_IPC_PORT,
|
||||
|
|
12
packages/next/src/server/lib/format-hostname.ts
Normal file
12
packages/next/src/server/lib/format-hostname.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { isIPv6 } from './is-ipv6'
|
||||
|
||||
/**
|
||||
* Formats a hostname so that it is a valid host that can be fetched by wrapping
|
||||
* IPv6 hosts with brackets.
|
||||
* @param hostname
|
||||
* @returns
|
||||
*/
|
||||
|
||||
export function formatHostname(hostname: string): string {
|
||||
return isIPv6(hostname) ? `[${hostname}]` : hostname
|
||||
}
|
42
packages/next/src/server/lib/is-ipv6.ts
Normal file
42
packages/next/src/server/lib/is-ipv6.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
// Regex from `node/lib/internal/net.js`: https://github.com/nodejs/node/blob/9fc57006c27564ed7f75eee090eca86786508f51/lib/internal/net.js#L19-L29
|
||||
// License included below:
|
||||
// Copyright Joyent, Inc. and other Node contributors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
// persons to whom the Software is furnished to do so, subject to the
|
||||
// following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included
|
||||
// in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
const v4Seg = '(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])'
|
||||
const v4Str = `(${v4Seg}[.]){3}${v4Seg}`
|
||||
const v6Seg = '(?:[0-9a-fA-F]{1,4})'
|
||||
const IPv6Reg = new RegExp(
|
||||
'^(' +
|
||||
`(?:${v6Seg}:){7}(?:${v6Seg}|:)|` +
|
||||
`(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|` +
|
||||
`(?:${v6Seg}:){5}(?::${v4Str}|(:${v6Seg}){1,2}|:)|` +
|
||||
`(?:${v6Seg}:){4}(?:(:${v6Seg}){0,1}:${v4Str}|(:${v6Seg}){1,3}|:)|` +
|
||||
`(?:${v6Seg}:){3}(?:(:${v6Seg}){0,2}:${v4Str}|(:${v6Seg}){1,4}|:)|` +
|
||||
`(?:${v6Seg}:){2}(?:(:${v6Seg}){0,3}:${v4Str}|(:${v6Seg}){1,5}|:)|` +
|
||||
`(?:${v6Seg}:){1}(?:(:${v6Seg}){0,4}:${v4Str}|(:${v6Seg}){1,6}|:)|` +
|
||||
`(?::((?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:))` +
|
||||
')(%[0-9a-zA-Z-.:]{1,})?$'
|
||||
)
|
||||
|
||||
export function isIPv6(s: string) {
|
||||
return IPv6Reg.test(s)
|
||||
}
|
|
@ -2,6 +2,7 @@ import type { RequestHandler } from '../next'
|
|||
|
||||
// this must come first as it includes require hooks
|
||||
import { initializeServerWorker } from './setup-server-worker'
|
||||
import { formatHostname } from './format-hostname'
|
||||
import next from '../next'
|
||||
import { PropagateToWorkersField } from './router-utils/types'
|
||||
|
||||
|
@ -98,7 +99,7 @@ export async function initialize(opts: {
|
|||
...opts,
|
||||
_routerWorker: opts.workerType === 'router',
|
||||
_renderWorker: opts.workerType === 'render',
|
||||
hostname: hostname === '0.0.0.0' ? 'localhost' : hostname,
|
||||
hostname,
|
||||
customServer: false,
|
||||
httpServer: server,
|
||||
port: opts.port,
|
||||
|
@ -111,7 +112,8 @@ export async function initialize(opts: {
|
|||
|
||||
result = {
|
||||
port,
|
||||
hostname: hostname === '0.0.0.0' ? '127.0.0.1' : hostname,
|
||||
hostname: formatHostname(hostname),
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -17,8 +17,10 @@ import { proxyRequest } from './router-utils/proxy-request'
|
|||
import { getResolveRoutes } from './router-utils/resolve-routes'
|
||||
import { PERMANENT_REDIRECT_STATUS } from '../../shared/lib/constants'
|
||||
import { splitCookiesString, toNodeOutgoingHttpHeaders } from '../web/utils'
|
||||
import { formatHostname } from './format-hostname'
|
||||
import { signalFromNodeResponse } from '../web/spec-extension/adapters/next-request'
|
||||
import { getMiddlewareRouteMatcher } from '../../shared/lib/router/utils/middleware-route-matcher'
|
||||
import type { RenderWorker } from './router-server'
|
||||
import { pipeReadable } from '../pipe-readable'
|
||||
|
||||
type RouteResult =
|
||||
|
@ -56,7 +58,7 @@ export async function makeResolver(
|
|||
dir: string,
|
||||
nextConfig: NextConfigComplete,
|
||||
middleware: MiddlewareConfig,
|
||||
serverAddr: Partial<ServerAddress>
|
||||
{ hostname = 'localhost', port = 3000 }: Partial<ServerAddress>
|
||||
) {
|
||||
const fsChecker = await setupFsCheck({
|
||||
dir,
|
||||
|
@ -68,6 +70,8 @@ export async function makeResolver(
|
|||
dir,
|
||||
!!nextConfig.experimental.appDir
|
||||
)
|
||||
// we format the hostname so that it can be fetched
|
||||
const fetchHostname = formatHostname(hostname)
|
||||
|
||||
fsChecker.ensureCallback(async (item) => {
|
||||
let result: string | null = null
|
||||
|
@ -108,7 +112,10 @@ export async function makeResolver(
|
|||
}
|
||||
: {}
|
||||
|
||||
const middlewareServerPort = await new Promise((resolve) => {
|
||||
const middlewareServerAddr = await new Promise<{
|
||||
hostname: string
|
||||
port: number
|
||||
}>((resolve) => {
|
||||
const srv = http.createServer(async (req, res) => {
|
||||
const cloneableBody = getCloneableBody(req)
|
||||
try {
|
||||
|
@ -128,9 +135,7 @@ export async function makeResolver(
|
|||
basePath: nextConfig.basePath,
|
||||
trailingSlash: nextConfig.trailingSlash,
|
||||
},
|
||||
url: `http://${serverAddr.hostname || 'localhost'}:${
|
||||
serverAddr.port || 3000
|
||||
}${req.url}`,
|
||||
url: `http://${fetchHostname}:${port}${req.url}`,
|
||||
body: cloneableBody,
|
||||
signal: signalFromNodeResponse(res),
|
||||
},
|
||||
|
@ -172,7 +177,14 @@ export async function makeResolver(
|
|||
}
|
||||
})
|
||||
srv.on('listening', () => {
|
||||
resolve((srv.address() as any).port)
|
||||
const srvAddr = srv.address()
|
||||
if (!srvAddr || typeof srvAddr === 'string') {
|
||||
throw new Error("Failed to determine middleware's host/port.")
|
||||
}
|
||||
resolve({
|
||||
hostname: srvAddr.address,
|
||||
port: srvAddr.port,
|
||||
})
|
||||
})
|
||||
srv.listen(0)
|
||||
})
|
||||
|
@ -191,8 +203,8 @@ export async function makeResolver(
|
|||
nextConfig,
|
||||
{
|
||||
dir,
|
||||
port: serverAddr.port || 3000,
|
||||
hostname: serverAddr.hostname,
|
||||
port,
|
||||
hostname,
|
||||
isNodeDebugging: false,
|
||||
dev: true,
|
||||
workerType: 'render',
|
||||
|
@ -201,15 +213,15 @@ export async function makeResolver(
|
|||
pages: {
|
||||
async initialize() {
|
||||
return {
|
||||
port: middlewareServerPort,
|
||||
hostname: '127.0.0.1',
|
||||
port: middlewareServerAddr.port,
|
||||
hostname: formatHostname(middlewareServerAddr.hostname),
|
||||
}
|
||||
},
|
||||
async deleteCache() {},
|
||||
async clearModuleContext() {},
|
||||
async deleteAppClientCache() {},
|
||||
async propagateServerField() {},
|
||||
} as any,
|
||||
} as Partial<RenderWorker> as any,
|
||||
},
|
||||
{} as any
|
||||
)
|
||||
|
|
|
@ -11,6 +11,7 @@ import { getCloneableBody } from '../../body-streams'
|
|||
import { filterReqHeaders, ipcForbiddenHeaders } from '../server-ipc/utils'
|
||||
import { Header } from '../../../lib/load-custom-routes'
|
||||
import { stringifyQuery } from '../../server-route-utils'
|
||||
import { formatHostname } from '../format-hostname'
|
||||
import { toNodeOutgoingHttpHeaders } from '../../web/utils'
|
||||
import { invokeRequest } from '../server-ipc/invoke-request'
|
||||
import { isAbortError } from '../../pipe-readable'
|
||||
|
@ -137,7 +138,9 @@ export function getResolveRoutes(
|
|||
const initUrl = (config.experimental as any).trustHostHeader
|
||||
? `https://${req.headers.host || 'localhost'}${req.url}`
|
||||
: opts.port
|
||||
? `${protocol}://${opts.hostname || 'localhost'}:${opts.port}${req.url}`
|
||||
? `${protocol}://${formatHostname(opts.hostname || 'localhost')}:${
|
||||
opts.port
|
||||
}${req.url}`
|
||||
: req.url || ''
|
||||
|
||||
addRequestMeta(req, '__NEXT_INIT_URL', initUrl)
|
||||
|
|
|
@ -66,7 +66,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') {
|
||||
|
|
|
@ -11,11 +11,6 @@ export const invokeRequest = async (
|
|||
},
|
||||
readableBody?: Readable | ReadableStream
|
||||
) => {
|
||||
// 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 invokeHeaders = filterReqHeaders(
|
||||
{
|
||||
'cache-control': '',
|
||||
|
@ -24,7 +19,7 @@ export const invokeRequest = async (
|
|||
ipcForbiddenHeaders
|
||||
) as IncomingMessage['headers']
|
||||
|
||||
return await fetch(parsedTargetUrl.toString(), {
|
||||
return await fetch(targetUrl, {
|
||||
headers: invokeHeaders as any as Headers,
|
||||
method: requestInit.method,
|
||||
redirect: 'manual',
|
||||
|
|
|
@ -29,13 +29,13 @@ export const deserializeErr = (serializedErr: any) => {
|
|||
}
|
||||
|
||||
export async function invokeIpcMethod({
|
||||
hostname = '127.0.0.1',
|
||||
fetchHostname = 'localhost',
|
||||
method,
|
||||
args,
|
||||
ipcPort,
|
||||
ipcKey,
|
||||
}: {
|
||||
hostname?: string
|
||||
fetchHostname?: string
|
||||
method: string
|
||||
args: any[]
|
||||
ipcPort?: string
|
||||
|
@ -43,7 +43,7 @@ export async function invokeIpcMethod({
|
|||
}): Promise<any> {
|
||||
if (ipcPort) {
|
||||
const res = await invokeRequest(
|
||||
`http://${hostname}:${ipcPort}?key=${ipcKey}&method=${
|
||||
`http://${fetchHostname}:${ipcPort}?key=${ipcKey}&method=${
|
||||
method as string
|
||||
}&args=${encodeURIComponent(JSON.stringify(args))}`,
|
||||
{
|
||||
|
|
|
@ -87,25 +87,30 @@ export async function initializeServerWorker(
|
|||
upgradeHandler(req, socket, upgrade)
|
||||
})
|
||||
}
|
||||
const hostname =
|
||||
!opts.hostname || opts.hostname === 'localhost'
|
||||
? '0.0.0.0'
|
||||
: opts.hostname
|
||||
let hostname = opts.hostname || 'localhost'
|
||||
|
||||
server.on('listening', async () => {
|
||||
try {
|
||||
const addr = server.address()
|
||||
const host = addr
|
||||
? typeof addr === 'object'
|
||||
? addr.address
|
||||
: addr
|
||||
: undefined
|
||||
const port = addr && typeof addr === 'object' ? addr.port : 0
|
||||
|
||||
if (!port) {
|
||||
console.error(`Invariant failed to detect render worker port`, addr)
|
||||
if (!port || !host) {
|
||||
console.error(
|
||||
`Invariant failed to detect render worker host/port`,
|
||||
addr
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
resolve({
|
||||
server,
|
||||
port,
|
||||
hostname,
|
||||
hostname: host,
|
||||
})
|
||||
} catch (err) {
|
||||
return reject(err)
|
||||
|
|
|
@ -3,10 +3,10 @@ import '../node-polyfill-fetch'
|
|||
import type { IncomingMessage, ServerResponse } from 'http'
|
||||
|
||||
import http from 'http'
|
||||
import { isIPv6 } from 'net'
|
||||
import * as Log from '../../build/output/log'
|
||||
import setupDebug from 'next/dist/compiled/debug'
|
||||
import { getDebugPort } from './utils'
|
||||
import { formatHostname } from './format-hostname'
|
||||
import { initialize } from './router-server'
|
||||
import {
|
||||
WorkerRequestHandler,
|
||||
|
@ -147,25 +147,26 @@ export async function startServer({
|
|||
}
|
||||
})
|
||||
|
||||
let targetHost = hostname
|
||||
const isNodeDebugging = checkIsNodeDebugging()
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.on('listening', async () => {
|
||||
const addr = server.address()
|
||||
const actualHostname = formatHostname(
|
||||
typeof addr === 'object'
|
||||
? addr?.address || hostname || 'localhost'
|
||||
: addr
|
||||
)
|
||||
|
||||
const formattedHostname =
|
||||
!hostname || hostname === '0.0.0.0'
|
||||
? 'localhost'
|
||||
: actualHostname === '[::]'
|
||||
? '[::1]'
|
||||
: actualHostname
|
||||
|
||||
port = typeof addr === 'object' ? addr?.port || port : port
|
||||
|
||||
let host = !hostname || hostname === '0.0.0.0' ? 'localhost' : hostname
|
||||
|
||||
let normalizedHostname = hostname || '0.0.0.0'
|
||||
|
||||
if (isIPv6(hostname)) {
|
||||
host = host === '::' ? '[::1]' : `[${host}]`
|
||||
normalizedHostname = `[${hostname}]`
|
||||
}
|
||||
targetHost = host
|
||||
|
||||
const appUrl = `http://${host}:${port}`
|
||||
const appUrl = `http://${formattedHostname}:${port}`
|
||||
|
||||
if (isNodeDebugging) {
|
||||
const debugPort = getDebugPort()
|
||||
|
@ -177,11 +178,7 @@ export async function startServer({
|
|||
}
|
||||
|
||||
if (logReady) {
|
||||
Log.ready(
|
||||
`started server on ${normalizedHostname}${
|
||||
(port + '').startsWith(':') ? '' : ':'
|
||||
}${port}, url: ${appUrl}`
|
||||
)
|
||||
Log.ready(`started server on ${actualHostname}:${port}, url: ${appUrl}`)
|
||||
// expose the main port to render workers
|
||||
process.env.PORT = port + ''
|
||||
}
|
||||
|
@ -202,7 +199,7 @@ export async function startServer({
|
|||
dir,
|
||||
port,
|
||||
isDev,
|
||||
hostname: targetHost,
|
||||
hostname,
|
||||
minimalMode,
|
||||
isNodeDebugging: Boolean(isNodeDebugging),
|
||||
keepAliveTimeout,
|
||||
|
@ -219,6 +216,6 @@ export async function startServer({
|
|||
|
||||
resolve()
|
||||
})
|
||||
server.listen(port, hostname === 'localhost' ? '0.0.0.0' : hostname)
|
||||
server.listen(port, hostname)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -425,7 +425,7 @@ export default class NextNodeServer extends BaseServer {
|
|||
trustHostHeader: this.nextConfig.experimental.trustHostHeader,
|
||||
allowedRevalidateHeaderKeys:
|
||||
this.nextConfig.experimental.allowedRevalidateHeaderKeys,
|
||||
hostname: this.hostname,
|
||||
hostname: this.fetchHostname,
|
||||
minimalMode: this.minimalMode,
|
||||
dev: this.renderOpts.dev === true,
|
||||
query,
|
||||
|
@ -515,7 +515,7 @@ export default class NextNodeServer extends BaseServer {
|
|||
|
||||
if (this.isRenderWorker) {
|
||||
const invokeRes = await invokeRequest(
|
||||
`http://${this.hostname || '127.0.0.1'}:${this.port}${
|
||||
`http://${this.fetchHostname || 'localhost'}:${this.port}${
|
||||
newReq.url || ''
|
||||
}`,
|
||||
{
|
||||
|
@ -1496,7 +1496,7 @@ export default class NextNodeServer extends BaseServer {
|
|||
const locale = params.parsed.query.__nextLocale
|
||||
|
||||
url = `${getRequestMeta(params.request, '_protocol')}://${
|
||||
this.hostname
|
||||
this.fetchHostname
|
||||
}:${this.port}${locale ? `/${locale}` : ''}${params.parsed.pathname}${
|
||||
query ? `?${query}` : ''
|
||||
}`
|
||||
|
@ -1747,9 +1747,9 @@ export default class NextNodeServer extends BaseServer {
|
|||
|
||||
// When there are hostname and port we build an absolute URL
|
||||
const initUrl =
|
||||
this.hostname && this.port
|
||||
? `${protocol}://${this.hostname}:${this.port}${req.url}`
|
||||
: (this.nextConfig.experimental as any).trustHostHeader
|
||||
this.fetchHostname && this.port
|
||||
? `${protocol}://${this.fetchHostname}:${this.port}${req.url}`
|
||||
: this.nextConfig.experimental.trustHostHeader
|
||||
? `https://${req.headers.host || 'localhost'}${req.url}`
|
||||
: req.url
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ interface Options {
|
|||
}
|
||||
|
||||
const REGEX_LOCALHOST_HOSTNAME =
|
||||
/(?!^https?:\/\/)(127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|::1|localhost)/
|
||||
/(?!^https?:\/\/)(127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|\[::1\]|localhost)/
|
||||
|
||||
function parseURL(url: string | URL, base?: string | URL) {
|
||||
return new URL(
|
||||
|
|
|
@ -67,7 +67,7 @@ if (!(globalThis as any).isNextStart) {
|
|||
const appPort = await findPort()
|
||||
server = await initNextServerScript(
|
||||
testServer,
|
||||
/Listening on/,
|
||||
/ready started server on/,
|
||||
{
|
||||
...process.env,
|
||||
PORT: appPort.toString(),
|
||||
|
|
|
@ -363,7 +363,7 @@ describe('CLI Usage', () => {
|
|||
}
|
||||
)
|
||||
try {
|
||||
await check(() => output, new RegExp(`on 0.0.0.0:${port}`))
|
||||
await check(() => output, new RegExp(`on \\[::\\]:${port}`))
|
||||
await check(() => output, new RegExp(`http://localhost:${port}`))
|
||||
} finally {
|
||||
await killApp(app)
|
||||
|
@ -383,12 +383,12 @@ describe('CLI Usage', () => {
|
|||
}
|
||||
)
|
||||
try {
|
||||
await check(() => output, new RegExp(`on 0.0.0.0:${port}`))
|
||||
await check(() => output, new RegExp(`on \\[::\\]:${port}`))
|
||||
await check(() => output, new RegExp(`http://localhost:${port}`))
|
||||
} finally {
|
||||
await killApp(app)
|
||||
}
|
||||
const matches = /on 0.0.0.0:(\d+)/.exec(output)
|
||||
const matches = /on \[::\]:(\d+)/.exec(output)
|
||||
expect(matches).not.toBe(null)
|
||||
|
||||
const _port = parseInt(matches[1])
|
||||
|
@ -408,8 +408,8 @@ describe('CLI Usage', () => {
|
|||
},
|
||||
})
|
||||
try {
|
||||
await check(() => output, /on 0.0.0.0:(\d+)/)
|
||||
const matches = /on 0.0.0.0:(\d+)/.exec(output)
|
||||
await check(() => output, /on \[::\]:(\d+)/)
|
||||
const matches = /on \[::\]:(\d+)/.exec(output)
|
||||
const _port = parseInt(matches[1])
|
||||
expect(matches).not.toBe(null)
|
||||
// Regression test: port 0 was interpreted as if no port had been
|
||||
|
@ -434,7 +434,7 @@ describe('CLI Usage', () => {
|
|||
}
|
||||
)
|
||||
try {
|
||||
await check(() => output, new RegExp(`on 0.0.0.0:${port}`))
|
||||
await check(() => output, new RegExp(`on \\[::\\]:${port}`))
|
||||
await check(() => output, new RegExp(`http://localhost:${port}`))
|
||||
} finally {
|
||||
await killApp(app)
|
||||
|
@ -451,7 +451,7 @@ describe('CLI Usage', () => {
|
|||
env: { NODE_OPTIONS: '--inspect' },
|
||||
})
|
||||
try {
|
||||
await check(() => output, new RegExp(`on 0.0.0.0:${port}`))
|
||||
await check(() => output, new RegExp(`on \\[::\\]:${port}`))
|
||||
await check(() => output, new RegExp(`http://localhost:${port}`))
|
||||
} finally {
|
||||
await killApp(app)
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
} from 'fs'
|
||||
import { promisify } from 'util'
|
||||
import http from 'http'
|
||||
import https from 'https'
|
||||
import path from 'path'
|
||||
|
||||
import spawn from 'cross-spawn'
|
||||
|
@ -162,19 +161,7 @@ export function fetchViaHTTP(
|
|||
opts?: RequestInit
|
||||
): Promise<Response> {
|
||||
const url = query ? withQuery(pathname, query) : pathname
|
||||
return fetch(getFullUrl(appPort, url), {
|
||||
// in node.js v17 fetch favors IPv6 but Next.js is
|
||||
// listening on IPv4 by default so force IPv4 DNS resolving
|
||||
agent: (parsedUrl) => {
|
||||
if (parsedUrl.protocol === 'https:') {
|
||||
return new https.Agent({ family: 4 })
|
||||
}
|
||||
if (parsedUrl.protocol === 'http:') {
|
||||
return new http.Agent({ family: 4 })
|
||||
}
|
||||
},
|
||||
...opts,
|
||||
})
|
||||
return fetch(getFullUrl(appPort, url), opts)
|
||||
}
|
||||
|
||||
export function renderViaHTTP(
|
||||
|
|
|
@ -103,7 +103,7 @@ describe('pnpm support', () => {
|
|||
)
|
||||
server = await initNextServerScript(
|
||||
path.join(standaloneDir, 'server.js'),
|
||||
/Listening/,
|
||||
/ready started server on/,
|
||||
{
|
||||
...process.env,
|
||||
PORT: appPort,
|
||||
|
|
|
@ -76,7 +76,7 @@ describe('should set-up next', () => {
|
|||
appPort = await findPort()
|
||||
server = await initNextServerScript(
|
||||
testServer,
|
||||
/Listening on/,
|
||||
/ready started server on/,
|
||||
{
|
||||
...process.env,
|
||||
PORT: appPort,
|
||||
|
|
|
@ -112,7 +112,7 @@ describe('should set-up next', () => {
|
|||
appPort = await findPort()
|
||||
server = await initNextServerScript(
|
||||
testServer,
|
||||
/Listening on/,
|
||||
/ready started server on/,
|
||||
{
|
||||
...process.env,
|
||||
PORT: appPort,
|
||||
|
|
|
@ -119,7 +119,7 @@ describe('should set-up next', () => {
|
|||
appPort = await findPort()
|
||||
server = await initNextServerScript(
|
||||
testServer,
|
||||
/Listening on/,
|
||||
/ready started server on/,
|
||||
{
|
||||
...process.env,
|
||||
PORT: appPort,
|
||||
|
@ -1289,7 +1289,7 @@ describe('should set-up next', () => {
|
|||
appPort = await findPort()
|
||||
server = await initNextServerScript(
|
||||
testServer,
|
||||
/Listening on/,
|
||||
/ready started server on/,
|
||||
{
|
||||
...process.env,
|
||||
PORT: appPort,
|
||||
|
|
|
@ -63,7 +63,7 @@ describe('minimal-mode-response-cache', () => {
|
|||
appPort = await findPort()
|
||||
server = await initNextServerScript(
|
||||
testServer,
|
||||
/Listening on/,
|
||||
/ready started server on/,
|
||||
{
|
||||
...process.env,
|
||||
HOSTNAME: '',
|
||||
|
@ -141,9 +141,12 @@ describe('minimal-mode-response-cache', () => {
|
|||
expect(res2.headers.get('content-type')).toContain('text/html')
|
||||
})
|
||||
|
||||
it('should have correct "Listening on" log', async () => {
|
||||
expect(output).toContain(`Listening on port`)
|
||||
expect(output).toContain(`url: http://localhost:${appPort}`)
|
||||
it('should have correct "Started server on" log', async () => {
|
||||
expect(output).toContain(`started server on`)
|
||||
let pattern = new RegExp(
|
||||
`url: http://localhost:${appPort}|url: http://127.0.0.1:${appPort}|url: http://\\[::1\\]:${appPort}`
|
||||
)
|
||||
expect(output).toMatch(pattern)
|
||||
})
|
||||
|
||||
it('should have correct responses', async () => {
|
||||
|
|
|
@ -43,7 +43,7 @@ describe('type-module', () => {
|
|||
const appPort = await findPort()
|
||||
const server = await initNextServerScript(
|
||||
serverFile,
|
||||
/Listening on/,
|
||||
/ready started server on/,
|
||||
{ ...process.env, PORT: appPort.toString() },
|
||||
undefined,
|
||||
{ cwd: next.testDir }
|
||||
|
|
|
@ -197,12 +197,12 @@ it('consider 127.0.0.1 and variations as localhost', () => {
|
|||
const httpUrl = new NextURL('http://localhost:3000/hello')
|
||||
expect(new NextURL('http://127.0.0.1:3000/hello')).toStrictEqual(httpUrl)
|
||||
expect(new NextURL('http://127.0.1.0:3000/hello')).toStrictEqual(httpUrl)
|
||||
expect(new NextURL('http://::1:3000/hello')).toStrictEqual(httpUrl)
|
||||
expect(new NextURL('http://[::1]:3000/hello')).toStrictEqual(httpUrl)
|
||||
|
||||
const httpsUrl = new NextURL('https://localhost:3000/hello')
|
||||
expect(new NextURL('https://127.0.0.1:3000/hello')).toStrictEqual(httpsUrl)
|
||||
expect(new NextURL('https://127.0.1.0:3000/hello')).toStrictEqual(httpsUrl)
|
||||
expect(new NextURL('https://::1:3000/hello')).toStrictEqual(httpsUrl)
|
||||
expect(new NextURL('https://[::1]:3000/hello')).toStrictEqual(httpsUrl)
|
||||
})
|
||||
|
||||
it('allows to change the port', () => {
|
||||
|
|
Loading…
Reference in a new issue